@jx3box/jx3box-ui 2.3.5 → 2.3.8

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.
@@ -2,7 +2,7 @@
2
2
  .c-sidebar-left {
3
3
  .w(@aside-left - 40px);
4
4
  box-sizing: border-box;
5
- padding: 10px;
5
+ // padding: 10px;
6
6
  .pf;
7
7
  .z(600);
8
8
  left: 0;
@@ -1,5 +1,11 @@
1
1
  /* assets/css/single/author.less */
2
2
  .c-author {
3
+
4
+ padding:15px;
5
+
6
+ background-repeat: no-repeat;
7
+ background-size: 100% auto;
8
+
3
9
  .u-tips {
4
10
  .db;
5
11
  color: #555;
@@ -67,13 +73,53 @@
67
73
  }
68
74
 
69
75
  .u-interact {
70
- .mt(5px);
76
+ .mt(14px);
71
77
  display: flex;
72
78
  align-items: center;
73
- gap:8px;
79
+ gap: 12px;
80
+
81
+ .el-button {
82
+ margin: 0;
83
+ }
84
+
85
+ .u-btn.el-button {
86
+ min-width: 70px;
87
+ height: 28px;
88
+ padding: 0 12px;
89
+ border-radius: 5px;
90
+ border-color: #d6dbe4;
91
+ background: rgba(255, 255, 255, 0.72);
92
+ color: #4b5563;
93
+ font-size: 12px;
94
+ font-weight: 700;
95
+ box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
96
+ backdrop-filter: blur(6px);
97
+
98
+ &:hover,
99
+ &:focus {
100
+ border-color: fade(@v4primary, 40%);
101
+ background: rgba(255, 255, 255, 0.9);
102
+ color: @v4primary;
103
+ }
104
+
105
+ .el-icon {
106
+ font-size: 13px;
107
+ }
108
+ }
109
+
110
+ .u-btn.el-button.c-author-rss.is-subscribed {
111
+ border-color: #dcdfe6;
112
+ background: rgba(245, 247, 250, 0.9);
113
+ color: #a8abb2;
114
+ box-shadow: none;
74
115
 
75
- .el-button{
76
- margin:0;
116
+ &:hover,
117
+ &:focus {
118
+ border-color: #dcdfe6;
119
+ background: rgba(245, 247, 250, 0.9);
120
+ color: #909399;
121
+ box-shadow: none;
122
+ }
77
123
  }
78
124
  }
79
125
  .u-links {
@@ -84,18 +130,18 @@
84
130
  }
85
131
 
86
132
  // .u-btn {
87
- //
133
+ //
88
134
  // &:hover {
89
- //
135
+ //
90
136
  // color: @primary;
91
137
  // }
92
138
  // }
93
139
  }
94
140
 
95
141
  .c-sidebar-left .c-author {
96
- margin: 10px;
142
+ // margin: 10px;
97
143
  }
98
144
 
99
145
  .c-sidebar-right .c-author {
100
- margin: 15px;
146
+ // margin: 15px;
101
147
  }
@@ -0,0 +1 @@
1
+ <svg clip-rule="evenodd" fill-rule="evenodd" height="512" image-rendering="optimizeQuality" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" viewBox="0 0 1707 1707" width="512" xmlns="http://www.w3.org/2000/svg"><g id="Layer_x0020_1"><path d="m1128 1066c-312 0-564-253-564-564 0 311-253 564-564 564 311 0 564 252 564 563 0-311 252-563 564-563z" fill="#ffd93b"/><g fill="#ffb030"><path d="m1707 1357c-171 0-309-139-309-309 0 170-139 309-309 309 170 0 309 138 309 309 0-171 138-309 309-309z"/><path d="m1529 417c-208 0-376-168-376-376 0 208-168 376-376 376 208 0 376 168 376 376 0-208 168-376 376-376z"/></g></g></svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jx3box/jx3box-ui",
3
- "version": "2.3.5",
3
+ "version": "2.3.8",
4
4
  "description": "JX3BOX Vue3 UI",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -34,7 +34,7 @@
34
34
  "@element-plus/icons-vue": "^2.3.2",
35
35
  "@jx3box/jx3box-common": "^9.2.5",
36
36
  "@jx3box/jx3box-data": "^3.9.5",
37
- "@jx3box/jx3box-editor": "^3.2.5",
37
+ "@jx3box/jx3box-editor": "^3.2.7",
38
38
  "@jx3box/jx3box-emotion": "^1.3.1",
39
39
  "@jx3box/jx3box-macro": "^1.0.4",
40
40
  "@jx3box/jx3box-talent": "^1.3.14",
package/service/cms.js CHANGED
@@ -24,6 +24,18 @@ function getDecorationV2(params) {
24
24
  params,
25
25
  });
26
26
  }
27
+ function getUserSkin(user_id) {
28
+ return $cms().get(`/api/cms/user/skin`, {
29
+ params: {
30
+ user_id,
31
+ },
32
+ });
33
+ }
34
+ function getUserSkinBatch(params = {}) {
35
+ return $cms().get(`/api/cms/user/skin/batch`, {
36
+ params,
37
+ });
38
+ }
27
39
  function getDecorationJson() {
28
40
  let url = JX3BOX.__cdn + "design/decoration/index.json";
29
41
  return axios.get(url);
@@ -149,6 +161,8 @@ export {
149
161
  upload,
150
162
  getDecoration,
151
163
  getDecorationV2,
164
+ getUserSkin,
165
+ getUserSkinBatch,
152
166
  getDecorationJson,
153
167
  checkTeamMember,
154
168
  getSliders,
package/src/App.vue CHANGED
@@ -18,8 +18,8 @@
18
18
  <AdminDrop :post="community" :isCommunity="false" :user-id="8" :showMove="true" />
19
19
  </template>
20
20
  </breadcrumb>
21
- <LeftSidebar :open="true" :uid="8719">
22
- <Author :uid="8719" />
21
+ <LeftSidebar :open="true">
22
+ <Author :uid="8" />
23
23
  </LeftSidebar>
24
24
  <Main :withoutLeft="false" :withoutRight="false">
25
25
  <el-tabs v-model="tab" type="card">
@@ -101,7 +101,8 @@
101
101
  </el-tab-pane>
102
102
  <el-tab-pane label="文章内容" name="content">
103
103
  <el-radio-group v-model="post_id">
104
- <el-radio value="23240">临时测试</el-radio>
104
+ <el-radio value="19382">临时测试</el-radio>
105
+ <!-- <el-radio value="23240">临时测试</el-radio> -->
105
106
  <el-radio value="35605">Markdown</el-radio>
106
107
  <el-radio value="32035">仅小册</el-radio>
107
108
  <el-radio value="30017">仅联合创作者</el-radio>
@@ -232,8 +233,7 @@ export default {
232
233
  return {
233
234
  tab: "content",
234
235
 
235
- // post_id: "19382",
236
- post_id: "99808",
236
+ post_id: "19382",
237
237
  post: {},
238
238
  client: location.href.includes("origin") ? "origin" : "std",
239
239
  item1: null,
@@ -159,10 +159,9 @@ export default {
159
159
  .c-author-info {
160
160
  .u-author {
161
161
  .flex(y);
162
- gap:15px;
163
- // .db;
164
- .mb(10px);
162
+ gap:12px;
165
163
  .pr;
164
+ margin:5px 5px 10px 5px;
166
165
  }
167
166
  .u-avatar {
168
167
  // .fl;
@@ -215,7 +214,7 @@ export default {
215
214
  .u-bio {
216
215
  .fz(12px, 2);
217
216
  .break(3);
218
- color: #888;
217
+ color: #9aa7b5;
219
218
  padding: 0 5px;
220
219
  white-space: pre-line;
221
220
  }
@@ -96,7 +96,7 @@ export default {
96
96
  padding: 3px 2px 3px 5px;
97
97
  .nobreak;
98
98
  .fz(12px,2);
99
- color: @color;
99
+ color: #42566d;
100
100
  // border-bottom: 1px solid transparent;
101
101
  &:hover {
102
102
  color: @v4primary;
@@ -0,0 +1,189 @@
1
+ <template>
2
+ <div class="c-author-role" v-if="roles.length">
3
+ <div class="u-label">
4
+ <img svg-inline src="../../assets/img/author/star.svg" />
5
+ <span>{{ $jx3boxT("jx3boxUi.authorRole.title", "作者角色") }}</span>
6
+ </div>
7
+ <div class="u-roles">
8
+ <component
9
+ :is="item.url ? 'a' : 'span'"
10
+ class="u-role"
11
+ :href="item.url"
12
+ :target="item.url ? '_blank' : null"
13
+ :style="roleStyle(item)"
14
+ v-for="item in roles"
15
+ :key="item.key"
16
+ >
17
+ <img class="u-role-icon" v-if="item.icon" :src="item.icon" />
18
+ <span class="u-role-content">
19
+ <span class="u-role-name">{{ item.name }}</span>
20
+ <span class="u-role-server" v-if="item.server">@{{ item.server }}</span>
21
+ </span>
22
+ </component>
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <script>
28
+ import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
29
+ import { showSchoolIcon } from "@jx3box/jx3box-common/js/utils";
30
+ import i18nMixin from "../../i18n/mixin";
31
+
32
+ const { __Root, __cdn } = JX3BOX;
33
+
34
+ export default {
35
+ name: "AuthorRole",
36
+ mixins: [i18nMixin],
37
+ props: {
38
+ data: {
39
+ type: Object,
40
+ default: function () {
41
+ return {};
42
+ },
43
+ },
44
+ },
45
+ computed: {
46
+ roles: function () {
47
+ const roles = this.normalizeRoles(this.data?.public_roles);
48
+ return roles.map(this.normalizeRole).filter((item) => item.name);
49
+ },
50
+ },
51
+ methods: {
52
+ normalizeRoles: function (roles) {
53
+ if (!roles) return [];
54
+ if (Array.isArray(roles)) return roles;
55
+ if (typeof roles === "string") {
56
+ try {
57
+ const parsed = JSON.parse(roles);
58
+ return Array.isArray(parsed) ? parsed : [parsed];
59
+ } catch (e) {
60
+ return roles
61
+ .split(/[,,]/)
62
+ .map((item) => item.trim())
63
+ .filter(Boolean);
64
+ }
65
+ }
66
+ return [roles];
67
+ },
68
+ normalizeRole: function (role, index) {
69
+ if (typeof role === "string") {
70
+ return {
71
+ key: role + index,
72
+ name: role,
73
+ };
74
+ }
75
+
76
+ const info = role?.role_info || role?.info || {};
77
+ const name =
78
+ role?.name ||
79
+ role?.title ||
80
+ role?.label ||
81
+ role?.role_name ||
82
+ role?.display_name ||
83
+ role?.remark ||
84
+ info?.name ||
85
+ info?.title ||
86
+ info?.label ||
87
+ info?.role_name ||
88
+ "";
89
+
90
+ return {
91
+ key: role?.id || role?.key || role?.val || name + index,
92
+ name,
93
+ server: role?.server || role?.role_server || role?.jx3_server || info?.server || info?.role_server || "",
94
+ desc: role?.desc || role?.description || role?.summary || info?.desc || info?.description || "",
95
+ url: this.normalizeLink(role?.url || role?.link || role?.href || info?.url || info?.link || ""),
96
+ icon: this.normalizeIcon(role, info),
97
+ color: role?.color || info?.color || "",
98
+ backgroundColor: role?.background_color || role?.bg_color || info?.background_color || info?.bg_color || "",
99
+ borderColor: role?.border_color || info?.border_color || "",
100
+ };
101
+ },
102
+ normalizeAsset: function (url) {
103
+ if (!url) return "";
104
+ url = String(url).trim();
105
+ if (/^(https?:)?\/\//.test(url)) return url;
106
+ return __cdn + url.replace(/^\/+/, "");
107
+ },
108
+ normalizeIcon: function (role, info) {
109
+ const mount = role?.mount || role?.school || role?.school_id || info?.mount || info?.school || info?.school_id || "";
110
+ if (mount) return showSchoolIcon(mount);
111
+
112
+ return this.normalizeAsset(role?.icon || role?.logo || role?.image || info?.icon || info?.logo || "");
113
+ },
114
+ normalizeLink: function (url) {
115
+ if (!url) return "";
116
+ url = String(url).trim();
117
+ if (/^(https?:)?\/\//.test(url)) return url;
118
+ return __Root + url.replace(/^\/+/, "");
119
+ },
120
+ roleStyle: function (role) {
121
+ return {
122
+ color: role.color || null,
123
+ backgroundColor: role.backgroundColor || null,
124
+ borderColor: role.borderColor || null,
125
+ };
126
+ },
127
+ },
128
+ };
129
+ </script>
130
+
131
+ <style lang="less">
132
+ /* src/author/AuthorRole.vue */
133
+ .c-author-role {
134
+ .u-roles {
135
+ display: flex;
136
+ flex-direction: column;
137
+ gap: 7px;
138
+ margin-top: 2px;
139
+ }
140
+ .u-role {
141
+ .flex(y);
142
+ gap: 8px;
143
+ max-width: 100%;
144
+ min-height: 22px;
145
+ color: #56677a;
146
+ font-size: 13px;
147
+ line-height: 22px;
148
+ box-sizing: border-box;
149
+ text-decoration: none;
150
+ }
151
+ a.u-role {
152
+ &:hover {
153
+ .u-role-name {
154
+ color: @v4primary;
155
+ }
156
+ .u-role-server {
157
+ color: fade(@v4primary, 65%);
158
+ }
159
+ }
160
+ }
161
+ .u-role-icon {
162
+ .size(22px);
163
+ flex-shrink: 0;
164
+ object-fit: contain;
165
+ }
166
+ .u-role-content {
167
+ min-width: 0;
168
+ display: flex;
169
+ align-items: baseline;
170
+ }
171
+ .u-role-name {
172
+ min-width: 0;
173
+ overflow: hidden;
174
+ text-overflow: ellipsis;
175
+ white-space: nowrap;
176
+ color: #42566d;
177
+ font-weight: 400;
178
+ font-size: 12px;
179
+ margin-right: 2px;
180
+ }
181
+ .u-role-server {
182
+ flex-shrink: 0;
183
+ // color: #9aa7b5;
184
+ color: @v4yellow;
185
+ font-size: 12px;
186
+ font-weight: 400;
187
+ }
188
+ }
189
+ </style>
@@ -132,25 +132,23 @@ export default {
132
132
  /* src/author/AuthorRss.vue */
133
133
  .c-author-rss.el-button {
134
134
  cursor: default;
135
+ transition: all 0.18s ease;
135
136
  &:hover,
136
137
  &.is-plain:hover,
137
138
  &.is-plain:focus {
138
139
  cursor: pointer;
139
- background-color: @light-pink;
140
- color: #fff;
141
- border-color: darken(@light-pink, 2%);
140
+ background-color: rgba(255, 51, 153, 0.08);
141
+ color: @v4primary;
142
+ border-color: fade(@v4primary, 36%);
143
+ box-shadow: 0 6px 14px rgba(255, 51, 153, 0.1);
142
144
  }
143
145
  .u-icon {
144
- .size(10px);
146
+ .size(11px);
145
147
  fill: currentColor;
146
148
  margin-right: 4px;
147
149
  }
148
150
  }
149
151
 
150
- .c-author-rss.el-button.is-subscribed {
151
- color: #999;
152
- }
153
-
154
152
  @media screen and (max-width: @phone) {
155
153
  .el-message-box {
156
154
  max-width: 60%;
@@ -95,25 +95,24 @@
95
95
  </time>
96
96
  </div>
97
97
  <div class="u-toolbar-right">
98
- <el-button class="u-admin u-filter" v-if="canAddWhite" link size="small" @click="setWhiteComment(true)">
99
- <img
100
- class="u-icon-filter"
101
- src="../../assets/img/editor/view.svg"
102
- :alt="$jx3boxT('jx3boxUi.commentContent.show', '显示')"
103
- />
98
+ <el-button
99
+ class="u-admin u-filter"
100
+ v-if="canAddWhite"
101
+ link
102
+ icon="View"
103
+ size="small"
104
+ @click="setWhiteComment(true)"
105
+ >
104
106
  {{ $jx3boxT("jx3boxUi.commentContent.show", "显示") }}</el-button
105
107
  >
106
108
  <el-button
107
109
  class="u-admin u-filter"
108
110
  v-if="canRemoveWhite"
109
111
  link
112
+ icon="Hide"
110
113
  size="small"
111
114
  @click="setWhiteComment(false)"
112
- ><img
113
- class="u-icon-filter"
114
- src="../../assets/img/comment/hide.svg"
115
- :alt="$jx3boxT('jx3boxUi.commentContent.hidden', '隐藏')"
116
- />{{ $jx3boxT("jx3boxUi.commentContent.hidden", "隐藏") }}</el-button
115
+ >{{ $jx3boxT("jx3boxUi.commentContent.hidden", "隐藏") }}</el-button
117
116
  >
118
117
  </div>
119
118
  </div>
@@ -450,10 +449,6 @@ export default {
450
449
  align-items: center;
451
450
  gap: 5px;
452
451
  }
453
- .u-icon-filter {
454
- width: 16px;
455
- height: 16px;
456
- }
457
452
  }
458
453
  .u-date {
459
454
  color: #c0c4cc;
@@ -73,20 +73,26 @@ import CommentContent from "./CommentContent.vue";
73
73
  import ReplyList from "./ReplyList.vue";
74
74
  import { POST, DELETE, GET } from "../../service/comment";
75
75
  import CommentAvatar from "../comment/Avatar.vue";
76
- import { getDecorationV2 } from "../../service/cms";
77
76
  import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
78
77
  import i18nMixin from "../../i18n/mixin";
79
78
  const { __cdn } = JX3BOX;
80
- const DECORATION_KEY = "decoration_comment_";
81
- const DECORATION_EMPTY_VALUE = "no";
82
79
  const DECORATION_POSITION_MAP = {
83
80
  lt: "left top",
81
+ mt: "center top",
84
82
  ct: "center top",
85
83
  rt: "right top",
84
+ lm: "left center",
86
85
  lc: "left center",
86
+ ml: "left center",
87
+ mm: "center center",
88
+ cm: "center center",
89
+ o: "center center",
87
90
  cc: "center center",
91
+ rm: "right center",
92
+ mr: "right center",
88
93
  rc: "right center",
89
94
  lb: "left bottom",
95
+ mb: "center bottom",
90
96
  cb: "center bottom",
91
97
  rb: "right bottom",
92
98
  };
@@ -117,6 +123,10 @@ export default {
117
123
  type: Boolean,
118
124
  default: false,
119
125
  },
126
+ skin: {
127
+ type: Object,
128
+ default: null,
129
+ },
120
130
  },
121
131
  components: {
122
132
  CommentContent,
@@ -133,8 +143,6 @@ export default {
133
143
  pageTotal: 1,
134
144
  total: 0,
135
145
  },
136
- decoration: null,
137
- decorationPosition: "",
138
146
  };
139
147
  },
140
148
  computed: {
@@ -142,10 +150,13 @@ export default {
142
150
  return this.item.userId;
143
151
  },
144
152
  decorationStyles() {
145
- return this.decoration
153
+ const image = this.normalizeDecorationImage(this.skin?.image);
154
+ return image
146
155
  ? {
147
- backgroundImage: `url(${this.decoration})`,
148
- backgroundPosition: this.decorationPosition,
156
+ backgroundImage: `url(${image})`,
157
+ backgroundPosition: this.resolveDecorationPosition(this.skin?.position),
158
+ backgroundRepeat: "no-repeat",
159
+ backgroundSize: "100% auto",
149
160
  borderRadius: "8px",
150
161
  }
151
162
  : null;
@@ -153,17 +164,8 @@ export default {
153
164
  },
154
165
  created() {
155
166
  this.replyList = this.item.reply || [];
156
- this.getDecoration();
157
167
  },
158
168
  methods: {
159
- resolveDecorationDetail(decoration) {
160
- if (!decoration) {
161
- return null;
162
- }
163
-
164
- const decorations = Array.isArray(decoration.decorations) ? decoration.decorations : [];
165
- return decorations.find((item) => item && item.image) || null;
166
- },
167
169
  normalizeDecorationImage(image) {
168
170
  if (!image) {
169
171
  return "";
@@ -177,45 +179,7 @@ export default {
177
179
  return __cdn + url.replace(/^\/+/, "");
178
180
  },
179
181
  resolveDecorationPosition(position) {
180
- return DECORATION_POSITION_MAP[position] || position || "";
181
- },
182
- setDecoration(decoration) {
183
- const decorationDetail = this.resolveDecorationDetail(decoration);
184
- const image = this.normalizeDecorationImage(decorationDetail?.image);
185
- if (!image) {
186
- return false;
187
- }
188
-
189
- this.decoration = image;
190
- this.decorationPosition = this.resolveDecorationPosition(decorationDetail.position);
191
- return true;
192
- },
193
- getDecoration() {
194
- let decoration_local = sessionStorage.getItem(DECORATION_KEY + this.uid);
195
- if (decoration_local == DECORATION_EMPTY_VALUE) return;
196
- if (decoration_local) {
197
- try {
198
- let decoration_parse = JSON.parse(decoration_local);
199
- if (decoration_parse && this.setDecoration(decoration_parse)) {
200
- return;
201
- }
202
- sessionStorage.removeItem(DECORATION_KEY + this.uid);
203
- } catch (err) {
204
- sessionStorage.removeItem(DECORATION_KEY + this.uid);
205
- }
206
- }
207
- getDecorationV2({ using: 1, user_id: this.uid, type: "comment", subtype: "pc_comment" }).then((res) => {
208
- let decorationList = res.data.data || [];
209
- let decoration = decorationList.find(
210
- (item) => item.type == "comment" && this.resolveDecorationDetail(item)
211
- );
212
- if (decoration && this.setDecoration(decoration)) {
213
- sessionStorage.setItem(DECORATION_KEY + this.uid, JSON.stringify(decoration));
214
- return;
215
- }
216
-
217
- sessionStorage.setItem(DECORATION_KEY + this.uid, DECORATION_EMPTY_VALUE);
218
- });
182
+ return DECORATION_POSITION_MAP[position] || "right top";
219
183
  },
220
184
  profileLink: function (uid) {
221
185
  return authorLink(uid);
@@ -39,6 +39,7 @@ import { getMenu } from "../../service/header";
39
39
  import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
40
40
  import User from "@jx3box/jx3box-common/js/user";
41
41
  import i18nMixin from "../../i18n/mixin";
42
+ import { getConfig } from "../../service/cms";
42
43
  // import manageIcon from "@/assets/img/components/common/header/manage.svg";
43
44
  const { __imgPath } = JX3BOX;
44
45
  const defaultPanel = [
@@ -90,16 +91,22 @@ export default {
90
91
  if (item?.key) return this.$jx3boxT(`jx3boxUi.commonHeader.panel.${item.key}`, item.label || item.key);
91
92
  return item?.label || "";
92
93
  },
93
- loadPanel: function () {
94
+ loadPanel: async function () {
94
95
  try {
95
96
  const panel = JSON.parse(sessionStorage.getItem("panel"));
97
+ let config = await getConfig({ key: "important_notice_url" });
96
98
  if (panel) {
97
99
  this.panel = panel;
98
100
  const item = this.panel?.find((i) => i.meta);
99
101
  this.initMeta(item);
100
102
  } else {
101
103
  getMenu("panel").then((res) => {
102
- this.panel = res.data?.data?.val;
104
+ this.panel = res.data?.data?.val?.map(item => {
105
+ return {
106
+ ...item,
107
+ link: item.remark == 'feature' ? config.val : item.link
108
+ };
109
+ });
103
110
  const item = this.panel?.find((i) => i.meta);
104
111
  this.initMeta(item);
105
112
  sessionStorage.setItem("panel", JSON.stringify(this.panel));
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="c-author">
2
+ <div class="c-author" :style="decorationStyles">
3
3
  <AuthorInfo :uid="uid" :anonymous="anonymous" @ready="installModules" />
4
4
  <template v-if="data">
5
5
  <div class="u-interact">
@@ -12,6 +12,7 @@
12
12
  >
13
13
  </div>
14
14
  <!-- <AuthorLink class="u-block u-links" :uid="uid" :data="data" /> -->
15
+ <AuthorRole class="u-block u-roles" :data="data" />
15
16
  <AuthorMedals class="u-block u-trophy" :uid="uid" />
16
17
  <!-- <AuthorTeams class="u-block u-teams" :uid="uid" /> -->
17
18
  <!-- <AuthorFans class="u-block u-fans" :uid="uid" /> -->
@@ -27,12 +28,33 @@ import AuthorInfo from "../author/AuthorInfo.vue";
27
28
  // import AuthorFollow from "../author/AuthorFollow.vue";
28
29
  // import AuthorGift from "../author/AuthorGift.vue";
29
30
  // import AuthorFans from "../author/AuthorFans.vue";
31
+ import AuthorRole from "../author/AuthorRole.vue";
30
32
  import AuthorMedals from "../author/AuthorMedals.vue";
31
33
  // import AuthorTeams from "../author/AuthorTeams.vue";
32
34
  import AuthorPosts from "../author/AuthorPosts.vue";
33
35
  import AuthorRss from "../author/AuthorRss.vue";
36
+ import { getUserSkin } from "../../service/cms";
34
37
  import i18nMixin from "../../i18n/mixin";
35
38
  const jx3box = require("@jx3box/jx3box-common/data/jx3box.json");
39
+ const { __cdn } = jx3box;
40
+ const SKIN_POSITION_MAP = {
41
+ lt: "left top",
42
+ ct: "center top",
43
+ mt: "center top",
44
+ rt: "right top",
45
+ lc: "left center",
46
+ ml: "left center",
47
+ cc: "center center",
48
+ cm: "center center",
49
+ mm: "center center",
50
+ o: "center center",
51
+ rc: "right center",
52
+ mr: "right center",
53
+ lb: "left bottom",
54
+ cb: "center bottom",
55
+ mb: "center bottom",
56
+ rb: "right bottom",
57
+ };
36
58
  export default {
37
59
  name: "AuthorComp",
38
60
  mixins: [i18nMixin],
@@ -49,8 +71,30 @@ export default {
49
71
  data: function () {
50
72
  return {
51
73
  data: "",
74
+ sidebarSkin: "",
75
+ sidebarSkinPosition: "",
52
76
  };
53
77
  },
78
+ computed: {
79
+ decorationStyles() {
80
+ return this.sidebarSkin
81
+ ? {
82
+ backgroundImage: `url(${this.sidebarSkin})`,
83
+ backgroundPosition: this.sidebarSkinPosition,
84
+ }
85
+ : null;
86
+ },
87
+ },
88
+ watch: {
89
+ uid() {
90
+ this.sidebarSkin = "";
91
+ this.sidebarSkinPosition = "";
92
+ this.loadSidebarSkin();
93
+ },
94
+ },
95
+ created() {
96
+ this.loadSidebarSkin();
97
+ },
54
98
  methods: {
55
99
  installModules: function (data) {
56
100
  if (this.anonymous == 1) {
@@ -61,12 +105,60 @@ export default {
61
105
  onMessage: function () {
62
106
  window.open("/dashboard/letter?receiver=" + this.uid, "_blank");
63
107
  },
108
+ resolveSkinDetail(record) {
109
+ if (!record) {
110
+ return null;
111
+ }
112
+
113
+ const skins = Array.isArray(record.skins) ? record.skins : [];
114
+ return skins.find((item) => item && item.subtype === "pc_sidebar" && item.image) || null;
115
+ },
116
+ normalizeSkinImage(image) {
117
+ if (!image) {
118
+ return "";
119
+ }
120
+
121
+ const url = String(image).trim();
122
+ if (/^(https?:)?\/\//.test(url)) {
123
+ return url;
124
+ }
125
+
126
+ return __cdn + url.replace(/^\/+/, "");
127
+ },
128
+ resolveSkinPosition(position) {
129
+ return SKIN_POSITION_MAP[position] || position || "";
130
+ },
131
+ setSidebarSkin(record) {
132
+ const detail = this.resolveSkinDetail(record);
133
+ const image = this.normalizeSkinImage(detail?.image);
134
+ if (!image) {
135
+ return false;
136
+ }
137
+
138
+ this.sidebarSkin = image;
139
+ this.sidebarSkinPosition = this.resolveSkinPosition(detail.position);
140
+ return true;
141
+ },
142
+ loadSidebarSkin() {
143
+ if (!this.uid || this.anonymous == 1) {
144
+ return;
145
+ }
146
+
147
+ getUserSkin(this.uid).then((res) => {
148
+ const records = res.data.data || [];
149
+ const record = records.find((item) => this.resolveSkinDetail(item));
150
+ if (record && this.setSidebarSkin(record)) {
151
+ return;
152
+ }
153
+ });
154
+ },
64
155
  },
65
156
  components: {
66
157
  AuthorInfo,
67
158
  // AuthorLink,
68
159
  // AuthorFollow,
69
160
  // AuthorGift,
161
+ AuthorRole,
70
162
  AuthorMedals,
71
163
  // AuthorTeams,
72
164
  AuthorPosts,
@@ -45,6 +45,7 @@
45
45
  :category="category"
46
46
  :power="commentPower"
47
47
  :support-video="supportVideo"
48
+ :skin="commentSkins[item.userId]"
48
49
  @deleteComment="deleteComment"
49
50
  @setTopComment="setTopComment"
50
51
  @setStarComment="setStarComment"
@@ -87,9 +88,11 @@ import { showAvatar, authorLink } from "@jx3box/jx3box-common/js/utils";
87
88
  import CommentInputForm from "../comment/CommentInputForm.vue";
88
89
  import CommentWithReply from "../comment/CommentWithReply.vue";
89
90
  import { GET, POST, DELETE, PUT, getOrderMode, setOrderMode } from "../../service/comment";
90
- import { getConfig } from "../../service/cms";
91
+ import { getConfig, getUserSkinBatch } from "../../service/cms";
91
92
  import User from "@jx3box/jx3box-common/js/user";
92
93
  import i18nMixin from "../../i18n/mixin";
94
+ const COMMENT_SKIN_TYPE = "comment";
95
+ const COMMENT_SKIN_SUBTYPE = "pc_comment";
93
96
  export default {
94
97
  name: "CommentComp",
95
98
  mixins: [i18nMixin],
@@ -138,6 +141,7 @@ export default {
138
141
  orderByLikes: false,
139
142
  openWhiteList: false,
140
143
  loading: false,
144
+ commentSkins: {},
141
145
 
142
146
  showMask: false,
143
147
  };
@@ -239,6 +243,7 @@ export default {
239
243
  .then((resp) => {
240
244
  this.commentList = resp.data || [];
241
245
  this.pager = resp.page;
246
+ this.loadCommentSkins(this.commentList);
242
247
  })
243
248
  .catch(() => {})
244
249
  .finally(() => {
@@ -281,6 +286,48 @@ export default {
281
286
  showAvatar: function (val) {
282
287
  return showAvatar(val, 144);
283
288
  },
289
+ collectCommentUserIds(list = []) {
290
+ return Array.from(
291
+ new Set(
292
+ list
293
+ .map((item) => Number(item?.userId || 0))
294
+ .filter((uid) => Number.isInteger(uid) && uid > 0)
295
+ )
296
+ );
297
+ },
298
+ selectCommentSkin(records = []) {
299
+ const skins = records.flatMap((record) => (Array.isArray(record?.skins) ? record.skins : []));
300
+ return (
301
+ skins.find((item) => item?.theme === "all" && item?.image) ||
302
+ skins.find((item) => !item?.theme && item?.image) ||
303
+ skins.find((item) => item?.image) ||
304
+ null
305
+ );
306
+ },
307
+ loadCommentSkins(list = []) {
308
+ const userIds = this.collectCommentUserIds(list);
309
+ if (!userIds.length) {
310
+ this.commentSkins = {};
311
+ return;
312
+ }
313
+
314
+ getUserSkinBatch({
315
+ user_ids: userIds.join(","),
316
+ type: COMMENT_SKIN_TYPE,
317
+ subtype: COMMENT_SKIN_SUBTYPE,
318
+ })
319
+ .then((res) => {
320
+ const data = res?.data?.data || {};
321
+ this.commentSkins = userIds.reduce((acc, uid) => {
322
+ const records = data[uid] || data[String(uid)] || [];
323
+ acc[uid] = this.selectCommentSkin(records);
324
+ return acc;
325
+ }, {});
326
+ })
327
+ .catch(() => {
328
+ this.commentSkins = {};
329
+ });
330
+ },
284
331
  reloadPower() {
285
332
  GET(`${this.baseAPI}/i-am-author`)
286
333
  .then((power) => {
@@ -0,0 +1,34 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const root = path.resolve(__dirname, "..");
5
+ const service = fs.readFileSync(path.join(root, "service/cms.js"), "utf8");
6
+ const author = fs.readFileSync(path.join(root, "src/single/Author.vue"), "utf8");
7
+ const authorInfo = fs.readFileSync(path.join(root, "src/author/AuthorInfo.vue"), "utf8");
8
+ const authorStyle = fs.readFileSync(path.join(root, "assets/css/single/author.less"), "utf8");
9
+
10
+ function assert(condition, message) {
11
+ if (!condition) {
12
+ throw new Error(message);
13
+ }
14
+ }
15
+
16
+ assert(service.includes("/api/cms/user/skin"), "user skin service should request the active skin endpoint");
17
+
18
+ assert(author.includes("getUserSkin"), "Author wrapper should request user skin data directly");
19
+
20
+ assert(!authorInfo.includes("getUserSkin"), "AuthorInfo should not request user skin data");
21
+
22
+ assert(author.includes('item.subtype === "pc_sidebar"'), "Author should consume the pc_sidebar skin part");
23
+
24
+ assert(author.includes("record.skins"), "Author should resolve active skin resources from skins[]");
25
+
26
+ assert(author.includes("backgroundPosition: this.sidebarSkinPosition"), "Author should apply skin position");
27
+
28
+ assert(author.includes('rt: "right top"'), "Author should map rt to right top");
29
+
30
+ assert(authorStyle.includes("background-repeat: no-repeat"), "Author skin background should not repeat");
31
+
32
+ assert(authorStyle.includes("background-size: 100% auto"), "Author skin background should use full width and auto height");
33
+
34
+ console.log("author pc sidebar skin checks passed");
@@ -3,9 +3,9 @@ const path = require("path");
3
3
 
4
4
  const root = path.resolve(__dirname, "..");
5
5
  const service = fs.readFileSync(path.join(root, "service/cms.js"), "utf8");
6
+ const commentList = fs.readFileSync(path.join(root, "src/single/Comment.vue"), "utf8");
6
7
  const component = fs.readFileSync(path.join(root, "src/comment/CommentWithReply.vue"), "utf8");
7
8
  const leftSidebar = fs.readFileSync(path.join(root, "src/LeftSidebar.vue"), "utf8");
8
- const app = fs.readFileSync(path.join(root, "src/App.vue"), "utf8");
9
9
 
10
10
  function assert(condition, message) {
11
11
  if (!condition) {
@@ -15,11 +15,21 @@ function assert(condition, message) {
15
15
 
16
16
  assert(service.includes("/api/cms/user/decoration/v2"), "getDecoration should support the decoration v2 endpoint");
17
17
 
18
- assert(component.includes("getDecorationV2"), "comment decoration should request the v2 decoration endpoint");
18
+ assert(service.includes("/api/cms/user/skin/batch"), "comment skin should support the batch user skin endpoint");
19
19
 
20
- assert(component.includes('subtype: "pc_comment"'), "comment decoration should request the pc_comment subtype");
20
+ assert(commentList.includes("getUserSkinBatch"), "comment list should request user skins in batch");
21
21
 
22
- assert(component.includes("resolveDecorationDetail"), "comment decoration should resolve v2 decorations payload");
22
+ assert(commentList.includes('type: COMMENT_SKIN_TYPE'), "comment skin request should include the comment type");
23
+
24
+ assert(commentList.includes('subtype: COMMENT_SKIN_SUBTYPE'), "comment skin request should include the pc_comment subtype");
25
+
26
+ assert(commentList.includes(":skin=\"commentSkins[item.userId]\""), "comment list should pass per-user skin to items");
27
+
28
+ assert(!component.includes("getDecorationV2"), "comment item should not request the legacy decoration v2 endpoint");
29
+
30
+ assert(component.includes("skin:"), "comment item should accept a skin prop");
31
+
32
+ assert(!component.includes("resolveDecorationDetail"), "comment item should not resolve legacy v2 decorations payload");
23
33
 
24
34
  assert(component.includes("normalizeDecorationImage"), "comment decoration should normalize returned image URLs");
25
35
 
@@ -48,9 +58,4 @@ assert(
48
58
  "left sidebar decoration should not depend on the old val/sidebar.png path"
49
59
  );
50
60
 
51
- assert(
52
- app.includes('<LeftSidebar :open="true" :uid="8719">') && app.includes('<Author :uid="8719" />'),
53
- "local author fixture should pass the same 8719 uid to LeftSidebar and Author"
54
- );
55
-
56
61
  console.log("decoration v2 checks passed");