Compare commits
404
Commits
f9bc0f69c4
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e48fdf61e | ||
|
|
1970db0b56 | ||
|
|
5f31c58df2 | ||
|
|
4a119358ca | ||
|
|
6928127bfa | ||
|
|
7885f42cff | ||
|
|
7c33482be7 | ||
|
|
7bdf7cf018 | ||
|
|
384d2b7aa0 | ||
|
|
4868de2cda | ||
|
|
9e8913ea0a | ||
|
|
f5e9420266 | ||
|
|
79909f449e | ||
|
|
d07cffe99e | ||
|
|
2c7262770b | ||
|
|
fac4a714e0 | ||
|
|
7a8279f8e7 | ||
|
|
fbb53c6890 | ||
|
|
7faa2d53d8 | ||
|
|
b124795d84 | ||
|
|
f53cdd4dc8 | ||
|
|
4c562428b4 | ||
|
|
ef7b5939ba | ||
|
|
6bcfbfb252 | ||
|
|
1dfcc38a83 | ||
|
|
8e74b60570 | ||
|
|
3ce5bfc20c | ||
|
|
b56b9bbe37 | ||
|
|
ed38dec314 | ||
|
|
3595935475 | ||
|
|
e6d9ff49a2 | ||
|
|
55423403a6 | ||
|
|
8c4aabb9a4 | ||
|
|
dcd306b428 | ||
|
|
bcba0f598a | ||
|
|
b1c3d86e44 | ||
|
|
6db34646ed | ||
|
|
6f29e3d865 | ||
|
|
7d78a33a9d | ||
|
|
6d01e7489a | ||
|
|
09b1e535e4 | ||
|
|
00ae62f62b | ||
|
|
41366e7fc4 | ||
|
|
d40906b4a3 | ||
|
|
8ad5ef9aff | ||
|
|
ba32e0ef9c | ||
|
|
02246c7ec8 | ||
|
|
6c3c0f7c6d | ||
|
|
451b04160b | ||
|
|
0007e78bfe | ||
|
|
f6fde18857 | ||
|
|
f14e6bd88d | ||
|
|
93e83e1835 | ||
|
|
3c07ef7a8d | ||
|
|
f80ca1e27d | ||
|
|
1549d5b25d | ||
|
|
030baa511e | ||
|
|
e30de6562c | ||
|
|
39aed0d430 | ||
|
|
427968a721 | ||
|
|
42cc64106d | ||
|
|
35bb96e82f | ||
|
|
5b16bc6e15 | ||
|
|
4e62b325d2 | ||
|
|
5c6b422866 | ||
|
|
1c95f94bca | ||
|
|
324bcfdadc | ||
|
|
df12f51199 | ||
|
|
4740078135 | ||
|
|
9c70d7929a | ||
|
|
869d437485 | ||
|
|
9805a371d8 | ||
|
|
d98f8d7975 | ||
|
|
1d6bc8f7cc | ||
|
|
b25ebc5fff | ||
|
|
1273a0138e | ||
|
|
4f6e17ba5e | ||
|
|
92a3c2b8c7 | ||
|
|
dc6dbc05d8 | ||
|
|
4083613dbe | ||
|
|
ed526709c8 | ||
|
|
b9000e1b93 | ||
|
|
54cc5301c3 | ||
|
|
d86a85e6a6 | ||
|
|
48e54dc115 | ||
|
|
b32b56f8bc | ||
|
|
8c553c7221 | ||
|
|
7b65b3c6d8 | ||
|
|
a9709377bb | ||
|
|
d7616b193e | ||
|
|
09f3a94cea | ||
|
|
7f1a0dbeb4 | ||
|
|
264015b7e9 | ||
|
|
b45fd161f0 | ||
|
|
b2811cc248 | ||
|
|
80aecab586 | ||
|
|
59ddf677b6 | ||
|
|
32ac2112b0 | ||
|
|
f9b7997ec2 | ||
|
|
bc26d62602 | ||
|
|
5732be847e | ||
|
|
a6776ffe0a | ||
|
|
1cd19434d1 | ||
|
|
f208424a26 | ||
|
|
479b324f95 | ||
|
|
1902ba1e87 | ||
|
|
fe3ef82718 | ||
|
|
6195508481 | ||
|
|
8f1bbf2411 | ||
|
|
117da19f16 | ||
|
|
1be52d75a0 | ||
|
|
18951b01d6 | ||
|
|
aa692f3c9f | ||
|
|
05c7ade3f5 | ||
|
|
7650c3537a | ||
|
|
a579a5b00a | ||
|
|
ea80cd96cc | ||
|
|
fc04bf65e6 | ||
|
|
4ea00f1322 | ||
|
|
4cdde50695 | ||
|
|
542daca457 | ||
|
|
4c13ef478c | ||
|
|
985f18c750 | ||
|
|
fd034ff055 | ||
|
|
17515cf9c5 | ||
|
|
8aa116ec43 | ||
|
|
b520d29368 | ||
|
|
f019760297 | ||
|
|
9b27508a5b | ||
|
|
1c501ff713 | ||
|
|
8e9d3a7e49 | ||
|
|
1df2822f01 | ||
|
|
b340b77d5a | ||
|
|
c3328e7279 | ||
|
|
d9d1122e9c | ||
|
|
7e86f2a821 | ||
|
|
b7a910b758 | ||
|
|
54ea83b262 | ||
|
|
794d981260 | ||
|
|
52749bf232 | ||
|
|
72e55d95fd | ||
|
|
293e1121c5 | ||
|
|
5d83a81b31 | ||
|
|
cfe806728d | ||
|
|
c152e00501 | ||
|
|
6ded62df92 | ||
|
|
9fbfab54c2 | ||
|
|
5dd7d14334 | ||
|
|
fcec5b996f | ||
|
|
ed1487d505 | ||
|
|
57ff405691 | ||
|
|
74021d26bb | ||
|
|
6da08d9d4f | ||
|
|
32172582ff | ||
|
|
7d61de70e3 | ||
|
|
fa89f43ac9 | ||
|
|
3179754670 | ||
|
|
116b6b2223 | ||
|
|
8843ae4261 | ||
|
|
135fb4eaa4 | ||
|
|
8d871c0828 | ||
|
|
174fbb54c5 | ||
|
|
63ab8b8fc9 | ||
|
|
6681b0c306 | ||
|
|
bbb41547e2 | ||
|
|
d71528f500 | ||
|
|
5febdb35c8 | ||
|
|
ac3ed9c956 | ||
|
|
a308f96837 | ||
|
|
13bde11dda | ||
|
|
b74381cfa2 | ||
|
|
b927b9ec7c | ||
|
|
3bd15feccb | ||
|
|
af4fb87e6d | ||
|
|
1a88498d57 | ||
|
|
d53d86332a | ||
|
|
44fa34fdff | ||
|
|
e7ca0ee5df | ||
|
|
d32bdc0581 | ||
|
|
ffd8d7d1b7 | ||
|
|
dbb7de41e9 | ||
|
|
761c84d70f | ||
|
|
81b60eb427 | ||
|
|
ef15416ef7 | ||
|
|
0cfd4c898b | ||
|
|
9a22c7eb8b | ||
|
|
de0e5cd481 | ||
|
|
a120f61e13 | ||
|
|
0d51beeaad | ||
|
|
573040381d | ||
|
|
7a0445fc4a | ||
|
|
0bc72616e5 | ||
|
|
064f4e0955 | ||
|
|
4b5098a753 | ||
|
|
05f16d5fd7 | ||
|
|
af8dccdc45 | ||
|
|
dcb80cff6e | ||
|
|
2e2558e5ce | ||
|
|
817ac9c647 | ||
|
|
e94ecdd7b1 | ||
|
|
659b77719c | ||
|
|
7f556892ad | ||
|
|
cb76b95b01 | ||
|
|
319ec2f47f | ||
|
|
4b18f7ef2c | ||
|
|
1461c9fe27 | ||
|
|
5641c40958 | ||
|
|
0bee2c605e | ||
|
|
c472045ce4 | ||
|
|
6dc9265260 | ||
|
|
008e63524d | ||
|
|
cff3083a91 | ||
|
|
4d69aaee50 | ||
|
|
ee39024549 | ||
|
|
5810693e01 | ||
|
|
0722c4f00e | ||
|
|
4f2edebab6 | ||
|
|
ce6fbb4e39 | ||
|
|
cf39e72126 | ||
|
|
740bd48f42 | ||
|
|
18e579a8c6 | ||
|
|
cdab7a5e5f | ||
|
|
bd01199930 | ||
|
|
7ed739615e | ||
|
|
60e4d67772 | ||
|
|
7287cfeaec | ||
|
|
f03d72c880 | ||
|
|
6e90a96a15 | ||
|
|
b368678662 | ||
|
|
40b9a3f36b | ||
|
|
6fd80be243 | ||
|
|
855e85e7db | ||
|
|
7e7bf6db23 | ||
|
|
6f815678ec | ||
|
|
8ff74f56ee | ||
|
|
a3b708f9d4 | ||
|
|
9cb5e7bea3 | ||
|
|
16b57dcf1c | ||
|
|
1875470ac7 | ||
|
|
c955fd35f2 | ||
|
|
36b3ba6f45 | ||
|
|
039fa6d863 | ||
|
|
26ce9a5904 | ||
|
|
31e6ba00af | ||
|
|
89ae462610 | ||
|
|
afd08885fc | ||
|
|
536ac67f3c | ||
|
|
7b83893cf6 | ||
|
|
83c164a427 | ||
|
|
5fd8c6fc31 | ||
|
|
b733e41993 | ||
|
|
88669f6989 | ||
|
|
80373746f8 | ||
|
|
d2034f5031 | ||
|
|
ad2290cbb7 | ||
|
|
c870f08395 | ||
|
|
80fb4068d1 | ||
|
|
01cd734989 | ||
|
|
f44180d06c | ||
|
|
c7d18d426c | ||
|
|
0ec405f566 | ||
|
|
dd320c99c1 | ||
|
|
fc75d7b810 | ||
|
|
b3012b3133 | ||
|
|
ab6c2339a0 | ||
|
|
3eb9cc739c | ||
|
|
e9a1a75259 | ||
|
|
d69e1d33c3 | ||
|
|
8023e2e472 | ||
|
|
918ca371be | ||
|
|
9e3267fef4 | ||
|
|
6081e8dcc9 | ||
|
|
c801ff5047 | ||
|
|
8460132feb | ||
|
|
732f615a09 | ||
|
|
738abe5102 | ||
|
|
915311dcb6 | ||
|
|
c200427358 | ||
|
|
7b5dcf16a1 | ||
|
|
2db2414270 | ||
|
|
365b2eafe5 | ||
|
|
ea88499996 | ||
|
|
f4ff3e16d4 | ||
|
|
c67d5ae4bc | ||
|
|
00a087b1a4 | ||
|
|
a0f59826fe | ||
|
|
e7e281f10c | ||
|
|
93579cbc9f | ||
|
|
4ea02f25ac | ||
|
|
1a9a2762f3 | ||
|
|
b447c41f13 | ||
|
|
8e1fd0a30f | ||
|
|
cf6dc24193 | ||
|
|
c8fdd81d6a | ||
|
|
27868b6131 | ||
|
|
ef718042d2 | ||
|
|
194280af72 | ||
|
|
50fac36169 | ||
|
|
2595bb02db | ||
|
|
fe57022550 | ||
|
|
962ab5c290 | ||
|
|
6dbe1fe4c6 | ||
|
|
12f60897d9 | ||
|
|
b75bf197a3 | ||
|
|
613b8a57a3 | ||
|
|
af72364078 | ||
|
|
224f86ec08 | ||
|
|
fc9bf27560 | ||
|
|
d8d0b5f290 | ||
|
|
ce429e1caf | ||
|
|
248912f610 | ||
|
|
f646fa8573 | ||
|
|
0cf4639d95 | ||
|
|
0a52bf36f9 | ||
|
|
346146b4fa | ||
|
|
f00c3faeaa | ||
|
|
59e9bbf8a6 | ||
|
|
1af51ab172 | ||
|
|
7b55cfb190 | ||
|
|
476e2f961d | ||
|
|
979639342a | ||
|
|
356f7fe5bd | ||
|
|
6037ca184b | ||
|
|
8ef2f7e49f | ||
|
|
5091da6a60 | ||
|
|
d0a3972ca7 | ||
|
|
2ae2b2fd07 | ||
|
|
90c1f7a62b | ||
|
|
8fcb136d55 | ||
|
|
d6e3b377b4 | ||
|
|
2b3e325d63 | ||
|
|
4caf81c9e2 | ||
|
|
88661e1574 | ||
|
|
67ec3c8aeb | ||
|
|
59901b9e10 | ||
|
|
5a17b399a0 | ||
|
|
4c6b02a6b9 | ||
|
|
56bbb63afd | ||
|
|
00817766a6 | ||
|
|
16d67c8a87 | ||
|
|
45dc909e4c | ||
|
|
20fde1d652 | ||
|
|
38bbd917a9 | ||
|
|
f61142ee9a | ||
|
|
cf4554e7c8 | ||
|
|
271e7f0518 | ||
|
|
b5f37d6d46 | ||
|
|
e506309434 | ||
|
|
89a9cf24e9 | ||
|
|
7fc1ac98de | ||
|
|
fa53a0f4f9 | ||
|
|
0374de3fec | ||
|
|
401d130f03 | ||
|
|
61667aa03c | ||
|
|
cded20ae02 | ||
|
|
f9ec4964f7 | ||
|
|
1355a9244f | ||
|
|
ea16f9c2fe | ||
|
|
ae793414be | ||
|
|
7761435a81 | ||
|
|
8d16ce6e02 | ||
|
|
1ce8e263c3 | ||
|
|
a366898f65 | ||
|
|
bd0abf891c | ||
|
|
cf6c11852e | ||
|
|
26ac8167f3 | ||
|
|
47bf9c8f0b | ||
|
|
f97614433c | ||
|
|
d6bc1067f9 | ||
|
|
eb14c4afd3 | ||
|
|
6f2adfaef1 | ||
|
|
7f60071460 | ||
|
|
63b5edd901 | ||
|
|
bd3d50c6c6 | ||
|
|
592bfb4fa2 | ||
|
|
b94c820361 | ||
|
|
ab495854d5 | ||
|
|
bc97c4ddc3 | ||
|
|
d49fd9f6a4 | ||
|
|
800baca0ca | ||
|
|
1c1d3f17b7 | ||
|
|
66f8bb933d | ||
|
|
a8060fa71d | ||
|
|
4d6b2c6045 | ||
|
|
c331b2aeef | ||
|
|
264f2c51b2 | ||
|
|
ed71314986 | ||
|
|
bd7586a3b7 | ||
|
|
95cdad5c5d | ||
|
|
7172fd942d | ||
|
|
d4bb43a275 | ||
|
|
505d9156fe | ||
|
|
2801a89c1f | ||
|
|
b798c7a45d | ||
|
|
edb60d08b4 | ||
|
|
456d57b2de | ||
|
|
9a40ad18c7 | ||
|
|
bbd5e97bac | ||
|
|
10c63e02d5 | ||
|
|
c315297d7f | ||
|
|
5201f2ab6f | ||
|
|
7a3f5d7c34 | ||
|
|
a4af024f32 | ||
|
|
11eda33e58 |
No files matched your search
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
/.workbuddy/
|
/.workbuddy/
|
||||||
|
|
||||||
# Project planning, internal docs, and local-only development files
|
# Project planning, internal docs, and local-only development files
|
||||||
/.aksrt-chrome-profile/
|
/.jio-chrome-profile/
|
||||||
/.specs/
|
/.specs/
|
||||||
/docs/
|
/docs/
|
||||||
|
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
# 用户公开主页通知按钮改为纯图标+未读角标
|
|
||||||
|
|
||||||
## 现状
|
|
||||||
- 目标按钮: `templates/parts/user/author-hero.php:144-149`(仅 `$is_own` 查看自己主页时输出),当前为铃铛图标 + "消息" 文字,无角标。
|
|
||||||
- 参照实现: 用户中心头部 `inc/helpers/user-center-renderers/profile-header.php:87-94` — `btn-ghost btn-sm btn-icon` 纯图标按钮,带 `aria-label`/`title`,未读数 > 0 时输出角标 `<span class="uc-header__action-badge">N</span>`(不封顶 99)。
|
|
||||||
- 未读数函数: `aksrt_get_user_unread_notification_count($user_id)`(`inc/modules/user-notifications.php:387`),模块经 `inc/bootstrap.php:78` 无条件加载,可直接调用。
|
|
||||||
- hero 卡片仅由 `page-user-home.php` 服务端静态渲染一次(DOM 无刷新只作用于 tab 面板),角标为静态服务端渲染,与用户中心行为一致。
|
|
||||||
- `.btn-icon` / `.btn-icon.btn-sm` 样式已存在于 `assets/css/core/buttons.css:129-136`,全局可用;但 `.uc-header__action-badge` 样式在 user-center 路由 CSS 中,不会在公开主页加载,需在 `assets/css/routes/user-home.css`(该页直接入队,无构建步骤)新增。
|
|
||||||
|
|
||||||
## 修改内容
|
|
||||||
|
|
||||||
### 1. `templates/parts/user/author-hero.php`
|
|
||||||
- 在第 58 行 `$messages_url` 之后新增:
|
|
||||||
```php
|
|
||||||
$unread_count = $is_own ? aksrt_get_user_unread_notification_count($user_id) : 0;
|
|
||||||
```
|
|
||||||
- 将消息按钮(144-149 行)整体替换为与用户中心一致的纯图标形式,类名沿用本文件 BEM 前缀:
|
|
||||||
```php
|
|
||||||
<a href="..." class="btn-ghost btn-sm btn-icon aksrt-author-hero__message-btn" aria-label="消息通知" title="消息通知">
|
|
||||||
<i class="fa fas fa-bell" aria-hidden="true"></i>
|
|
||||||
<?php if ($unread_count > 0) : ?>
|
|
||||||
<span class="aksrt-author-hero__action-badge"><?php echo esc_html((string) $unread_count); ?></span>
|
|
||||||
<?php endif; ?>
|
|
||||||
</a>
|
|
||||||
```
|
|
||||||
- 删除原 `<span>消息</span>` 文字输出,不保留旧结构。
|
|
||||||
|
|
||||||
### 2. `assets/css/routes/user-home.css`
|
|
||||||
在 `.aksrt-author-hero__follow-btn` 样式附近(约 167 行后)新增,视觉与用户中心角标(`user-center/shell.css:136-157`)完全一致:
|
|
||||||
```css
|
|
||||||
.aksrt-author-hero__message-btn {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-author-hero__action-badge {
|
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
right: -5px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0 4px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--color-error, #ef4444);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 验证
|
|
||||||
- `php -l templates/parts/user/author-hero.php`
|
|
||||||
- 运行 `node --test tests/widget-system/page-display-contract.test.cjs tests/widget-system/post-visual-contract.test.cjs`(已确认两者均未断言该按钮标记,应保持通过)
|
|
||||||
- 桌面/移动端检查: 图标按钮为 28px 方形(`.btn-icon.btn-sm`),角标绝对定位右上角,移动端居中布局不受影响(角标不占布局空间)
|
|
||||||
|
|
||||||
不写 function_exists 兜底、不保留文字版本、不添加迁移逻辑。
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
修复文章详情页分割线右侧三点图标不居中的问题
|
|
||||||
|
|
||||||
## 根因
|
|
||||||
|
|
||||||
三点按钮(`inc/helpers/aksrt-single-components.php:131-146`)为 `<button class="but cir post-drop-meta__toggle"><i class="fa fas fa-ellipsis-h"></i></button>`,位于 `templates/parts/single-author-bar.php` 中 `.single-meta-row` 分割线右端。`.but.cir` 是 `inline-flex; justify-content: center`,flex 居中的是子元素的 margin box。`assets/css/core/buttons.css` 中存在特异性冲突:
|
|
||||||
|
|
||||||
- 第 183 行 `.but > i[class*="fa-"]:not(.fa-angle-right) { margin-right: 0.25em; }` 特异性 (0,3,1)
|
|
||||||
- 第 225 行 `.but.cir > .fa { margin: 0; }`(本意的重置规则)特异性 (0,3,0),被压掉
|
|
||||||
|
|
||||||
导致图标右侧残留 0.25em 外边距(15px 字号下约 3.75px),三点整体左偏约 1.9px。这是全局 bug,所有 `.but.cir` 图标按钮都受影响。
|
|
||||||
|
|
||||||
## 修改内容
|
|
||||||
|
|
||||||
1. **`assets/css/core/buttons.css:224-225`** — 加强重置规则的特异性到 (0,4,0),稳定压过 (0,3,1) 的 margin 规则(不依赖源码顺序):
|
|
||||||
```css
|
|
||||||
.but.cir svg.icon,
|
|
||||||
.but.cir > .fa,
|
|
||||||
.but.cir > .fa:not(.fa-angle-right),
|
|
||||||
.but.cir > i[class*="fa-"]:not(.fa-angle-right) { margin: 0; }
|
|
||||||
```
|
|
||||||
附一行注释说明 `:not()` 变体是为压过 `.but > i[class*="fa-"]:not(.fa-angle-right)` 的 margin 规则。保留原 `.but.cir > .fa` 选择器以继续覆盖 fa-angle-right 图标。不改 main.css(旧的手工捆绑副本):public 捆绑包中 core/buttons.css 副本排在 main.min.css 之后,高特异性重置必然胜出。
|
|
||||||
|
|
||||||
2. **重建捆绑包** — 运行 `node tools/build-public-assets.mjs`(esbuild 已就绪),重新生成 `assets/css/build/public.css/.min.css` 等捆绑文件。
|
|
||||||
|
|
||||||
3. **新增契约测试** `tests/widget-system/circular-button-icon-margin-contract.test.cjs`:断言源文件 buttons.css 与构建产物 public.min.css 中包含加强后的重置选择器(防止特异性回归),沿用现有 `*.test.cjs` 契约测试模式。
|
|
||||||
|
|
||||||
## 验证
|
|
||||||
|
|
||||||
- `node --test tests/widget-system/circular-button-icon-margin-contract.test.cjs` 通过
|
|
||||||
- 在构建后的 `public.min.css` 中确认重置规则存在且位于 margin 规则之后
|
|
||||||
- 刷新文章详情页目视确认三点图标在圆形按钮内水平居中(同时全站其他 `.but.cir` 图标按钮居中恢复正常)
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
## 目标与边界
|
|
||||||
|
|
||||||
将“全局功能 → Email邮件”收敛为一套真正可用的邮件系统:后台完整展示 SMTP、发件品牌、认证/账号、内容互动、审核、私信、身份/账号处理、举报,以及订单、支付、提现、返佣、分成、转账等商城/资金类邮件;模板主题和正文默认直接显示,不再用空值加 placeholder 代替;所有真实发送统一由同一服务渲染和发送。
|
|
||||||
|
|
||||||
明确排除:
|
|
||||||
|
|
||||||
- 不实现论坛、版主、论坛帖子审核、回答采纳等论坛专属邮件。
|
|
||||||
- 不实现邮箱回滚、撤销链接、回滚令牌或旧邮箱恢复流程;邮箱变更邮件只做安全通知。
|
|
||||||
- 不做旧 option 的数据迁移,不保留新旧双路,不写兼容 adapter。
|
|
||||||
- 不做浏览器或真实视觉验收,只做代码、契约、语法和构建验证。
|
|
||||||
|
|
||||||
## 1. 建立唯一邮件配置与模板目录
|
|
||||||
|
|
||||||
- 以 `aksrt_settings` 的 Email section 作为唯一配置来源;SMTP、品牌、通知开关和模板都通过 `/settings/schema/global/email` 读写。
|
|
||||||
- 在邮件域建立集中式模板目录,统一声明每个非论坛事件的事件键、后台名称、默认开关、默认主题、默认 HTML 正文、占位符及说明。
|
|
||||||
- 邮件运行时和后台 Schema 均从该目录生成,避免默认文案继续散落在业务调用点。
|
|
||||||
- 对每个事件同时声明“开关、主题模板、正文模板”;模板正文在后台直接显示为非空默认值。数据库中已有空主题/正文时,仅在读取响应和表单初始化时解析为目录默认值,不执行迁移。
|
|
||||||
- HTML 模板和 `mail_more_content` 使用 `wp_kses_post()` 清理;主题、SMTP host/username/name 等按文本清理;邮箱地址使用 `sanitize_email()`;端口限制到合法范围;加密方式只允许 `ssl`、`tls` 或空值;SMTP 密码使用 secret 掩码/清除机制。
|
|
||||||
|
|
||||||
## 2. 完整重做 Email 后台 Schema
|
|
||||||
|
|
||||||
在 `src/Admin/Schema/GlobalSchema.php` 中由集中目录生成 Email section;让 `SchemaPage.vue` 对 `global.email` 使用服务端 Schema,删除前端重复的静态 Email schema 定义,避免 PHP/JS 两份字段漂移。
|
|
||||||
|
|
||||||
页面分组与事件如下:
|
|
||||||
|
|
||||||
1. **管理员邮件推送**
|
|
||||||
- 友情链接提交
|
|
||||||
- 新订单
|
|
||||||
- 提现申请
|
|
||||||
- 投稿待审核
|
|
||||||
- 身份认证申请
|
|
||||||
- 账号封禁申诉
|
|
||||||
- 举报提交
|
|
||||||
|
|
||||||
2. **用户邮件推送**
|
|
||||||
- 注册验证码
|
|
||||||
- 注册成功
|
|
||||||
- 免密登录验证码
|
|
||||||
- 密码重置验证码
|
|
||||||
- 邮箱/敏感操作验证码
|
|
||||||
- 邮箱变更安全通知
|
|
||||||
- 新订单/支付结果
|
|
||||||
- 提现处理结果
|
|
||||||
- 返佣到账
|
|
||||||
- 创作分成到账
|
|
||||||
- 收到积分/余额转账
|
|
||||||
- 评论通过审核
|
|
||||||
- 评论回复
|
|
||||||
- 投稿发布/驳回结果
|
|
||||||
- 手机号修改通知
|
|
||||||
- 身份认证审核结果
|
|
||||||
- 账号封禁/解封通知
|
|
||||||
- 举报处理结果
|
|
||||||
- 私信通知
|
|
||||||
- 私信邮件用户组限制
|
|
||||||
- 所有用户
|
|
||||||
- 所有会员
|
|
||||||
- 二级会员及以上
|
|
||||||
- 仅管理员
|
|
||||||
|
|
||||||
3. **自定义发件人/品牌**
|
|
||||||
- `mail_showname`,默认站点名
|
|
||||||
- `mail_title_prefix`,默认 `[站点名]`
|
|
||||||
- `mail_description`,默认“此信为系统邮件,请不要直接回复。”
|
|
||||||
- `mail_more_content`,默认显示“访问网站 / 联系站长”等基础 HTML 链接
|
|
||||||
|
|
||||||
4. **SMTP 配置**(沿用参考主题语义,但使用当前主题唯一字段契约)
|
|
||||||
- `mail_smtps`
|
|
||||||
- `mail_name`
|
|
||||||
- `mail_passwd`
|
|
||||||
- `mail_smtp_name`
|
|
||||||
- `mail_host`
|
|
||||||
- `mail_port`
|
|
||||||
- `mail_smtpauth`
|
|
||||||
- `mail_smtpsecure`
|
|
||||||
- SMTP 风险提示和依赖显示
|
|
||||||
|
|
||||||
5. **测试邮件**
|
|
||||||
- Email 页面内提供测试收件地址和发送操作。
|
|
||||||
- 有未保存修改时禁止测试并提示先保存,确保测试当前生效配置。
|
|
||||||
- 测试邮件走统一 HTML 包装、品牌和 SMTP 主线。
|
|
||||||
|
|
||||||
每个事件组直接显示“启用开关 + 主题模板 + HTML 正文模板 + 可用占位符说明”。论坛事件不进入 Schema、模板目录、默认值或运行时注册。
|
|
||||||
|
|
||||||
## 3. 实现参考主题式统一邮件渲染
|
|
||||||
|
|
||||||
重构 `src/Settings/Mail.php` 和 `inc/core-hooks/part-2.php`:
|
|
||||||
|
|
||||||
- 集中服务完成事件查询、开关判断、占位符替换、主题前缀、HTML 安全处理、品牌包装和 `wp_mail()` 调用。
|
|
||||||
- 复刻参考主题邮件信封:站点 Logo、主体内容、站点名称、底部说明、附加链接及现有 `assets/img/mail-bg.png` 背景图。
|
|
||||||
- 主题前缀只添加一次,并清除 WordPress 默认重复 `[站点名]` 前缀。
|
|
||||||
- 发件人名称保留明确优先级:`mail_showname` → 站点名;SMTP 发件地址和 SMTPAuth 使用同一 Email section 的 `mail_*` 字段。
|
|
||||||
- 将 `send_event_email()` 统一为事件键、收件人、占位符(必要时允许结构化上下文),默认模板不再由调用方重复传入。
|
|
||||||
- 未知事件失败关闭;无效收件地址不发送;事件关闭时不发送。
|
|
||||||
- 统一管理员收件人获取、去重和有效邮箱过滤。
|
|
||||||
|
|
||||||
## 4. 接通所有非论坛真实业务发送点
|
|
||||||
|
|
||||||
新增聚合通知注册类,基于当前主题已有 WordPress/AKSRT action 注册邮件发送;不凭空添加论坛功能。每个事件都复用集中模板目录和统一发送服务。
|
|
||||||
|
|
||||||
### 认证与账号
|
|
||||||
|
|
||||||
- 改造 `UserControllerAuthPhoneOperations.php`:注册验证码、免密登录验证码、密码重置验证码按独立事件发送;失败时删除验证码 transient,维持现有频率限制和安全行为。
|
|
||||||
- 注册成功在用户创建成功且用户对象有效时发送,提供用户名、站点名、个人中心/站点链接等占位符。
|
|
||||||
- 改造 `account-ajax.php`:敏感操作验证码、邮箱变更通知走统一服务;删除直接 `wp_mail()` fallback;不增加邮箱回滚。
|
|
||||||
- 手机号绑定/换绑成功后向有效邮箱发送通知。
|
|
||||||
|
|
||||||
### 内容互动与审核
|
|
||||||
|
|
||||||
- 评论通过审核、评论回复:接入 WordPress 评论状态/回复钩子,避免重复状态流转、自己回复自己和无效邮箱。
|
|
||||||
- 投稿待审核与发布/驳回结果:接入现有 `aksrt_new_posts_pending` 与文章状态转换,只处理当前主题实际支持的投稿内容,防止重复通知。
|
|
||||||
- 友情链接提交:接入当前友情链接写入成功动作,向有效管理员邮箱发送。
|
|
||||||
|
|
||||||
### 身份、账号、举报和私信
|
|
||||||
|
|
||||||
- 身份认证申请/审核、封禁申诉/处理、举报提交/处理:接入现有成功动作;若成功路径缺少动作,只在事务成功点增加明确的 `aksrt_*` action。
|
|
||||||
- 私信通知:接入现有消息/通知主线,只处理 private 类型;实现参考主题的用户组限制,并过滤无效或占位邮箱。
|
|
||||||
|
|
||||||
### 订单与资金业务(保留)
|
|
||||||
|
|
||||||
- 订单创建/支付成功/支付失败或状态变化:接入当前支付订单状态 action,分别通知管理员、买家及适用的订单关联用户。
|
|
||||||
- 提现申请与处理结果:接入当前提现申请、审核/完成/拒绝路径,通知管理员和申请用户。
|
|
||||||
- 返佣通知:接入当前 referral/佣金到账 action,向推荐人发送订单和佣金信息。
|
|
||||||
- 创作分成通知:接入当前 ledger/收益分配成功 action,向分成作者发送到账明细。
|
|
||||||
- 积分/余额转账通知:接入当前资产转账完成 action,向收款用户发送转账信息。
|
|
||||||
- 上述商城/资金事件只使用仓库中已存在的订单、支付、ledger、referral、资产动作和数据结构;若某事件没有可直接监听的成功动作,在对应现有事务完成点补充 action,不复制业务逻辑、不新建第二套交易流程。
|
|
||||||
|
|
||||||
论坛/版主/回答采纳动作不注册任何邮件监听器,也不产生对应模板字段。
|
|
||||||
|
|
||||||
## 5. 删除旧接口、旧存储与绕路代码
|
|
||||||
|
|
||||||
- 删除 `GlobalController` 的 `/settings/mail` 路由和 `rest_handle_mail()`,不再使用 `aksrt_mail_config`。
|
|
||||||
- 保留 `/mail/test` 作为动作接口,但重写为统一邮件服务测试;配置读写只有 Schema 路由。
|
|
||||||
- 删除前端 `getMailSettings()` / `saveMailSettings()`,只保留测试邮件 API 方法。
|
|
||||||
- 删除 `UserController` 的 `/settings/mail-templates` 路由、`handle_mail_templates()` 和 `aksrt_mail_templates` 契约。
|
|
||||||
- 删除 `Settings::get_smtp()` 对 `aksrt_mail_smtp` 的读取,并从 standalone/secret option 注册表移除该旧 option;SMTP 只读 Email section 的统一字段。
|
|
||||||
- 删除认证、账号和各业务模块中邮件服务不存在时的直接 `wp_mail()` fallback;正常业务只允许集中邮件服务内部调用 `wp_mail()`。
|
|
||||||
- 删除旧六事件空模板回退参数、重复前端 Email Schema 及不再使用的旧字段;不读取、不转换旧 option 值。
|
|
||||||
- 不删除数据库中的旧 option 值,但代码完全停止消费它们;不执行迁移脚本或双写。
|
|
||||||
|
|
||||||
## 6. 契约测试与构建验证
|
|
||||||
|
|
||||||
新增邮件系统契约测试/harness,覆盖:
|
|
||||||
|
|
||||||
- 后端 Email Schema 包含认证、账号、内容互动、审核、私信、身份/账号处理、举报、订单、支付、提现、返佣、分成、转账等事件。
|
|
||||||
- Schema 明确不包含 `bbs`、moderator、answer-adopted 及其它论坛字段。
|
|
||||||
- 每个主题和正文默认值非空;空存储读取时后台仍返回可见模板;模板占位符替换后无未知占位符。
|
|
||||||
- HTML 邮件信封包含品牌说明、附加内容、Logo 和背景图;主题前缀不重复;发件人和 SMTP 字段优先级稳定。
|
|
||||||
- SMTP 开关、认证、host、port、encryption、secret 掩码/清除行为正确。
|
|
||||||
- 事件关闭、未知事件、无效收件地址均不发送。
|
|
||||||
- 注册/登录/重置/邮箱变更路径不再绕过集中服务,发送失败时验证码缓存处理正确;不出现邮箱回滚入口或令牌。
|
|
||||||
- 评论、投稿、友情链接、身份认证、封禁、举报、私信及所有订单/资金事件分别绑定真实 action,并覆盖去重、无效邮箱和边界条件。
|
|
||||||
- 论坛事件不注册、不发送、不出现在后台;订单/支付/提现/返佣/分成/转账事件必须存在并可配置。
|
|
||||||
- 旧 `/settings/mail`、`/settings/mail-templates`、`aksrt_mail_config`、`aksrt_mail_templates`、`aksrt_mail_smtp` 和旧前端 API 不再存在。
|
|
||||||
- 全仓库业务代码中的 `wp_mail()` 只剩集中邮件服务内部调用。
|
|
||||||
|
|
||||||
执行验证:
|
|
||||||
|
|
||||||
1. 对所有变更 PHP 文件运行 `php -l`。
|
|
||||||
2. 运行新增邮件契约测试及受影响的 admin/UI 契约测试。
|
|
||||||
3. 在 `assets/admin` 执行 `npm run build`,由 Vite 清理旧 chunk 并更新完整 `assets/admin/build/`。
|
|
||||||
4. 使用 `rg` 做最终静态检查:无论坛邮件字段、无旧 option/API、无散落直接 `wp_mail()`,且订单/支付/提现/返佣/分成/转账事件均有 Schema、模板目录和发送入口。
|
|
||||||
5. 不启动浏览器、不截图、不做真实视觉效果验收。
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
## 目标与口径
|
|
||||||
|
|
||||||
将 AKSRT 小工具系统中 12 个小工具对照 Zibll(仅参考"(新)"模块,忽略一切"旧版即将删除")做全量 1:1 复刻:后台配置项(字段、类型、选项、默认值、依赖、min/max/step、分组小标题)与前端 DOM/样式/状态全部对齐参考主题。已确认口径:
|
|
||||||
- **专题全部移除**:本项目无 topics 分类法,12 个小工具范围内所有"专题限制"字段与 `PostQuery` 的 topics 查询分支一并删除,测试同步更新。
|
|
||||||
- **修正参考源码明显 bug 的文案**(如排序 label 颠倒、最近评论 style 默认值 'card' 不在选项中),数据值与行为保持一致。
|
|
||||||
- **省略 AKSRT 无法支撑的排序选项**(销售价格/积分售价/销售数量),排序值域 = views/like/comment_count/favorite/modified/date/rand。
|
|
||||||
- **新增"搜索式多选+拖拽排序"分类选择字段**(terms 类型)。
|
|
||||||
- 不写双路/迁移;清理被替换的死代码;不做 Docker 视觉验收;不改 37 个 slug 与 REST 路由形态。
|
|
||||||
|
|
||||||
## 复用基础(已核实)
|
|
||||||
|
|
||||||
- 文章 item DOM 契约已在 `inc/helpers/posts-renderer/markup.php`(posts-item/posts-mini/badges/meta,9.0 契约);`main.css` 已含 `.zci-*`、`.zps-*`、`.graphic*` 等 Zibll 同款 CSS(76 处命中);`frontend.css:179-228` 已有 tab/slider 的 Zibll 契约块;runtime `main.js` 已有 `.swiper-scroll`/`.new-swiper` 初始化。复刻 = 补齐缺失部分并对齐 markup,而非从零移植。
|
|
||||||
- 前次复刻先例:tab_posts/slider/buttons/notice 走 `FrontendRenderer` "特殊壳"路径(before_widget + reference_title + 内容自闭环),本次 12 个中需要 Zibll 壳的一并加入该列表。
|
|
||||||
|
|
||||||
## 实施步骤
|
|
||||||
|
|
||||||
### Phase A 基础设施
|
|
||||||
1. `FieldSchema.php`/`FormRenderer.php`:新增 `terms` 字段类型(动态 get_terms 分类选项、搜索过滤、拖拽排序、按选择顺序保存 int 数组;支持 taxonomy 参数以供合作伙伴的链接分类复用);spinner/slider 渲染 `unit` 后缀(此前 unit 是死键)。
|
|
||||||
2. `PostQuery.php`:删除 topics 分支;tabs 段支持 `zibpay_type`(数组,沿用 apply_pay_type 映射 1/2/5/6);orderby 值域与 allowlist 对齐。
|
|
||||||
3. `PostRenderer.php`:分页三态对齐 Zibll —— `''`→隐藏 `.ajax-pag.hide` 占位、`ajax`→"加载更多" `.ajax-pag .next-page.ajax-next`、`number`→`.pagenav.ajax-pag` 数字页码(全部带 `ajaxpager-target=".widget-ajaxpager"` 契约,REST 数据属性保留);`mini_opt` 应用于 posts;热榜从通用分支拆出专用渲染。
|
|
||||||
4. `FrontendRenderer.php`:特殊壳列表加入 `posts, oneline_posts, hot_posts, term_cards, term_aggregation, media_cards, combo_info, partners, recent_comments, posts_toc`。
|
|
||||||
5. `frontend.js`:移植/新增 ①IAS IntersectionObserver 懒加载(列表 `load_mode=ajax` 与加载更多自动触发,替代 Zibll lazysizes ias);②合作伙伴滚动初始化(测量 seed 组宽→克隆→`--zps-duration`,resize 重算,Zibll main.js:4204-4296);③幻灯片组合卡 footer 重算 `--cover-ratio`(widget-slider.php:848-875);④目录树逻辑(section-navs 移植:h1-h4 抓取、排除 `.item-heading`/`.no-nav`、`wznav_{i}`、scrollspy offset 105、>380px 折叠、hash 定位);保持 role=status/aria-busy/reduced-motion a11y 契约。
|
|
||||||
6. `admin.js`/`admin-core.js`/`admin.css`:terms 字段交互(搜索、拖拽排序、选序同步)。
|
|
||||||
|
|
||||||
### Phase B 逐个小工具(schema+render+CSS+JS+空态,均以参考文件为准逐字段移植)
|
|
||||||
|
|
||||||
| # | AKSRT slug | 参考定义(行号) | 要点 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| 1 | posts 文章列表 | widget-posts.php:1881-2006 | load_mode radio(detail/ajax 默认 ajax)、cat(reference_ids)、zibpay_type checkbox、limit_day、orderby、order、style(list/card/mini)+mini_opt、count 4..20 默认 12、paginate radio ''/ajax/number;壳 `widget-main-post mb20 style-X`(mini 加 `zib-widget posts-mini-lists`);删旧 tag_ids/paginate select |
|
|
||||||
| 2 | tab_posts 多栏目文章 | widget-posts.php:2008-2155 | tabs 子字段 title/cat/zibpay_type/limit_day/orderby/order;移除 topics;**删除现有 legacy `post_ajax_trigger` 双路**,翻页三态;mini 骨架含缩略图变体 |
|
|
||||||
| 3 | oneline_posts 横向滚动文章 | widget-posts.php:2157-2234 | cat/zibpay_type/orderby/order/style 默认 card/count;移除 limit_day/paginate/autoplay(参考无);swiper-scroll DOM 已对齐,清 data-autoplay |
|
|
||||||
| 4 | hot_posts 热榜文章 | widget-posts.php:1803-1851 + zib-posts-list.php:769-888 | orderby radio 4 榜单、limit_day、count 默认 6、target_blank;`zib-widget hot-posts`、TOP1 大图+linear-mask+TOP 徽章(jb-red/jb-yellow/b-gray)、行布局与计数文案(人收藏/人点赞/条讨论/人已阅读);删 show_thumbnail/通用样式字段 |
|
|
||||||
| 5 | term_cards 分类图文卡片 | widget-posts.php:1565-1694 + functions.php:1939-2046 | 模块级 obs_animation/animation_repeat(common.animation 关)、terms 选择器、type style-1..4、height_scale、pc_row(1/2/3/4/6/12 默认 2)/m_row、mask_opacity 默认 10、target_blank;`row gutters-5` + `graphic hover-zoom-img` + `--cover-height-scale` + graphic-mask,style-2/3/4 文案差异与 tooltip;新建共享助手 `aksrt_graphic_card()`(inc/helpers),term 分类默认封面回退 |
|
|
||||||
| 6 | term_aggregation → 改名"分类聚合卡片" | widget-posts.php:1696-1801 + zib-category.php:606-751 | 仅分类(去标签/专题):模块级动画、pc_row slider 1..2 默认 2、terms、orderby/order、count 1..20 默认 4、target_blank;移植 `zib-widget term-aggregation`(term-img+img-badge 浏览合计、term-title/meta/更多文章、按 orderby 的行内 meta、--delay 交错);Catalog 名称/描述/共同字段更新;服务端仅 category |
|
|
||||||
| 7 | media_cards 图文视频封面卡片 | widget-more.php:2005-2191 | 模块级动画、pc_row/m_row(1..12,默认 4/2)、mask_opacity、height_scale 默认 30、font_size_pc/m、font_bold、font_color、covers group{image,video,hide,title,link};`widget-graphic-cover` + this-font 变量 + 移动端回退图片;删旧 description/image_dark 卡片字段 |
|
|
||||||
| 8 | slider_media_combo 幻灯片加图文视频卡片 | widget-slider.php:361-887 | 模块级动画;补 5 个 subheading 分组;option 默认值逐项核对(源文件为准);m_layout_type=default 时 m_row=pc_row;clamp 上限 4/6 修正;footer 重算 JS 接入 |
|
|
||||||
| 9 | partners 横向滚动合作伙伴 | widget-more.php:2317-2581 | rows/m_rows 1..6 默认 3、style_type card/logo/chip、card_width/height(依赖)、card_bg/card_radius、speed slider 0.1..10 步 0.1、direction/pause_hover/show_widget_bg/go_link/end_fade、link_type(link 类别多选用 terms 类型 taxonomy=link_category/link_limit/link_orderby/page_links_order)、partners group{title,desc,image,link};zps-* DOM、round-robin 行分配、get_bookmarks 来源、go_link 走 aksrt_go_link、CSS 动画(已有)+JS 初始化 |
|
|
||||||
| 10 | combo_info 复合信息卡片 | widget-more.php:2583-2833 | 模块级动画、pc_row 2/3/4/6 默认 4、m_row 1/2、pc_gap/m_gap(gutters)、card_radius、top_band/top_band_height、go_link、cta_text/cta_color、cards group{title,icon,icon_image,tags 嵌套 group{text,color},desc,link,cta_text,cta_color,band_color auto/7色};zci-* DOM + band auto 循环 + --delay 交错;markup 改挂到已有 `.aksrt-combo-info-card` CSS;删 aksrt-combo-card 死代码 |
|
|
||||||
| 11 | recent_comments 最近评论 | widget-more.php:3315-3363 + zib-comments-list.php:340-407 | limit 1..50 默认 8、outer/outpost、show_source 默认 false、style radio(''简约/style_2 风格2,默认修正为 '');`box-body comment-mini-lists zib-widget` + comment-widget-item(avatar box、昵称+icon-spot 时间、点赞、con-link text-ellipsis-5、style_2 盒装、来源行);复用 inc/helpers/aksrt-comments.php 时间/头像,内容过滤图片→[图片] |
|
|
||||||
| 12 | posts_toc 文章目录树 | widget-more.php:445-532 | 改旧式字段:title 默认"文章目录"、mini_title、in_affix(删 min_headings/collapsed/offset);渲染空占位 `.posts-nav-box[data-title]`;目录构建/scrollspy/折叠全部移入 frontend.js;`.posts-nav-lists` CSS 若缺则从 main.css:4835-4946 补 |
|
|
||||||
|
|
||||||
### Phase C 死代码清理
|
|
||||||
- 删除被替换的渲染分支与 CSS:aksrt-term-cards、aksrt-term-aggregation、aksrt-combo-card、aksrt-widget-partners 旧轨、aksrt-widget-media-cards/comments/toc/pagination/load-more 旧标记、tab_posts legacy trigger、hot_posts 的 show_thumbnail、oneline 的 autoplay;`media_card_fields()` 仍被 knowledge_product 组使用故保留,仅解除 12 个目标对它的引用。
|
|
||||||
|
|
||||||
### Phase D 测试与校验
|
|
||||||
- `tests/widget-system/run.php`:query_contract 移除 topics、加 tab zibpay_type;modern_widget_contract 扩充 12 个的 schema/DOM 断言;保持既有断言全绿。
|
|
||||||
- 新增 `tests/widget-system/zibll-parity-widgets-contract.test.cjs`(node --test 源码 token 契约:每 widget 的字段 id 清单、关键 DOM 类、CSS token、frontend.js 行为、死类负向断言)。
|
|
||||||
- 更新受影响的 zibll-modern-widgets / post-visual / a11y 契约测试(保持 aria-busy、role=status、reduced-motion)。
|
|
||||||
- 重新生成 `tests/fixtures/widget-golden-v1.json`(php tools/generate-widget-golden-fixture.php)并跑 php tools/validate-widget-golden.php(不带 --require-results)。
|
|
||||||
- 验证:`php -l` 全部改动 PHP;`node --test tests/widget-system/` 全量;`composer dump-autoload -o`(优先不新增类,仅改现有文件)。
|
|
||||||
|
|
||||||
### 明确不做
|
|
||||||
不做旧设置迁移与双路渲染;不做真实视觉/Docker 验收;不参考"(旧版即将删除)"模块;不改 37 slug 顺序、REST 路由形态、其他 25 个小工具。
|
|
||||||
|
|
||||||
### 主要参考文件
|
|
||||||
zibll:`inc/widgets/widget-posts.php`、`widget-more.php`、`widget-slider.php`、`inc/functions/{zib-posts-list,zib-category,functions}.php`、`js/section-navs.js`、`js/main.js`、`css/main.css`;AKSRT:`src/WidgetSystem/*`、`inc/helpers/*`、`assets/widgets/*`、`tests/widget-system/*`。
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
诊断结论(已核实):
|
|
||||||
|
|
||||||
`video-player.js:177` 是 `toggle()` 中的 `this.video.play()`。播放器进入“无可播放源”状态后,点击封面/遮罩触发 `toggle()`,该 Promise 没有 catch,于是抛出 `Uncaught (in promise) NotSupportedError: The element has no supported sources`。播放器进入无源状态有四条已确认路径:
|
|
||||||
1. `.m3u8` 在 Chrome:hls.js 从 `cdn.jsdelivr.net` 懒加载(video-player.js:22),CDN 不可达时 `loadScript` 静默失败(promise 也未 catch),`<video>` 始终没有源;
|
|
||||||
2. `.flv` 同理(flv.js,video-player.js:23);
|
|
||||||
3. 直链 mp4 地址 404/编码不支持,源加载失败后 `currentSrc` 为空;
|
|
||||||
4. `.switch-iframe` 剧集按钮被 video-player 的委托处理器(video-player.js:225)捕获,把 iframe 嵌入页 URL 当 mp4 塞给 `video.src`,必然无源。`.switch-iframe` 本应由 `runtime/main.js:1351` 的 iframe 链路处理。
|
|
||||||
|
|
||||||
修复方案(单一路径,不引入双路/迁移):
|
|
||||||
|
|
||||||
1. `assets/js/video-player.js`:
|
|
||||||
- `toggle()`:播放前守卫无源状态(`video.currentSrc`/`video.src`/`this.hls`/`this.flvPlayer` 均为空时直接返回),并对 `play()` 返回的 Promise 加 `.catch()`,播放失败不再产生未捕获异常;
|
|
||||||
- 两处 `loadScript()`(hls/flv)加 `.catch()`,CDN 失败被收敛为“无源”状态,不再产生未处理 rejection;
|
|
||||||
- 委托处理器限定为 `.switch-video`,移除对 `.switch-iframe` 的错误接管(iframe 剧集继续由 main.js 既有链路处理,属于删除错误重叠而非新增实现)。
|
|
||||||
2. 契约测试 `tests/widget-system/single-cover-height-contract.test.cjs` 的 video cover 用例追加断言:toggle 守卫 + play catch、loadScript catch、处理器不含 `.switch-iframe`。
|
|
||||||
3. 验证:`node --check` 播放器文件、运行 `single-cover-height-contract` 及相关契约测试、`rg` 复查、`git diff --check`;不做真实视觉效果验收。
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
## 目标与边界
|
|
||||||
|
|
||||||
将 AKSRT 前台 DOM 架构一次性硬切换为 `C:\Users\Xing\Desktop\zibll` 的真实架构:
|
|
||||||
|
|
||||||
```text
|
|
||||||
WordPress 配置/_win
|
|
||||||
→ jQuery → Bootstrap → runtime/loader.js
|
|
||||||
→ tbquire(['main'])
|
|
||||||
→ main.js 顶层全局委托 + window.auto_fun()
|
|
||||||
→ 局部 AJAX:post_ajax.ed
|
|
||||||
→ JSON AJAX:zib_ajax.success
|
|
||||||
→ 远程 modal:loaded.bs.modal → auto_fun()
|
|
||||||
```
|
|
||||||
|
|
||||||
不保留 `AksrtFrontend`、不写兼容适配器、不做双路初始化、不保留“迁移中”分支。保留 AKSRT 的业务能力和安全处理,但全部挂到参考主题的唯一 `main/auto_fun` owner 上。按用户要求只做静态、契约、VM、语法和构建验证,不做真实视觉验收。
|
|
||||||
|
|
||||||
## 一、运行时入口硬切换
|
|
||||||
|
|
||||||
1. **重写 `assets/js/runtime/loader.js`**
|
|
||||||
- 以参考主题 loader 为行为基准,恢复 RequireJS 改名运行时、`tbquire.config()` 和最后的 `tbquire(['main'])`。
|
|
||||||
- 根据 AKSRT 实际目录调整 `baseUrl` 与 URL 版本参数,但保持参考主题的加载语义和 module mapping 结构。
|
|
||||||
- 将当前仍有业务消费者的模块纳入 mapping:main、imgbox、page-navs、section-navs、comment、message、input-expand、mini-upload、page-template、page-edit、author、poster-share、captcha、pay、swiper、dplayer、lazySizes、clipboard、svg-icon、enlighter、translate 等;每个 mapping 只指向一个真实文件,不复制第二份实现。
|
|
||||||
- 删除当前仅为 `AksrtFrontend`/`aksrt.tbquire` 服务的映射和 fallback。
|
|
||||||
|
|
||||||
2. **重写 `assets/js/runtime/main.js`**
|
|
||||||
- 以参考主题 `zibll/js/main.js` 的全局顺序为骨架:顶层建立 `_win.bd`/`_win.window`,注册 body/document 委托,定义全局 API 与插件,定义 `z_auto_fun`,暴露 `window.auto_fun = debounce(z_auto_fun, 100)`,顶层首次调用 `auto_fun()`,最后执行单一 `$(document).ready(...)` 首屏逻辑。
|
|
||||||
- 将 AKSRT 当前 `aksrt-runtime.js` 中仍需要的 declarative action、确认、通知、上传/安全/站点功能直接并入该唯一运行时或对应 `tbquire` 模块,不再通过第二个 runtime 注册表调用。
|
|
||||||
- 将所有按 DOM 条件加载逻辑集中到 `z_auto_fun()`:支付、消息、输入扩展、上传、海报分享、验证码、复制、tooltip、popover、affix、Swiper、DPlayer、SVG、代码高亮、灯箱等,严格按参考主题调用 `tbquire()`。
|
|
||||||
- 删除 `AksrtFrontend.register('aksrt-legacy-main', ...)`、`window.auto_fun` 到 `AksrtFrontend.rehydrate()` 的 wrapper、`z_auto_fun()` 中对新 registry 的依赖。
|
|
||||||
- 将原先局部更新后的 `AksrtFrontend.rehydrate(root, 'ajax'/'modal')` 全部改为参考主题调用顺序:注入完成后触发 `post_ajax.ed`(列表场景)或 `loaded.bs.modal`(远程 modal),随后调用防抖 `auto_fun()`。
|
|
||||||
- 保留必要的幂等 class/data 标记(如 `is-on`、`swiper-container-initialized`、`dplayer-initialized`),但删除无法再被调用的 registry 保护代码。
|
|
||||||
|
|
||||||
3. **删除新生命周期核心**
|
|
||||||
- 删除 `assets/js/rehydrate.js`。
|
|
||||||
- 将 `assets/js/aksrt-runtime.js` 中仍需的实际业务行为迁移到 `runtime/main.js` 或对应按需模块后,删除该文件及其作为公共入口的职责;不得留下空壳 `aksrt-runtime` 适配层。
|
|
||||||
- 清理所有 `AksrtFrontend.register/boot/rehydrate`、`aksrt.autoFun`、`aksrt:ready`、`aksrt:rehydrated` 引用和注释。
|
|
||||||
|
|
||||||
## 二、公共构建与 PHP 入队重构
|
|
||||||
|
|
||||||
1. **调整 `tools/build-public-assets.mjs`**
|
|
||||||
- 不再把整个 canonical 模块集合拼成一个 `public.js` 再依赖 `AksrtFrontend`。
|
|
||||||
- 以参考主题的独立 loader + 独立动态模块为构建图;对仍需生成的压缩文件只从唯一源码生成,避免 source/min 双 owner。
|
|
||||||
- 保留 CSS bundle 构建;JS 构建只生成 loader/main 及实际 mapping 所需的 min 产物。
|
|
||||||
- 生成后执行静态顺序检查,确保 loader 先于 main 动态加载,且没有 `tbquire(['main'])` 之外的第二个 boot。
|
|
||||||
|
|
||||||
2. **调整 `src/Core/Scripts.php`、`ScriptsPart1b.php`、`ScriptsPart1c.php` 及 WidgetSystem AssetManager**
|
|
||||||
- 前台基础依赖改为 jQuery → Bootstrap → `runtime/loader.js`,不再将 `build/public.js` 作为全局生命周期入口。
|
|
||||||
- 删除所有 routed script 对 `aksrt-runtime` 的依赖;路由模块改由 loader/main 按 DOM 条件加载,直接遵循参考主题的条件加载方式。
|
|
||||||
- 将 `_win` 作为唯一前台运行时配置对象;把必须保留的 AKSRT 数据字段合并进去,避免同时维护 `_win`、`aksrtData` 两套 DOM 生命周期配置。
|
|
||||||
- WidgetSystem 只保留一个前端 owner:将 widget 的初始化接入 `auto_fun`/对应动态模块,禁止 `assets/widgets/frontend.js` 与旧 widget-set/main 重复初始化同一节点;同步调整第三方资源注册和依赖。
|
|
||||||
- 清理已迁移到 loader/main 的独立 enqueue、重复 inline boot、重复条件判断和无实际动作的 Swiper 注册。
|
|
||||||
|
|
||||||
## 三、局部更新协议统一为参考主题行为
|
|
||||||
|
|
||||||
1. **列表 AJAX:`assets/js/core/ajax-list.js`、`homepage-fixed.js`、`pagination.js`、`comments.js`、`routes/user-center.js`、`author.js`、`widgets/frontend.js` 等**
|
|
||||||
- 保留现有请求、安全、错误提示和业务响应格式。
|
|
||||||
- 删除直接 `AksrtFrontend.rehydrate()` 的调用。
|
|
||||||
- 按场景统一为:before 状态 → HTML parse/replace/append → 触发容器上的 `post_ajax.ed` → `auto_fun()`;保留参考主题的 `ing`/loading/分页/route 顺序。
|
|
||||||
- 对复杂 panel 继续使用现有 destroy 能力(例如订单中心),但 destroy 只是业务实例清理,不再扩展成第二套全局 lifecycle。
|
|
||||||
- 明确旧 `.ajax-next` 与新 `data-aksrt-*` 的唯一 ownership;最终删除重复 click handler,控制只由一个 `post_ajax` 实现处理。
|
|
||||||
|
|
||||||
2. **JSON AJAX:`comment-ajax.js`、`auth.js`、`follow.js`、`credits.js`、`reward.js`、`messages.js` 等**
|
|
||||||
- 统一使用参考主题的 `zib_ajax()` 语义:按钮状态、错误/成功提示、`zib_ajax.success` 事件、业务 callback、可选 modal/reload。
|
|
||||||
- 删除以 `aksrt:rehydrated` 或 registry 事件作为成功信号的路径;只有真正 HTML 注入后才调用 `auto_fun()`。
|
|
||||||
|
|
||||||
3. **远程 modal 与历史路由**
|
|
||||||
- modal HTML 更新改为 Bootstrap `loaded.bs.modal`,由 main 的全局监听调用 `auto_fun()`。
|
|
||||||
- 保留 `history.pushState/replaceState` 与 Tab `popstate` 行为,但删除 `AksrtFrontend` 路由 API 和独立页面 boot;不引入 PJAX、Turbolinks、InstantClick 或全局 MutationObserver 重初始化。
|
|
||||||
|
|
||||||
## 四、灯箱、懒加载和 DOM 标记恢复为参考主题契约
|
|
||||||
|
|
||||||
1. **灯箱硬切换**
|
|
||||||
- 删除 `AksrtLightbox`、`data-aksrt-lightbox*`、`aksrt-lightbox` overlay 和其专用 CSS/配置桥接。
|
|
||||||
- 从参考主题恢复单一 `imgbox` owner:新增/恢复 `assets/js/runtime/imgbox.js`(压缩产物由构建生成),使用 `Miniimgbox`、`imgbox-container`、`a[data-imgbox]`、`imgbox_data` 和参考主题的幂等标记。
|
|
||||||
- `main.js` 只在 `_win.imgbox` 且存在目标 DOM 时 `tbquire(['imgbox'])`;不得再有第二个灯箱监听器。
|
|
||||||
- 更新 `single.php`、`page.php`、gallery/content renderer、图片 helper、相关 CSS 和设置输出,使 DOM 属性、分组、缩略图、zoom/full/play/down 开关与参考主题一致。
|
|
||||||
|
|
||||||
2. **懒加载单一 owner**
|
|
||||||
- 删除 `lazy-image-attr.js` bridge 及其 enqueue/build/test 依赖;lazySizes、`lazybeforeunveil`、`data-src`/`data-srcset`/`data-bg`、兼容属性处理全部由 main/lazySizes owner 负责。
|
|
||||||
- 保留必要的 PHP 图片 URL 生成和占位图能力,但统一输出参考主题可识别的 DOM,不再并行维护第二套 lazy switch 或 IntersectionObserver 初始化。
|
|
||||||
- 保留 lazySizes 自有 MutationObserver;禁止把它扩展成全局组件 rehydrate 机制。
|
|
||||||
|
|
||||||
## 五、所有业务模块去 registry 化
|
|
||||||
|
|
||||||
逐一处理当前已注册的 public/routed 文件(article、comments、comment-fixed-input、pagination、archive、footer-tabbar、misc、follow、enhancements、enhancements2、color-extract、effects、captcha、auth、author、page-navs、section-navs、messages、front-submit、mini-upload、page-template、page-edit、post-share、poster-share、pay-vip、video-player、download-status、ban-appeal、docs-navigation、input-expand、waterfall、yiyan、site-notice、user-badge-actions、widgets 等):
|
|
||||||
|
|
||||||
- 删除 `AksrtFrontend.register()` 包装和对 registry API 的防御性分支。
|
|
||||||
- 将脚本改成参考主题式:被 `tbquire` 加载后立即定义/绑定,或暴露单一模块初始化函数供 main 调用;依赖 DOM 委托和 `is-on`/class/data 标记实现重复调用安全。
|
|
||||||
- 任何需要窗口/文档监听的模块只绑定一次,并使用参考主题的全局 owner;不新增 destroy registry、root 广播或双入口 ready。
|
|
||||||
- 保留 AKSRT 的 nonce、REST 权限、输出转义和业务接口,不改变安全边界。
|
|
||||||
|
|
||||||
## 六、死代码清理
|
|
||||||
|
|
||||||
在改造前后用 `rg/git grep` 建立引用图,仅删除已确认无入口的代码,避免把仍被动态 loader 使用的资源误删:
|
|
||||||
|
|
||||||
- 删除 `assets/js/revenue.js`(确认无 PHP、JS、构建和模板引用后)。
|
|
||||||
- 删除当前 `rehydrate.js`、`aksrt-runtime.js` 生命周期专用代码及其测试夹具。
|
|
||||||
- 删除 `runtime/main.js` 中已迁移功能的 registry 适配、注释掉的 waterfall 分支、未使用 `auto_fun` 转发代码和重复事件派发。
|
|
||||||
- 删除确认无 mapping、enqueue、模板或第三方引用的 `runtime/widget-set.*`、`runtime/mini-touch.*`、`waterfall.*` 等 orphan;若仍被参考主题式 loader 使用,则保留唯一映射,不凭文件名删除。
|
|
||||||
- 删除 `AksrtLightbox`、旧 overlay、runtime/imgbox 的冲突副本和无消费者的 `aksrt.*` API;`_win`、`tbquire`、main、loader、Swiper/DPlayer/lazySizes 只在确认所有消费者迁移后保留。
|
|
||||||
- 同步清理文档、注释、PHP handles、构建清单、测试中的旧 token,确保仓库不存在“旧入口还能被重新接回”的死配置。
|
|
||||||
|
|
||||||
## 七、测试与非视觉验证改造
|
|
||||||
|
|
||||||
1. 删除/重写锁定旧架构的测试:
|
|
||||||
- `global-dom-init-contract.test.cjs` → `legacy-loader-runtime-contract.test.cjs`:验证 loader config、mapping、`tbquire(['main'])`、唯一 boot owner。
|
|
||||||
- `rehydrate-runtime-contract.test.cjs` → `legacy-auto-fun-contract.test.cjs`:VM 验证 `auto_fun` 防抖、重复调用、`auto_fun` 事件、异常隔离和 DOM 标记幂等。
|
|
||||||
- `public-bundle-parity-contract.test.cjs`:改为验证 loader/main 构建图,不再断言 `AksrtFrontend`。
|
|
||||||
- `homepage-dom-contract.test.cjs`:改为验证 HTML 注入后 `post_ajax.ed → auto_fun` 顺序和唯一 delegated owner。
|
|
||||||
- `lightbox-contract.test.cjs`:改为 `Miniimgbox`/`data-imgbox`/`imgbox` mapping 契约。
|
|
||||||
- `lazyload-contract.test.cjs`:改为 main/lazySizes 单 owner 契约。
|
|
||||||
- `legacy-navigation-contract.test.cjs`:验证 loader 的 page-navs/section-navs mapping 和 `auto_fun` 条件加载。
|
|
||||||
|
|
||||||
2. 新增/保留 VM 与静态契约:
|
|
||||||
- loader URL 解析和模块缓存只发生一次。
|
|
||||||
- 同一 DOM 多次 `auto_fun()` 不重复绑定 tooltip、Swiper、DPlayer、imgbox、上传和 widget。
|
|
||||||
- `post_ajax.ed`、`zib_ajax.success`、`loaded.bs.modal` 三种事件不混用。
|
|
||||||
- 全仓库禁止 `AksrtFrontend`、`aksrt.autoFun`、`aksrt:ready`、`aksrt:rehydrated`、第二套 `DOMContentLoaded` 页面 boot。
|
|
||||||
- 静态检查没有 PJAX/InstantClick/全局 MutationObserver reinit 新入口。
|
|
||||||
|
|
||||||
3. 继续运行与改造后的验证:
|
|
||||||
- Node widget/navigation 契约测试。
|
|
||||||
- `tools/test-credits-rehydrate.mjs`(改名或改断言为 auto_fun 重入 harness)。
|
|
||||||
- WidgetSystem PHP audit、asset ledger、业务安全/支付/评论相关测试。
|
|
||||||
- `node --check` 检查所有改动后的 JS 和构建脚本。
|
|
||||||
- `php -l` 检查所有改动后的 PHP。
|
|
||||||
- `node tools/build-public-assets.mjs` 后再次运行 bundle/source parity、旧入口扫描和 `git diff --check`。
|
|
||||||
|
|
||||||
## 八、实施顺序与完成标准
|
|
||||||
|
|
||||||
1. 先完成只读引用图和测试基线,记录当前失败/通过集合。
|
|
||||||
2. 先改 loader/main/构建/PHP 入队,建立唯一运行时入口。
|
|
||||||
3. 再批量去掉所有 registry 调用并统一 AJAX/modal 事件顺序。
|
|
||||||
4. 再恢复参考主题灯箱和 lazySizes owner,更新模板/CSS/配置。
|
|
||||||
5. 最后删除确认死代码与旧测试,重建生成产物。
|
|
||||||
6. 完成标准:
|
|
||||||
- 前台只有 `loader → main → auto_fun` 一条初始 DOM 路径。
|
|
||||||
- 没有任何 `AksrtFrontend` 或其双路兼容代码。
|
|
||||||
- 列表、JSON、modal 三类更新分别遵循参考主题事件协议。
|
|
||||||
- Swiper、DPlayer、lazySizes、imgbox、WidgetSystem 各只有一个 owner。
|
|
||||||
- 所有已确认死代码和旧契约被删除或改写。
|
|
||||||
- 非视觉测试、语法检查、构建和静态引用审计通过;不执行真实视觉验收。
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# 第三方人机验证优化改造计划
|
|
||||||
|
|
||||||
约束:不写双路/兼容迁移代码;顺手清理死代码;不做真实视觉效果验收(用语法检查、契约测试、构建产物校验代替)。
|
|
||||||
已确认决策:范围全做(P0+P1+P2);下载场景 pass 放行 10 分钟;凭据不齐时仅加后台警告、前台保持放行。
|
|
||||||
|
|
||||||
## 改动文件总览
|
|
||||||
|
|
||||||
| 文件 | 动作 |
|
|
||||||
|---|---|
|
|
||||||
| `inc/modules/captcha.php` | 重写校验层 + 瘦身渲染 + 后台警告 + 静态缓存 |
|
|
||||||
| `assets/js/captcha-front.js` | 新建(从内联脚本迁移,~340 行) |
|
|
||||||
| `assets/css/shared/captcha.css` | 保留不动(改为进 bundle) |
|
|
||||||
| `tools/build-public-assets.mjs` | publicScripts 加 captcha-front.js;public.css 加 captcha.css |
|
|
||||||
| `src/Admin/Schema/SchemaPermissions.php` | 删除 'mode' 死字段(L32-34) |
|
|
||||||
| `assets/admin/src/schema/sections/permissions/index.js` | 删除 'mode' 死字段(L18 defaults、L50-56) |
|
|
||||||
| `src/Admin/SchemaProvider.php` | L403 白名单删 'mode';保存后 flush 配置静态缓存 |
|
|
||||||
| `tests/widget-system/captcha-cloud-contract.test.cjs` | 新建契约测试 |
|
|
||||||
|
|
||||||
不动的文件及原因:`aksrt-header-userbox.php`(toast-contract 测试锁定 signin 接线)、`data-aksrt-download-captcha` 挂载点(`templates/parts/resource-download-box.php:467`、`inc/helpers/aksrt-pay.php:1586`)、所有业务 JS(auth/signin/comments/cms-download/user-center/front-submit 只消费 `AKSRTCaptchaState.scenes[scene]` 与 mount 内 token input,接口不变即零改动)。
|
|
||||||
|
|
||||||
## 1. inc/modules/captcha.php — 校验层
|
|
||||||
|
|
||||||
**P0-1 腾讯云改走 WP HTTP API**:`aksrt_verify_tencent()` 删除 curl 块(L374-390,含 `CURLOPT_SSL_VERIFYPEER => false`),改用 `wp_remote_post('https://captcha.tencentcloudapi.com/', ['timeout'=>5, 'headers'=>aksrt_tencent_tc3_sign(...)生成的一维数组, 'body'=>$payload])`,与另外三家一致。签名函数不动。
|
|
||||||
|
|
||||||
**P1-5 错误信息收敛**:四家服务商的传输层错误(wp_remote_post 返回 WP_Error、响应不可解析)统一 `error_log('[AKSRT Captcha] {provider}: ' . 详情)` 后返回通用文案 `__('人机验证服务暂不可用,请稍后重试', 'aksrt')`;保留对用户有行动意义的映射文案(腾讯 CaptchaCode 表、阿里 F0xx 表)。腾讯 API Error 分支(签名错误等)也走 log+通用文案。
|
|
||||||
|
|
||||||
**P1-4**:阿里云 `SignatureNonce` 由 `uniqid('', true)` 改 `wp_generate_uuid4()`。
|
|
||||||
|
|
||||||
**P1-8**:四家 `'timeout'` 统一 5 秒(现 10 秒)。
|
|
||||||
|
|
||||||
**P1-6 Turnstile 绑定场景**:`aksrt_verify_captcha($post_data, $scene = '')` 增加第二参数(唯一调用方是 `aksrt_require_captcha`,无散落调用点);Cloudflare 成功响应校验:`action !== 'aksrt-verify'` 或 `cdata !== $scene`(存在才比对,lenient)→ 失败。客户端配套见 §2。
|
|
||||||
|
|
||||||
**P0-2 下载 pass 放行(10 分钟)**:`aksrt_require_captcha()` 内,scene 命中放行名单时先查 transient、验证通过后写 transient:
|
|
||||||
- `aksrt_captcha_pass_ttl($scene)`:仅 `download` 返回 `10 * MINUTE_IN_SECONDS`,其余返回 0(单一代码路径,无散落分支)
|
|
||||||
- key = `'aksrt_captcha_pass_' . md5($scene . '|' . get_current_user_id() . '|' . aksrt_get_client_ip() . '|' . md5(UA))`,仿 `aksrt_auth_rate_` 既有模式
|
|
||||||
|
|
||||||
## 2. assets/js/captcha-front.js — 新建(P2-9 + P1-7 客户端)
|
|
||||||
|
|
||||||
把 `aksrt_render_captcha()` 内联 `<script>`(L632-969)整体迁移为静态文件,逻辑不改名不重写:`loadScriptOnce`、四家 `load*`、`AKSRTCaptchaState.scenes[scene].verify/reset` 注册、`setField/resetVerificationState/syncDownloadSiblings` 等原样搬移。仅两处行为修正:
|
|
||||||
- 极验 `onClose` 空回调改为 `cancelPending('已取消验证')`(P1-7,与腾讯/CF 取消体验对齐)
|
|
||||||
- Turnstile render 配置加 `cdata: C.scene`(P1-6 客户端配套)
|
|
||||||
|
|
||||||
激活模型:扫描 `document.querySelectorAll('[data-aksrt-captcha-config]')`,`JSON.parse(el.getAttribute(...))` 逐个 init;DOMContentLoaded 后再补扫一次并暴露 `window.AKSRTCaptchaState.rescan()` 供未来 ajax 注入使用。保持 vanilla JS、无新依赖。
|
|
||||||
|
|
||||||
对应 PHP 渲染端瘦身:`aksrt_render_captcha()` 只输出
|
|
||||||
1. 原 wrapper 结构不变(`.aksrt-captcha-wrap` + `#uid` box + `#vid` captcha_verified 隐藏域),
|
|
||||||
2. wrapper 上挂 `data-aksrt-captcha-config`(`esc_attr(wp_json_encode($js_config))`),
|
|
||||||
3. 删除内联 `<script>` 与逐次 `<link rel="stylesheet">`。
|
|
||||||
|
|
||||||
TOKEN_FIELDS 常量、i18n 字符串原样进 JS 文件(与仓库其它 bundle 内硬编码中文一致)。
|
|
||||||
|
|
||||||
## 3. 构建与后台(P2-10 + bundle 接线)
|
|
||||||
|
|
||||||
- `tools/build-public-assets.mjs`:`publicScripts` 追加 `'assets/js/captcha-front.js'`;`cssBundles['public.css']` 追加 `'assets/css/shared/captcha.css'`。走既有 bundle 版本机制,无需新增 enqueue 代码。
|
|
||||||
- `SchemaPermissions.php`:删除 'mode' 字段定义(L32-34,单选项 always 的死字段)。
|
|
||||||
- `assets/admin/src/schema/sections/permissions/index.js`:删除 `mode` 的 defaults(L18)与字段定义(L50-56)。
|
|
||||||
- `SchemaProvider.php` L403:归一化白名单去掉 `'mode'`;`write_section_settings()` 更新 `aksrt_captcha_config` 后调用新助手 `aksrt_captcha_config_reset()` 清静态缓存(function_exists 守卫,与现有 captcha 专用块并排)。
|
|
||||||
- DB 里残留的 `mode` 键不做迁移清洗(读路径白名单已忽略它,符合"不做迁移"约束)。
|
|
||||||
|
|
||||||
## 4. inc/modules/captcha.php — 后台警告与缓存
|
|
||||||
|
|
||||||
- **P0-3**:新增顶层 `add_action('admin_notices', ...)`:`manage_options` 权限 + `enabled` 为真 + `active_provider` 非空 + `aksrt_captcha_provider_is_configured()` 为假时,输出 `notice-error` 横幅,文案指向「功能&权限 → 人机验证」补全配置(复用现有 misconfig 提示文案)。前台行为不变(保持放行)。
|
|
||||||
- **P2-11**:`aksrt_get_captcha_config()` 加 `static $cache`;新增 `aksrt_captcha_config_reset()`;写入路径(SchemaProvider)调用 reset。`aksrt_get_captcha_status_summary()` 的 12 场景循环随之受益。
|
|
||||||
|
|
||||||
## 5. 新契约测试 tests/widget-system/captcha-cloud-contract.test.cjs
|
|
||||||
|
|
||||||
仿 toast-contract 的正则断言风格:
|
|
||||||
- `captcha.php`:不匹配 `curl_init|CURLOPT_SSL_VERIFYPEER|machine-verification|slidercaptcha|imagecaptcha`;匹配 `wp_remote_post`(≥4 处)、`wp_generate_uuid4`、`'timeout'\s*=>\s*5`、`aksrt_captcha_pass_`;渲染函数不匹配 `/<script|<link/`(内联抽取完成)
|
|
||||||
- `captcha-front.js` 存在且含 `AKSRTCaptchaState`、`data-aksrt-captcha-config`
|
|
||||||
- `build-public-assets.mjs` 的 publicScripts 含 captcha-front.js、public.css 含 shared/captcha.css
|
|
||||||
- `SchemaPermissions.php` 与 admin `index.js` 的 captcha 段不匹配 `mode`
|
|
||||||
- `SchemaProvider.php` 不匹配 `'mode'`
|
|
||||||
|
|
||||||
## 6. 验证(不做视觉验收)
|
|
||||||
|
|
||||||
1. `php -l` 全部改动的 PHP 文件
|
|
||||||
2. `node --check` captcha-front.js
|
|
||||||
3. `node tools/build-public-assets.mjs` 重建 bundle
|
|
||||||
4. `grep` 全仓清扫:无 `curl_init`、无 `'mode'`(captcha 相关文件)、无 legacy 标记
|
|
||||||
5. `node --test tests/widget-system/*.test.cjs` 全量跑,预期仅存量 page-display-contract 一项失败(与本次无关,已验证在 HEAD 上同样失败)
|
|
||||||
|
|
||||||
## 明确不做
|
|
||||||
|
|
||||||
- 不做视觉验收/浏览器实测(按要求)
|
|
||||||
- 不写新旧配置迁移、不清洗 DB 残留 `mode` 键
|
|
||||||
- 不动腾讯 trerror 容灾票据策略(维持拒绝)
|
|
||||||
- 不做阿里云签名 V3 升级、Turnstile appearance/retry 配置项(列为后续可选项)
|
|
||||||
- 不动 `ExtensionController` 的 extensions 状态端点(SPA 未消费属另一话题)
|
|
||||||
|
|
||||||
## 建议提交拆分(如需提交)
|
|
||||||
|
|
||||||
1. `fix(captcha): use WP HTTP API with TLS verification for tencent`
|
|
||||||
2. `feat(captcha): short-lived download pass + turnstile binding + error hardening`
|
|
||||||
3. `refactor(captcha): extract inline runtime to captcha-front.js bundle`
|
|
||||||
4. `chore(captcha): drop dead mode field, config cache, contract tests`
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# 侧栏空槽/挤压修复(全站,单一决策源)
|
|
||||||
|
|
||||||
## 根因
|
|
||||||
"是否渲染侧栏列"有两套脱节的决策:
|
|
||||||
- **body 布局类**(`site-layout-1/2/3`,决定 CSS 留白)来自 `aksrt_is_show_sidebar_filter()`,但"小工具是否为空"检查只对 `is_single()` 生效(`inc/helpers/template-tags/site-layout.php:150-152`)。
|
|
||||||
- **aside 渲染**各自为政:固定首页 `aksrt_homepage_render_sidebar()` 额外检查小工具(`inc/helpers/homepage/content.php:146-162`),空时整个不渲染;`sidebar.php`(page/search/archive 用)却渲染空列 div("legacy parity")。
|
|
||||||
|
|
||||||
于是"开关开 + 无小工具":首页 body 仍是 `site-layout-2` → `layout.css:156-162` 保留 `margin-right: 侧栏宽+15px` → 主内容挤压、右侧 326px 空槽;其他页面则是空列占位同样挤压。后台关开关的路径本身自洽(layout-1),但因上述不一致,任何"列被保留而无内容"的场景都出现挤压。
|
|
||||||
|
|
||||||
参考 Zibll(`zib-theme.php` 判断 + `sidebar.php:14-16` 早退 + `main.css` 留白只挂 `site-layout-2/3`):判断集中一处,body class 承载几何,渲染与判定同源。AKSRT 骨架已具备,只缺"小工具为空"一维的全局化。
|
|
||||||
|
|
||||||
## 修改
|
|
||||||
|
|
||||||
### 1. `inc/helpers/template-tags/site-layout.php` — 核心(覆盖所有页面)
|
|
||||||
- `aksrt_is_show_sidebar_filter()`:把 `is_single() && !aksrt_has_current_sidebar_widgets()` 提升为无条件 `if (!aksrt_has_current_sidebar_widgets()) { return false; }`(空侧栏列 = 空槽,全部场景折叠为 layout-1;single 既有行为不变,home/category/tag/search/page/日期归档一并对齐)。
|
|
||||||
- `aksrt_resolve_sidebar_id()`:把 `is_home() || is_front_page()` 分支移到 `is_page()` 之前——静态首页目前解析成 `page_sidebar_{id}` 但渲染器输出 `home_sidebar`,判定/渲染不匹配;重排后一致(该函数仅 sidebar.php 与小工具检查两处调用,重排安全)。
|
|
||||||
|
|
||||||
### 2. `inc/helpers/homepage/content.php` — 删双路
|
|
||||||
- 删除 `aksrt_homepage_has_sidebar()`(修复后与 `aksrt_is_show_sidebar()` 完全等价);`aksrt_homepage_render_sidebar()` 守卫直接用 `!aksrt_is_show_sidebar()`。
|
|
||||||
|
|
||||||
### 3. `sidebar.php` — Zibll 式早退
|
|
||||||
- 顶部加 `if (!function_exists('aksrt_is_show_sidebar') || !aksrt_is_show_sidebar()) { return; }`(覆盖 `search.php:126` 的无条件 `get_sidebar()`)。
|
|
||||||
- 删除"Keep the column even if widgets are empty (legacy parity)"注释与空列行为(此后不可能渲染空列:is_show_sidebar 为真 ⇔ 至少一个 zone 有小工具);删除 `elseif ((is_home() || is_front_page()) && ...home_sidebar...)` 死分支与 `$sidebar_id` 兜底。
|
|
||||||
|
|
||||||
### 4. `inc/helpers/aksrt-archive.php` + `category.php` / `tag.php` — 死参数清理
|
|
||||||
- `aksrt_archive_has_sidebar(string $preferred = '', bool $enabled = true)` → `(bool $enabled = true)`(`$preferred` 自注释 "compat only" 后即 unset,死参数);删除 `return !wp_is_mobile();` 死兜底(主题已明确弃用 wp_is_mobile 布局门控,见 site-layout.php:141-143)。
|
|
||||||
- 更新调用点:`category.php:49`、`tag.php:30` 改为 `aksrt_archive_has_sidebar(!empty($archive_settings['show_sidebar']))`。
|
|
||||||
|
|
||||||
### 5. 死类/死代码清理
|
|
||||||
- `single.php:45`:删除无任何 CSS/JS 引用的 `container--no-sidebar` 输出。
|
|
||||||
- `front-page.php:14-29`:删除不可达的 `function_exists('aksrt_homepage_render_layout')` 兜底双路(`inc/bootstrap.php:33` 无条件加载),模板直接 `get_header(); aksrt_homepage_render_layout(); get_footer();`。
|
|
||||||
- `index.php:23-25`:注释"widget activity never changes the body layout after render"与新契约矛盾,更新为单一决策源描述。
|
|
||||||
- 保留:`layout.css:149-153` 的 `content-layout--no-sidebar` compat 规则(go.php/archive-resources.php/page-ban-appeal.php/page-user-home.php 仍在用)。
|
|
||||||
|
|
||||||
### 6. 测试
|
|
||||||
- `tests/widget-system/post-visual-contract.test.cjs:359-367`:更新测试名与 :364 正则为全局检查 `if \(!aksrt_has_current_sidebar_widgets\(\)\) \{\s*return false;`;新增断言钉住:homepage 渲染守卫用 `aksrt_is_show_sidebar()` 且 `aksrt_homepage_has_sidebar` 已不存在、sidebar.php 早退、resolver 中 home 分支先于 page 分支、archive helper 无 `$preferred` 参数。
|
|
||||||
- 运行 `node --test tests/widget-system/`(至少 post-visual-contract)与 `php -l` 全部改动 PHP 文件。
|
|
||||||
|
|
||||||
## 修复后行为矩阵(每场景验证推演)
|
|
||||||
| 状态 | body 类 | aside |
|
|
||||||
|---|---|---|
|
|
||||||
| 开关开 + 有小工具 | layout-2/3 | 渲染,留白匹配 |
|
|
||||||
| 开关开 + 无小工具 | **layout-1** | 不渲染,主内容占满(首页/分类/标签/页面/搜索/归档统一) |
|
|
||||||
| 开关关 | layout-1 | 不渲染 |
|
|
||||||
|
|
||||||
## 不改动
|
|
||||||
SPA schema(`sidebar_home_enabled` 等字段已齐全)、`get_sidebar_config()` 缺省语义、主题的响应式 CSS 隐藏策略(弃用 wp_is_mobile 的既有决定)、single 页既有判定(已被测试钉住,保持原样)。
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
## 现状结论(影响方案的既有事实)
|
|
||||||
|
|
||||||
- 下载系统集中在 `src/Content/Download.php`:路由前缀硬编码为常量 `ENDPOINT='aksrt-dl'`(同时用于 rewrite 正则 :68 和 URL 拼接 :226)、TTL 硬编码 `TOKEN_TTL=600`(:12)、`max_uses` 固定写死 1(:422)、`expires_in` 返回固定值(:242)。
|
|
||||||
- 扣次在"点击兑换"时发生(`Download.php:286-292`),无 HEAD 特判、无断开检测,需按需求重构。
|
|
||||||
- 过期/耗尽已是 `fail_410()`(HTTP 410 + wp_die 提示"回到资源页重新获取"),**不自动生成新链接** —— 该行为现状已满足,保持不变。
|
|
||||||
- `aksest_dl_token` 表(`src/Core/DBMigratorPart2.php:133-150`)的 token 列已是 `PRIMARY KEY`(即唯一索引),重复密钥不可能入库;需补的是 issue_token 插入失败时的换号重试。
|
|
||||||
- 设置体系是双端 schema:PHP 权威在 `src/Admin/Schema/*.php`,Vue 镜像在 `assets/admin/src/schema/sections/**`,菜单在 `assets/admin/src/constants/sections/permissions.js`,REST 用泛型路由自动生效。"功能&权限"= SPA 内 permissions 域。
|
|
||||||
- `Auth\Router::maybe_flush_rewrite_rules()`(:78-84) 已有"版本标记式 flush_rewrite_rules()"先例,可直接套用。
|
|
||||||
|
|
||||||
## 一、新增设置分区(permissions 域 → "下载配置")
|
|
||||||
|
|
||||||
存储为独立 option `aksrt_download_config`(沿用 captcha/moderation 的 standalone 模式),三个字段:
|
|
||||||
|
|
||||||
| key | 类型 | 默认 | 校验 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `url_alias` | input | `download` | 必填;trim 后非空;仅允许 `[A-Za-z0-9_-]`(禁中文/空格// \ ? # & 等);≤48 字符;拒绝保留字黑名单(wp-json、wp-admin、wp-content、wp-includes、admin、login、logout、register、lostpassword、feed、search、author、page、comments、sitemap、sign 等,防路由冲突/404) |
|
|
||||||
| `default_max_uses` | number | `1` | min 1 / max 100 |
|
|
||||||
| `ttl_minutes` | number | `10` | min 1 / max 1440 |
|
|
||||||
|
|
||||||
修改文件:
|
|
||||||
1. **`src/Admin/Schema/SchemaPermissions.php`**:新增 `download` section(version 1.0.0,option_key `aksrt_download_config`,单组 basic 三字段,含中文标签/help 说明"完整下载才计次、到期或耗尽后 410 需重新获取")。
|
|
||||||
2. **`src/Admin/SchemaProvider.php` `validate()`**:仿照 payment/withdrawal 特例先例(:269-292),对 `permissions/download` 增加中文错误消息校验块(空值/非法字符/过长/保留字)——422 错误经前端 `message.error('保存失败: …')` 弹窗呈现,满足"后台弹窗提示"。
|
|
||||||
3. **`src/Settings/Settings.php`**:`STANDALONE_OPTION_KEYS` 追加 `aksrt_download_config`(导入导出完整性 + 缓存失效覆盖)。
|
|
||||||
4. **`assets/admin/src/schema/sections/permissions/index.js`**:镜像新增 `download` 分区(defaults 与 PHP 完全一致)。
|
|
||||||
5. **`assets/admin/src/constants/sections/permissions.js`**:追加子页 `schemaPage({ key:'permissions-download', route:'/permissions/download', label:'下载配置', section:'download' })`,置于附件配置之后。泛型 REST 路由与 SchemaPage 无需改动。
|
|
||||||
|
|
||||||
## 二、Download.php 改造(`src/Content/Download.php`)
|
|
||||||
|
|
||||||
1. **动态前缀**:删掉 `const ENDPOINT`;新增私有 `endpoint()` 读配置(无效/被污染时回落 `download`);`register_rewrite()` 用 `preg_quote($endpoint)` 拼正则(:66-72);URL 拼接(:226)同源。
|
|
||||||
2. **别名变更刷新路由**:仿 `Auth\Router` 版本标记法——`init` 尾部挂 `maybe_flush_rewrite()`(priority 99):比较 `AKSRT_VERSION + 当前别名` 与 option `aksrt_dl_rewrite_version`,不一致则 `flush_rewrite_rules(false)` 并回写。导入/重置/直接改库都覆盖,无双路兼容逻辑。
|
|
||||||
3. **动态 TTL / 次数**:删掉 `TOKEN_TTL` 常量引用;`issue_token()` 写入 `max_uses = 配置.default_max_uses`、`expires_at = now + 配置.ttl_minutes*60`;`rest_request` 的 `expires_in` 改为动态秒数。配置读取做 clamp 兜底(别名非法回落 download、次数 1..100、分钟 1..1440)。
|
|
||||||
4. **完整下载才扣次**:
|
|
||||||
- 将扣次(UPDATE used_count)与文章计数 `_aksrt_res_download_count`(:286-300) 移到交付成功之后的新方法 `consume_token()`;UPDATE 加原子条件 `WHERE token=%s AND used_count < max_uses`,并发下计数封顶不超发。
|
|
||||||
- `stream_local_file()` 改为返回是否完整送达:输出循环中 `connection_aborted()` 为真立即中断并返回 false;收尾再核验一次。仅当完整送达且非 HEAD 才 `consume_token()`——网络波动中断不消耗次数,用户可重新点同一链接续下。
|
|
||||||
- **HEAD 探测**:`REQUEST_METHOD === 'HEAD'` 时照常走校验/输出 headers 但不扣次。
|
|
||||||
- **外链 302 源**(远端主机无法感知"是否下完"):把 302 交接视为一次交付,重定向发出后扣次(HEAD 时同样跳过)。此为唯一可实现语义,计划中明示。
|
|
||||||
- 过期/耗尽仍走现有 `fail_410()`(改动前后行为一致)。
|
|
||||||
5. **密钥冲突重试**:`issue_token()` 插入失败时用全新随机串重试至多 3 次(PRIMARY KEY 已保证唯一,此为极低概率兜底;表缺失仍在入口提前返回 WP_Error 不进循环)。
|
|
||||||
|
|
||||||
## 三、清理与文档
|
|
||||||
|
|
||||||
- 删除不再使用的 `Download::ENDPOINT` / `Download::TOKEN_TTL` 常量及全部残留引用(实现时全库 grep 复核;已知其余命中仅为 CSS 类名巧合,不动)。
|
|
||||||
- 更新 `CLAUDE.md:132` 文档行:rewrite 前缀改为"可配置别名(默认 download)",并补充"完整送达才计次"契约。
|
|
||||||
- 不写任何迁移/双路:现有表结构不动,存量过期 token 由每日 cron 自然清理。
|
|
||||||
- **关于唯一索引**:PRIMARY KEY(token) 本身即唯一索引且已存在(不加冗余 UNIQUE KEY,也不做表结构变更——与"不写迁移"一致),以插入重试落实需求的防冲突意图。
|
|
||||||
|
|
||||||
## 四、验证
|
|
||||||
|
|
||||||
1. 新增 Node 契约测试 `tests/widget-system/download-config-contract.test.cjs`:断言双端 schema 的字段/默认值/option_key 一致、菜单子项注册、Download.php 关键契约(动态 endpoint、流式完成后才条件 UPDATE 计次、HEAD 跳过、410 不自动续签、无 ENDPOINT/TOKEN_TTL 常量残留)。
|
|
||||||
2. `node --test tests/widget-system/download-config-contract.test.cjs` + 跑现有相关契约测试确认无回归。
|
|
||||||
3. `php -l` 全部改动的 PHP 文件;`php tools/audit-admin-contracts.php` 通过双端合同审计。
|
|
||||||
4. `cd assets/admin && npm ci && npm run build`,确认 build 产物完整(PHP 端有 chunk 缺失探测)。
|
|
||||||
5. 视觉不做真实验收(按约定跳过 Docker 对比环境)。
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
# 评论区 Zibll 1:1 全量复刻计划
|
|
||||||
|
|
||||||
目标:将 AKSRT 评论区渲染层(DOM、类名、布局、CSS、交互行为)替换为与 Zibll 完全一致,不保留旧类名、不写兼容/迁移代码。现有 REST 后端(`src/Core/CommentActions.php`)保留,仅前端选择器随之更新。
|
|
||||||
|
|
||||||
## 参考源(Zibll 侧,只读对照)
|
|
||||||
- `zibll/template/comments.php`(整体结构 L21–210)
|
|
||||||
- `zibll/inc/functions/zib-comments-list.php`(`zib_comments_list` 回调 L14、分页 L569–678)
|
|
||||||
- `zibll/inc/functions/functions.php` L1775 `zib_get_input_expand_but()`(表情/代码/图片/链接/快捷回复工具栏)
|
|
||||||
- `zibll/css/main.css` L9411–9660(桌面端)+ L12470–12560(移动端)
|
|
||||||
- `zibll/js/comment.js`(交互行为参照)
|
|
||||||
- `zibll/img/smilies/*.gif`(约 100 个表情资源)
|
|
||||||
|
|
||||||
## 改动清单
|
|
||||||
|
|
||||||
### 1. 评论列表项 DOM — `inc/helpers/comments/item.php` 重写
|
|
||||||
输出 Zibll 原始结构(去掉所有 `aksrt-comment__*` 类):
|
|
||||||
```
|
|
||||||
li.comment#comment-{id}(用 WP comment_class(),含 depth-N)
|
|
||||||
└ ul.list-inline > li.comt-main#div-comment-{id}
|
|
||||||
├ div.comment-header.mb10 > div.author-box.flex.ac
|
|
||||||
│ ├ span.avatar-img.comt-avatar > img.avatar(含嵌套 ul.avatar-icontag 徽章位)
|
|
||||||
│ ├ <name>(置顶 badge.badge-topping.jb-blue + a.text-ellipsis.font-bold / b.mr6 + 作者 badg)
|
|
||||||
│ └ a.action.action-comment-like.muted-2-color.flex0.ml10(svg like + <count>)
|
|
||||||
└ div.comment-footer
|
|
||||||
├ div.mb10.comment-content#comment-content-{id}
|
|
||||||
└ div.comt-meta.muted-2-color
|
|
||||||
├ span.comt-author(time-ago,title=完整时间)· IP 属地 badg · span @父级作者
|
|
||||||
├ span.badge-approve > span.badg.c-red.badg-sm「待审核」
|
|
||||||
├ span.reply-link > a.comment-reply-link(data-commentid / data-postid)
|
|
||||||
└ span.dropdown.drop-fixed-sm.padding-6(menu_2 ⋮ 菜单:置顶/编辑/通过/删除,wp-ajax-submit 类)
|
|
||||||
子级:li.comment > ul.children > li.comment…
|
|
||||||
```
|
|
||||||
保留 Zibll 的自定义语义标签 `<name>` `<badge>` `<text>` `<count>` 以确保 CSS 选择器 1:1 命中。置顶/点赞/神评等数据仍取自现有 `inc/helpers/comments/actions.php`。
|
|
||||||
|
|
||||||
### 2. 评论区外壳与表单 — `inc/helpers/aksrt-comments.php` + `comments/form-parts.php` 重写
|
|
||||||
- 外壳:`div.theme-box#comments` > `div.box-body.notop > div.title-theme`(+ `<small>共N条</small>`/抢沙发)> `div.no_webshot.main-bg.theme-box.box-body.radius8.main-shadow`
|
|
||||||
- 登录门槛:`div.comment-signarea.text-center` > h3 + 登录/注册按钮 + 社交登录(复用现有 oauth 输出,仅换外壳类名)
|
|
||||||
- 移动端假输入:`div.flex.ac.jsb.virtual-input`(`fixed-input="#respond"`)
|
|
||||||
- 表单 `div#respond.mobile-fixed > form#commentform`:
|
|
||||||
- `div.flex.ac > div.comt-title.text-center.flex0.mr10`(comt-avatar 60px + 昵称)+ `div.comt-box.grow1`
|
|
||||||
- `div.action-text.mb10.em09.muted-2-color`(回复@xxx)
|
|
||||||
- `textarea.form-control.grin#comment`(Ctrl+Enter 提交)
|
|
||||||
- `div.comt-ctrl.relative`:comt-tips(隐藏字段+nonce)、comt-tips-right(`a.but.c-red#cancel-comment-reply-link` 取消 + `button.but.c-blue.pw-1em.comment-send#submit`)、comt-tips-left(`span.dropup#comment-user-info` 游客姓名/邮箱/网址 line-form 下拉 + 工具栏)
|
|
||||||
- 工具栏按 Zibll `zib_get_input_expand_but()` 结构:`span.dropup.relative > a.but.btn-input-expand.input-{smilie|code|image|link|quick}.mr6` + `div.dropdown-menu`(表情面板 `div.dropdown-smilie.scroll-y.mini-scrollbar > a.smilie-icon[data-smilie] > img`),按现有设置项开关
|
|
||||||
- 列表:`div#postcomments > ol.commentlist.list-unstyled`,过滤器 `div.comment-filter.tab-nav-theme.flex.ac.jsb`(最新/最热 + 只看作者)放在 commentlist 之前、#postcomments 内
|
|
||||||
- 分页:AJAX 模式 `div.theme-pagination.lazyload.ias-pagenav.pagenav > div.order-ajax-next > a.ias-btn 加载更多`;数字模式 `div.pagenav` + paginate_links(>8 页带 `a.pag-jump` 跳页 input)
|
|
||||||
- 空状态:`zib_get_null()` 同款 `img/null.svg` 结构
|
|
||||||
|
|
||||||
### 3. CSS 全量移植 — `assets/css/frontend/comments/`
|
|
||||||
- 把 `zibll/css/main.css` L9411–9660 与移动端 L12470–12560 规则逐条移植为新文件(按现有 shell/list/form/mobile 拆分或单文件 `zibll-comment.css`),选择器保持原样(`#comments .comt-title`、`#postcomments .commentlist > .comment > .list-inline .comment-footer {margin-left:65px; margin-top:-25px}`、`.children {margin-left:86px}`、`.children .children {margin-left:23px}`、`.smilie-icon {width:30px!important}` 等)
|
|
||||||
- 依赖的通用变量(`--main-shadow/--main-border-color/--main-radius/--main-bg-color`)与基础类(`.avatar-img`、`.but`、`.badg`、`.line-form`、`.pagenav`)核对 AKSRT 是否已有;缺的补进对应基础样式文件,值与 Zibll 一致
|
|
||||||
- **删除** 现有 `comments/shell.css`、`comments/mobile-item.css`、`comment-tools.css` 中所有旧评论样式与 `--aksrt-comment-*` 变量,无兼容层
|
|
||||||
- 保持 enqueue 结构(`src/Core/ScriptsPart1b.php` / `ScriptsPart1c.php`)指向新文件
|
|
||||||
|
|
||||||
### 4. JS 选择器对齐
|
|
||||||
- `assets/js/core/comments.js`、`comment-ajax.js`、`comment-tools.js`、`comment-fixed-input.js`、`comment-move-form.js`:全部选择器改为 Zibll 目标(`.commentlist`、`.comment-reply-link`、`#div-comment-{id}`、`.comment-send`、`#respond.mobile-fixed`、`[fixed-input]` 等),提交后的局部插入逻辑改为 Zibll 同款(prepend 到 `.children` 或 `.commentlist`,`slideDown`,15s 冷却倒计时按钮禁用)
|
|
||||||
- REST 端点与 nonce 机制不变;AJAX 分页/排序沿用现有 fragment 管线,仅换容器选择器
|
|
||||||
|
|
||||||
### 5. 资源(按用户确认)
|
|
||||||
- **删除 `assets/img/smilies/` 中现有全部旧表情包资源**,从 `zibll/img/smilies/` 拷贝全套 gif 替换;`content.php` 的 `[g=]` 转换与 data-smilie 表情面板统一指向该目录的 Zibll 文件名列表
|
|
||||||
- 空状态 `null.svg`、点赞/置顶/菜单等 svg 图标按 Zibll 形状补齐到 `assets/img/`
|
|
||||||
|
|
||||||
### 6. 清理与验证
|
|
||||||
- 全库 grep `aksrt-comment__`、`--aksrt-comment-`、`.comments-area`(评论语境)确保零残留(用户中心页的评论条目不在本次范围)
|
|
||||||
- 检查 `tests/widget-system/post-visual-contract.test.cjs` 是否引用旧评论类名,如有则同步更新契约
|
|
||||||
- `php -l` 校验全部改动 PHP 文件;运行 `node --test tests/widget-system/post-visual-contract.test.cjs`
|
|
||||||
- 桌面+移动端断点目测核对 Zibll 截图/本地对照环境(`tests/visual/README.md`)
|
|
||||||
|
|
||||||
## 不做的事
|
|
||||||
- 不写任何双路渲染、旧类名兼容或数据迁移代码
|
|
||||||
- 不动 `src/Core/CommentActions.php` REST 逻辑、管理后台 Vue 设置页(除非契约字段需对齐)
|
|
||||||
- 不动论坛/商铺评论(Zibll 中亦为独立模块)
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
## 修复方案
|
|
||||||
|
|
||||||
### 1. 删除废弃的静态 action 入口
|
|
||||||
|
|
||||||
修改 `assets/js/runtime/main.js`:
|
|
||||||
|
|
||||||
- 完整删除全局 `_win.bd.on('click', '[data-action]', ...)` 委托。
|
|
||||||
- 完整删除仅被该委托调用的 `action_ajax()` 函数。
|
|
||||||
- 保留紧随其后的独立 `[ajax-action]` 处理器以及其他合法的 `data-action` 属性读取。
|
|
||||||
- 不创建 `assets/action/action.php`,不增加兼容代理,不把旧协议映射到新接口。
|
|
||||||
|
|
||||||
仓库内文章互动、评论点赞、关注等已有 WordPress AJAX/REST 实现,因此这是单一路径硬切换。
|
|
||||||
|
|
||||||
### 2. 精确同步运行时压缩产物
|
|
||||||
|
|
||||||
修改 `assets/js/runtime/main.min.js`:
|
|
||||||
|
|
||||||
- 从唯一 `_win.bd.on("click","[data-action]"` 起点,精确删除到下一个 `_win.bd.on("click","[ajax-action]"` 起点之前的旧压缩块。
|
|
||||||
- 不执行全量前端构建,以免覆盖或重新格式化其他已有运行时产物。
|
|
||||||
- 删除后确认 `[ajax-action]` 处理器仍存在,`action_ajax` 和 `/action/action.php` 均消失。
|
|
||||||
|
|
||||||
当前该文件虽然出现在状态列表中,但内容与 HEAD 完全一致,因此可安全形成仅针对旧块的内容差异。
|
|
||||||
|
|
||||||
### 3. 增加硬切换契约测试
|
|
||||||
|
|
||||||
新增 `tests/widget-system/runtime-action-hard-cut-contract.test.cjs`:
|
|
||||||
|
|
||||||
- 同时检查 `main.js` 与 `main.min.js`。
|
|
||||||
- 断言旧全局 `[data-action]` 委托不存在。
|
|
||||||
- 断言 `action_ajax` 不存在。
|
|
||||||
- 断言 `/action/action.php` 不存在。
|
|
||||||
- 断言独立 `[ajax-action]` 处理器仍保留且只有一份。
|
|
||||||
- 不禁止整个仓库使用 `data-action`,避免误伤下载、弹窗和其他新业务属性。
|
|
||||||
|
|
||||||
### 4. 验证
|
|
||||||
|
|
||||||
- 运行新增的 runtime action 契约测试。
|
|
||||||
- 运行全部 `tests/widget-system/*.test.cjs`。
|
|
||||||
- 使用 `rg` 检查源码和压缩产物中不存在 `action/action.php`、`action_ajax` 和旧委托。
|
|
||||||
- 执行 `git diff --check`,确认没有格式问题。
|
|
||||||
- 不做真实视觉验收,不恢复静态 PHP 直连入口。
|
|
||||||
|
|
||||||
### 5. 提交
|
|
||||||
|
|
||||||
从当前 `origin/main` 创建 `worktree/fix-legacy-action-404` 分支,只提交:
|
|
||||||
|
|
||||||
- `assets/js/runtime/main.js`
|
|
||||||
- `assets/js/runtime/main.min.js`
|
|
||||||
- `tests/widget-system/runtime-action-hard-cut-contract.test.cjs`
|
|
||||||
|
|
||||||
现有其他 CSS、JavaScript 状态项和 `.zcode/plans` 文件不进入提交。验证通过后推送该分支到远端。
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
## 目标
|
|
||||||
|
|
||||||
在后台「AKSRT → 功能&权限 → 下载配置」中新增总开关 `enabled`(switch,**默认开启**)。开启 = 现有本站 token 中转下载(`/download/{token}`,别名/次数/有效期生效);关闭 = 直连模式,下载按钮直接渲染原始下载地址,不再生成本站中转链接。**权限门控保留**(未登录/未购买/非会员仍显示解锁提示,已确认)。不写迁移、不留死代码、不做真实视觉效果验收。
|
|
||||||
|
|
||||||
## 现状(调研结论)
|
|
||||||
|
|
||||||
- 「下载配置」是 SPA 通用 schema 页(`SchemaPage.vue`),PHP schema 在 `src/Admin/Schema/SchemaPermissions.php:73-85`(option `aksrt_download_config`),前端镜像在 `assets/admin/src/schema/sections/permissions/index.js:246-269`
|
|
||||||
- 下载链路:按钮 `data-action="aksrt-download"` → `POST /aksrt/v1/download/request`(`src/Content/Download.php:215`)签发 token → 返回 `home_url('/别名/{token}')` → rewrite + `maybe_serve_download()` 中转(本地流式/302 外链)
|
|
||||||
- 发现死代码:`rest_policy()` + `/download/policy` 路由(全库无调用方)、`aksrt_is_download_alone_page_enabled()`(`inc/helpers/aksrt-pay.php:921`,无调用方)
|
|
||||||
|
|
||||||
## 变更清单
|
|
||||||
|
|
||||||
**1. `src/Admin/Schema/SchemaPermissions.php`(PHP 权威 schema)**
|
|
||||||
- download section version 1.0.0 → 1.1.0
|
|
||||||
- groups 重构(仿 login/moderation 模式):新 `basic` 组「下载总开关」,字段 `enabled`(switch,default true,help 说明「关闭后下载采用直连,下方链接规则不生效」);原 `url_alias`/`default_max_uses`/`ttl_minutes` 移入 `rules` 组「链接规则」,各加 `dependsOn: {key: 'enabled', value: true}`
|
|
||||||
|
|
||||||
**2. `assets/admin/src/schema/sections/permissions/index.js`(前端镜像)**
|
|
||||||
- `download.defaults` 增加 `enabled: true`;groups 同步 PHP 双组结构(switch + dependsOn);更新 description
|
|
||||||
|
|
||||||
**3. `src/Content/Download.php`**
|
|
||||||
- 新增 `public function is_enabled(): bool`:读 `aksrt_download_config`,`enabled` 键缺失视为开启(默认 true),否则 `!empty()` 判定(与 PostDisplay 等现有默认 true 惯用法一致)
|
|
||||||
- 关闭时提前 return:`register_rewrite()`(不注册路由)、`register_routes()`(不注册 `/download/request`)、`maybe_serve_download()`(旧 token 链接失效,含 `?aksrt_dl_token=` 直查)
|
|
||||||
- **删除死代码**:`rest_policy()` 方法与 `/download/policy` 路由注册
|
|
||||||
- token 签发/流式输出等开启路径逻辑不动(现有契约测试正则不受影响)
|
|
||||||
|
|
||||||
**4. `templates/parts/resource-download-box.php`**
|
|
||||||
- 顶部计算 `$direct_download`(`class_exists` 守卫 + `is_enabled()` 取反)
|
|
||||||
- 下载源循环:直连模式且非 need_login 时,渲染 `<a href="{原始url}" target="_blank" rel="nofollow noopener">`(保留网盘识别徽章/着色),提取码旁复用全局 clipboard 委托渲染复制按钮(`data-clipboard-text`,main.min.js 已全局处理);need_login 分支不变
|
|
||||||
- 直连模式下 `$captcha_active` 置 false(验证码容器成死 UI)
|
|
||||||
- 独立下载页入口(`down_alone_page`)是独立功能,不与本开关耦合
|
|
||||||
|
|
||||||
**5. `inc/helpers/aksrt-pay.php`**
|
|
||||||
- `aksrt_get_paid_download_actions_html()`:直连模式渲染 `<a class="but ...">` 直链替代 token 按钮;提取码/复制按钮逻辑不变
|
|
||||||
- **删除死代码** `aksrt_is_download_alone_page_enabled()`
|
|
||||||
|
|
||||||
**6. `tests/widget-system/download-config-contract.test.cjs`(扩展契约测试)**
|
|
||||||
- 字段对齐测试加入 `enabled => true`(PHP)/ `enabled: true`(JS)
|
|
||||||
- 新增断言:Download.php 含 `is_enabled` 守卫;`/download/policy` 已移除;直连模式下盒模板与付费卡函数含直链 `<a>` 渲染分支
|
|
||||||
|
|
||||||
## 行为语义(写入代码注释与测试预期)
|
|
||||||
|
|
||||||
- 默认开启,存量站点零感知(不迁移 option,键缺失即 true)
|
|
||||||
- 关闭后:无下载计次、无免费额度扣减、无下载就绪通知、无验证码——直连语义的自然结果
|
|
||||||
- 关闭后旧 `/download/{token}` 链接全部失效
|
|
||||||
|
|
||||||
## 验证
|
|
||||||
|
|
||||||
- `php -l` 检查所有改动的 PHP 文件
|
|
||||||
- `node --test tests/widget-system/download-config-contract.test.cjs`,并跑 pay-card-contract、post-visual-contract 确认无回归
|
|
||||||
- `cd assets/admin && npm run build` 重建 SPA(前端 schema 变更需构建生效);若 `assets/admin/build/` 被 git 跟踪则一并纳入变更
|
|
||||||
- 按要求跳过真实视觉效果验收
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
## 目标
|
|
||||||
|
|
||||||
把"点关注"从同步跑完整结算链(积分+勋章×3遍+等级,约几十条 SQL)优化为:接口只写关注表立即返回,结算在响应送达客户端之后执行。
|
|
||||||
|
|
||||||
## 改动内容(3 个文件)
|
|
||||||
|
|
||||||
### 1. `src/User/Metrics.php` — 删除冗余订阅
|
|
||||||
`on_user_followed()`(Metrics.php:93-98)只做 `reevaluate_medals(following_id)`,而 `BadgeAutoAward::on_follow` 已经评估过该用户——这是同一请求内的第 3 遍重复评估。删除该方法和它的 `add_action` 注册(Metrics.php:34)。`reevaluate_medals` 方法本身保留(点赞/浏览等其他钩子仍在用)。
|
|
||||||
|
|
||||||
### 2. `src/User/BadgeAutoAward.php` — 删除对"关注者"的无效评估
|
|
||||||
`on_follow()`(BadgeAutoAward.php:61-68)同时评估关注者和被关注者,但 `Badges::metric_value` 的全部指标分支(registration_time/checkin/new_post/comment/**followed(被关注数)**/like/views/pay_income/pay_rebate)没有任何一个依赖"关注了多少人"——关注者评估必然零产出。改为只评估 `$following_id`(保留双参数签名以兼容钩子,`unset($follower_id)`)。
|
|
||||||
|
|
||||||
### 3. `src/User/Follow.php` — 结算钩子延迟到响应之后
|
|
||||||
`follow()` 里的 `do_action('aksrt_user_followed', ...)`(Follow.php:50)改为经 `register_shutdown_function` 延迟执行:关闭回调里先调 `fastcgi_finish_request()`(PHP-FPM 下存在)把响应立即送达客户端,再触发钩子跑积分/勋章/等级结算。新增一个 `private static function defer_action(string $hook, int ...$args)` 辅助方法。
|
|
||||||
|
|
||||||
- 非-FPM 环境(无 `fastcgi_finish_request`):退化为 shutdown 时机同步执行,行为与现状等价,无回归。
|
|
||||||
- 不用 `wp_schedule_single_event`:WP-Cron 粒度不可控(禁用循环回执的站点要等系统 cron,积分勋章到账会延迟数分钟),shutdown 方案在所有环境都能即时结算。
|
|
||||||
- `aksrt_user_followed` 仅 Follow.php:50 一处触发,4 个订阅者全部按传入 ID 工作、不依赖请求上下文,延迟执行安全。
|
|
||||||
- 效果:关注接口响应路径只剩 WP REST 启动 + 1 次 SELECT + 1 次 INSERT + 1 次 COUNT(全部命中索引),勋章评估从 3 遍降为 1 遍且不再阻塞响应。
|
|
||||||
|
|
||||||
## 不在本次范围
|
|
||||||
|
|
||||||
前端乐观更新(点击瞬间翻转按钮、失败回滚)——上轮标注为可选项,用户未明确要求;异步化后反馈延迟已大幅下降,且与全站点赞/收藏的交互模式保持一致。需要可后续单独做。
|
|
||||||
|
|
||||||
## 验证
|
|
||||||
|
|
||||||
- `php -l` 检查 3 个改动文件。
|
|
||||||
- 浏览器实测:关注点击后按钮/Toast 应明显变快(仅剩接口往返);稍后刷新可见积分/勋章/等级照常到账(结算仍在同一请求生命周期内完成,只是后置)。
|
|
||||||
- 取消关注路径不受影响(本就无钩子)。
|
|
||||||
|
|
||||||
## 提交
|
|
||||||
|
|
||||||
单条提交:`perf(user): 关注结算异步化并去除冗余勋章评估`。
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# 修复前台文章卡片无刷新加载出现空位的问题
|
|
||||||
|
|
||||||
## 根因
|
|
||||||
|
|
||||||
规范分页控件(`aksrt_render_pagination()` 输出的 `.ajax-pag[data-aksrt-pagination]`,内含 `.next-page.ajax-next`)携带了遗留引擎的触发类 `ajax-next`。全站加载的 `main.min.js` 在 body 上委托监听 `.ajax-next` 点击(另有 IAS 无限滚动路径用 jQuery 模拟点击 `.ajax-next/.ias-btn`),导致每次"加载更多"被两套引擎同时处理:
|
|
||||||
|
|
||||||
1. **规范引擎**(pagination.js):POST `aksrt_load_more_posts`,把卡片正确插入 `.ajax-pag` 之前;
|
|
||||||
2. **遗留引擎**(main.min.js `post_ajax()`):抓取整页 `?paged=2` HTML,**先删除 `.ajax-pag`**,把占位加载器、重复的一批卡片和一个新的**块级** `.ajax-pag` div 插到最后一张卡之后。
|
|
||||||
|
|
||||||
卡片网格是 inline-block 布局(`width: calc(33.333% - 16px)`),块级分页 div / 重复批次插入流中会强制换行 → 网格中出现空位(即"隔出一个卡片位置"),且第 2 页内容被加载两次。评论分页(`inc/helpers/template-tags/search-meta.php:533`)已用 `order-ajax-next` + `ias-btn` 避开遗留引擎,文章分页是唯一例外。
|
|
||||||
|
|
||||||
另经确认:生产环境实际生效的懒加载运行时是 main.min.js 内置版(`assets/js/lazysizes.js` 无任何 enqueue),其 IAS 自动加载走 jQuery 模拟点击,只到得了遗留引擎——所以无限滚动模式目前完全由遗留引擎执行。若只摘掉 `ajax-next` 类而不接管 IAS 触发,无限滚动会失效,因此两处改动必须配套。
|
|
||||||
|
|
||||||
## 修改内容(4 个文件)
|
|
||||||
|
|
||||||
### 1. `inc/helpers/template-tags/pagination-permissions.php:123`
|
|
||||||
`.next-page` 包装类的 `ajax-next` → `order-ajax-next ias-btn`:
|
|
||||||
- 移除遗留引擎的点击触发 token(手动点击与 IAS 模拟点击双双失效于遗留引擎);
|
|
||||||
- `order-ajax-next` 复用现有按钮样式 `.theme-pagination .order-ajax-next a`(main.css:9659+),**零 CSS 改动**;
|
|
||||||
- `ias-btn` 保持 main.min.js 的 IAS 匹配器仍能找到元素(其后续 jQuery 点击成为无害空操作),且 pagination.js 的 clickTarget 选择器 `.next-page.ajax-next, .ias-btn` 原生支持,**零 JS 选择器改动**;
|
|
||||||
- 与评论分页的既有模式完全一致。
|
|
||||||
|
|
||||||
### 2. `assets/js/core/pagination.js`
|
|
||||||
在点击绑定块后新增容器级 `lazybeforeunveil` 监听(仅当服务端输出了 `lazyload` 契约类时挂载):unveil 事件由 main.min.js 内置 lazySizes 原生派发(可冒泡),监听到后设置 `lazyAuto` 并原生 `click()`,把无限滚动自动加载**收归规范引擎**的既有点击管线(复用 loading 守卫、iasMax 上限、按钮状态机):
|
|
||||||
```js
|
|
||||||
if (container.classList.contains("lazyload") || clickTarget.classList.contains("lazyload")) {
|
|
||||||
container.addEventListener("lazybeforeunveil", function (e) {
|
|
||||||
if (e.target !== container && e.target !== clickTarget) return;
|
|
||||||
clickTarget.dataset.lazyAuto = "1";
|
|
||||||
clickTarget.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
附带修复:评论分页的 IAS 自动加载此前同样到不了规范引擎(jQuery 模拟点击不可见),此监听一并接管。
|
|
||||||
|
|
||||||
### 3. `search.php:112-118` — 消除双层包裹
|
|
||||||
去掉外层冗余 `<div class="posts-row mtn6">`,把 `mtn6` 直接传入 `aksrt_render_post_list(['wrapper_class' => 'mtn6'])`。这样规范引擎的 `previousElementSibling` 目标解析落在真正的卡片网格上,追加的卡片与首页批在同一 inline-block 网格内连续排布(此前新卡片会追加为内层网格块的兄弟节点,卡片模式下在分页边界处出现空位)。ajax-list.js(搜索 tab 切换的实际引擎)用 `querySelectorAll('.ajax-item')` 提取、其替换流程本身就会把卡片拍平到容器层,结构兼容。
|
|
||||||
|
|
||||||
### 4. `tests/widget-system/post-visual-contract.test.cjs:238`
|
|
||||||
- 断言 `/class="next-page ajax-next/` → `/class="next-page order-ajax-next ias-btn/`;
|
|
||||||
- 新增 `assert.doesNotMatch(php, /next-page ajax-next/)` 锁定规范控件不再携带遗留触发类;
|
|
||||||
- 新增 `assert.match(js, /lazybeforeunveil/)` 锁定 IAS 接管。
|
|
||||||
|
|
||||||
## 验证
|
|
||||||
|
|
||||||
- `php -l inc/helpers/template-tags/pagination-permissions.php` 和 `php -l search.php`
|
|
||||||
- `node --test tests/widget-system/post-visual-contract.test.cjs`
|
|
||||||
- 浏览器实测(桌面 3 列卡片网格):首页点"加载更多"只发一个 `admin-ajax` 请求、无重复批次、无网格空位、`.ajax-pag` 始终位于网格下方;无限滚动模式每次自动加载一页并在 iasMax 后转手动按钮;搜索页卡片模式追加后与首批网格连续。
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# 评论区硬切换:1:1 复刻 Zibll(不含表情包/快捷回复)
|
|
||||||
|
|
||||||
## 现状与结论
|
|
||||||
- AKSRT 评论区的 PHP 渲染(`inc/helpers/comments/item.php`、`form-parts.php`、`aksrt-comments.php`)与 JS(`assets/js/core/comments.js` 等走 `aksrt/v1` REST)已是自有代码,DOM 与 Zibll 基本一致(`li#comment-{id} > ul.list-inline > li.comt-main`、`.comment-header/.comment-content/.comt-meta`、`#commentform/.comt-title/.comt-box/.comt-ctrl` 均已对齐)。
|
|
||||||
- **样式仍由 vendored `assets/css/main.min.css` 提供**(拼进全站 `build/public.css`;`comments/shell.css` 头部明文"以 vendor 正版样式为准、禁止复制")——这是本次硬切换的主体:样式归属权切到自有文件,vendor 副本剥离。
|
|
||||||
- 快捷回复链路完整存在(schema/PHP/JS/CSS),需全链路删除;表情包已被此前的 commit 删除并有契约测试锁定,保持。
|
|
||||||
- 传输层不动:提交/点赞/编辑/删除/置顶仍走 AKSRT REST(含权限回调),不改回 Zibll 的 admin-ajax 协议;UI 与 DOM 对齐 Zibll。
|
|
||||||
|
|
||||||
## 1. CSS 硬切换(主体)
|
|
||||||
- 以参考主题 `C:\Users\Xing\Desktop\zibll\css\main.css` 为准移植评论区规则(与 AKSRT vendored 副本 diff 后以参考版为准):
|
|
||||||
- 新建 `assets/css/frontend/comments/layout.css`:桌面主段(AKSRT main.css 约 9273–9510 对应段:`#comments/#postcomments/#respond/#comment/.commentlist/.comt-*/.comment-*/.comment-signarea/.children/.comment-footer 缩进` 等)+ 散点规则(`.action-comment-like.action-animation`、`.comment-content .box-img`、`.comment-content a`、`.comment-content pre`)。
|
|
||||||
- 填充 `assets/css/frontend/comments/mobile-item.css`(现为空壳占位):移动端 `@media` 内评论规则(`.mobile-fixed/.virtual-input/头像缩进/分页` 等,main.css 约 12208 起)。
|
|
||||||
- `shell.css` 重写文件头教条(不再"vendor 提供"),保留 identity chip/medal 补充规则。
|
|
||||||
- **剔除**:`.dropdown-smilie/.smilie-icon/.dropdown-quick-often` 与 `img:not(.smilie-icon)` 类选择器。
|
|
||||||
- 从 vendored `assets/css/main.css`/`main.min.css` 剥离同批评论区域专属规则(死代码清理、消除双路)。保留:`.comment-widget-item/.comment-mini-lists`(侧栏小部件在非 singular 页使用)及共享选择器(`.pagenav/.theme-pagination/.line-form/.author-box/.avatar-img` 等别处仍用)。
|
|
||||||
- 先校验 `main.min.css ≙ minify(main.css)`(esbuild dry-run diff,防漂移),剥离后重新生成 `main.min.css`,再跑 `node tools/build-public-assets.mjs` 重出 bundle。
|
|
||||||
|
|
||||||
## 2. Markup 1:1 对齐(PHP 小增量)
|
|
||||||
- `item.php`:点赞按钮对齐 Zibll `zib_get_comment_like`(SVG 图标 + `<text></text><count>N</count>`,保留 AKSRT `data-comment-action` 接线);置顶/批准/菜单/注册图标由 FA 换成 Zibll SVG——新增小助手(如 `inc/helpers/comments/icons.php`)移植 `zib_get_svg` 所需 glyph(like/topping/approve/menu_2/signup);时间/回复链接 tooltip 属性对齐。
|
|
||||||
- `aksrt-comments.php` + `form-parts.php`:
|
|
||||||
- `!comments_open()` 时整体不渲染(Zibll 行为;当前会渲染标题+列表)。
|
|
||||||
- 工具栏渲染 `code→image→link`,补齐缺失的"链接"按钮(新设置 `comment_link`,默认关——Zibll 默认值即 false)。
|
|
||||||
- 游客 dropup 补"博客地址"字段(新设置 `comment_user_url_s`,默认关,同 Zibll)。
|
|
||||||
- `comt-title` 登录态昵称旁渲染 AKSRT 身份徽章(对应 Zibll VIP 图标位)。
|
|
||||||
|
|
||||||
## 3. 快捷回复全链路删除 + 死代码清理
|
|
||||||
- PHP:`form-parts.php` quick case 与工具栏分支;`aksrt-comments.php` context 的 `quick_s/quick_phrases`;`PostDisplay.php` 的 `quick_phrases` 字段与 `parse_comment_quick_phrases()`;`SchemaPostList.php` 镜像字段。
|
|
||||||
- Vue schema:`assets/admin/src/schema/sections/post-list/comment.js` 删 `comment_quick_s/comment_quick_often`。
|
|
||||||
- JS:`comment-tools.js` 快捷短语插入逻辑;`ScriptsPart1c.php` 删 `aksrtData.commentQuickPhrases`。
|
|
||||||
- 删除死文件:`assets/js/runtime/comment.js(+.min)`、`input-expand.js(+.min)`(已确认无代码引用)。
|
|
||||||
- `aksrt-runtime.js` 的 vendor comment 模块 stub 与委托解绑**保留**(vendor `main.min.js` 仍在全国 bundle,防回潮)。
|
|
||||||
|
|
||||||
## 4. 设置 schema(前后端镜像)
|
|
||||||
- 新增 `comment_link`、`comment_user_url_s` 开关(默认 false),镜像到 `SchemaPostList.php` 与 `PostDisplay::get_comment_display_settings()`;`npm run build` 重出 admin SPA。
|
|
||||||
|
|
||||||
## 5. 测试
|
|
||||||
- 更新 `comment-smilies-removal-contract.test.cjs`(文件清单移除已删 runtime 存档)。
|
|
||||||
- 新增 `tests/widget-system/comment-hard-cut-contract.test.cjs`:自有 comments CSS 含关键选择器清单;vendor main.css/min 不再含评论区选择器、仍含 `.comment-widget-item`;全库无快捷回复 token(`comment_quick_s/quick-reply-item/dropdown-quick-often/commentQuickPhrases/quick_phrases` 等);死文件不存在;markup 契约(`<count>`/SVG、工具栏 code/image/link 且无 quick/smilie)。
|
|
||||||
|
|
||||||
## 6. 构建与验证(不做视觉验收)
|
|
||||||
- `php -l` 全部改动 PHP;`node --test` 相关契约测试;`node tools/build-public-assets.mjs`;`npm run build`(admin)。
|
|
||||||
- 不启用 Docker 视觉对比环境(按用户要求)。
|
|
||||||
|
|
||||||
## 风险与对策
|
|
||||||
- vendor main.css 手术:先 min-sync 校验;剥离仅限评论专属选择器,逐个 grep AKSRT 模板确认无跨区使用;剥离后选择器清单复核。
|
|
||||||
- 评论区规则的 CSS 变量(`var(--…)`)与 bootstrap 类来自 bundle 中更早加载的 tokens.css/bootstrap,`aksrt-comments` 在其后加载,无影响。
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
# 公开用户主页去参数化 + 全链路无刷新加载
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
|
|
||||||
`/author/demo/` 的 URL 永远干净(无 `?tab=`、`follow_sub`、`follow_page`、`posts_page` 等),Tab 切换、粉丝/关注子切换、各面板分页全部走 REST AJAX 无刷新加载。单路径实现,不写双路/兼容/迁移代码。
|
|
||||||
|
|
||||||
现状:主 Tab 已是 DOM 切换,但 `author.js` 会把 `?tab=` 写进 URL;follow 子切换和 comments/follow 分页是带参整页链接;posts/favorites 分页走 `aksrt_render_pagination()`(numbered 模式产出 `?posts_page=N` 链接;load_more/infinite 走 admin-ajax)。
|
|
||||||
|
|
||||||
## 服务端
|
|
||||||
|
|
||||||
### 1. 上下文构建 — `inc/helpers/aksrt-user-home.php`
|
|
||||||
- `aksrt_get_user_home_page_context()` 删除全部 `$_GET` 读取(第 104、134-136 行):`active_tab` 恒等于 `default_tab`,三个 page 恒为 1(保留键,供 REST 覆写);新增 `follow_page => 1`、`follow_sub => 'followers'` 键。
|
|
||||||
- 新增 `aksrt_render_user_home_pagination(int $total, int $current)`:输出 `<nav class="profile-pagination uh-pagination">` + `<button type="button" class="page-numbers" data-uh-page="N">`(当前页带 `current` 类,含 `‹ ›` 前后页),复用现有 `.profile-pagination` 样式(lists.css:331+)。
|
|
||||||
- 新增 `aksrt_capture_user_home_panel(string $tab, array $ctx): string`:`locate_template('templates/parts/user-home/panel-{$tab}.php')` + ob_start 捕获(仿 `aksrt_capture_user_center_panel`,aksrt-user-center.php:199)。
|
|
||||||
|
|
||||||
### 2. 面板模板重构 — `templates/parts/user-home/`
|
|
||||||
- 删除 `panels-core.php`,拆为按面板 body 模板:`panel-posts.php`、`panel-comments.php`、`panel-favorites.php`、`panel-credits.php`;`panel-follow.php` 改为纯 body(去掉外层 wrapper,第 21-23 行 `$_GET` 读取改为从 `$args` 取 `$follow_page`/`$follow_sub`)。
|
|
||||||
- `page-user-home.php`:dashboard 根节点加 `data-user-id="<?php echo (int) $user_id; ?>"`;循环 `$enabled_tabs`,对存在 body 模板的 tab 输出 `<div id="panel-{tab}" class="dashboard-panel…">` wrapper + `get_template_part('templates/parts/user-home/panel', $tab, $ctx)`。
|
|
||||||
- 各 body 分页统一改用 `aksrt_render_user_home_pagination()`:
|
|
||||||
- posts/favorites 不再用 `aksrt_render_pagination()`(本页退出全站三模式分页体系);
|
|
||||||
- comments 不再用 `aksrt_render_profile_pagination()`(该函数保留,用户中心仍在用)。
|
|
||||||
- `panel-follow.php` 子切换 `<a href="?tab=follow&…">`(第 40、47 行)→ `<button type="button" data-uh-sub="followers|following">`;`paginate_links`(第 63-69 行)→ 新分页渲染器。
|
|
||||||
|
|
||||||
### 3. REST 端点 — `src/User/Home.php`(仿 `src/User/Center.php:34-110`)
|
|
||||||
- `init()` 挂 `rest_api_init`;路由 `GET aksrt/v1/user-home/panel/(?P<user_id>\d+)/(?P<tab>[a-z0-9_-]+)`,query 参数 `page`(int ≥1)、`sub`(followers|following),`permission_callback => '__return_true'`(公开数据)。
|
|
||||||
- `rest_panel()`:校验用户存在 → `aksrt_get_user_home_page_context()` 重建上下文 → 用请求参数覆写 `active_tab`、对应 page、`follow_sub` → tab 不在 `enabled_tabs`(如 credits 非本人)返回 404 → 捕获 body → 返回 `{success:true, data:{tab, html}}`。
|
|
||||||
|
|
||||||
## 前端 — `assets/js/author.js`
|
|
||||||
|
|
||||||
- 删除 `updateTabUrl()`(第 15-24 行)与 `resolveInitialTab()`(第 70-73 行):不再读写任何 URL 参数,初始态以服务端渲染的 `is-active`/`data-default-tab` 为准。
|
|
||||||
- 新增 `loadPanelBody(tab, page, sub)`:`fetch(aksrtData.apiUrl + '/user-home/panel/{data-user-id}/{tab}?page=N(&sub=X)', {credentials:'same-origin', headers:{'X-WP-Nonce': aksrtData.restNonce}})` → `panel.innerHTML = data.html` → `AksrtFrontend.rehydrate(panel, 'ajax')`;per-panel 请求去重防竞态(仿 user-center.js `_aksrtPanelRequest`);`is-loading` 态 + 失败 notyf 提示。
|
|
||||||
- dashboard 根上事件委托(覆盖 AJAX 注入的新按钮):
|
|
||||||
- `[data-uh-sub]` → 加载 follow 面板 `sub=X, page=1`;
|
|
||||||
- `[data-uh-page]` → `btn.closest('.dashboard-panel')` 得 tab,`panel.dataset.uhSub`(默认 followers)得子项,加载对应页。
|
|
||||||
- URL 永不变更(无 pushState/replaceState)。`follow.js` 是 document 级委托,注入的 minicard 关注按钮自动可用,无需改动。
|
|
||||||
|
|
||||||
## 附带清理与 CSS
|
|
||||||
|
|
||||||
- `templates/parts/submit/editor-main.php:106-108`:「查看草稿」现指向公开主页 + `?tab=post&post_status=draft`(公开主页从未支持该参数,本就错位)。改为 `add_query_arg('post_status', 'draft', aksrt_get_user_center_tab_url('posts'))`(用户中心支持该过滤,posts-list.php:14-16)。
|
|
||||||
- `assets/css/routes/user-home.css`:`button.page-numbers` 复位 + hover 态(现 hover 选择器只覆盖 `a.page-numbers`);`.uh-follow__subtab` 改 button 后的复位;`.dashboard-panel.is-loading` 半透明过渡。
|
|
||||||
|
|
||||||
## 测试与验证
|
|
||||||
|
|
||||||
- 更新 `tests/widget-system/post-visual-contract.test.cjs`:移除 `panels-core.php`/`page_var` 断言(第 39、205、222-224、328-330 行),改为断言:各 `panel-*.php` body 模板存在且调用 `aksrt_render_user_home_pagination`;user-home 模板无 `$_GET`/`add_query_arg`;author.js 无 `searchParams`(防回归契约)。
|
|
||||||
- `php -l` 全部改动 PHP 文件;`node --test tests/widget-system/post-visual-contract.test.cjs`。
|
|
||||||
|
|
||||||
## 明确的行为变化(单路径的直接结果)
|
|
||||||
|
|
||||||
- 公开主页 posts/favorites 分页不再跟随后台「文章分页模式」(numbered/加载更多/无限滚动)设置,统一为本页 AJAX 数字分页。
|
|
||||||
- 刷新/分享/回退永远回到默认 Tab 第 1 页(URL 无状态,需求本身决定)。
|
|
||||||
- 旧外链 `?tab=follow` 不解析不重定向,按默认 Tab 加载(无兼容代码)。
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# 复刻 Zibll 粉丝/关注用户列表(公开主页 follow TAB)
|
|
||||||
|
|
||||||
## 问题根源
|
|
||||||
|
|
||||||
`templates/parts/user-home/panel-follow.php:46-65` 手写的列表 markup(`.uh-follow__list/item/avatar/name`)在全仓库没有任何 CSS 定义,头像+用户名以内联文本堆叠显示。列表项也只有头像和用户名,没有 Zibll 的卡片结构。
|
|
||||||
|
|
||||||
关键事实:AKSRT 已 vendored Zibll 的 `main.css`(`.author-minicard` 卡片规则、工具类齐全),且 `inc/helpers/aksrt-graphic-card.php:215` 已有 `aksrt_author_minicard()` —— Zibll `zib_author_card()` 的移植版(42px 懒加载头像、用户名+勋章/认证/等级图标、逐项关注按钮、个性签名一行省略、默认文案"这家伙很懒,什么都没有写…"),搜索页在用。**修复 = 粉丝/关注列表改走这条既有链路**,不新写卡片样式。
|
|
||||||
|
|
||||||
## 改动清单
|
|
||||||
|
|
||||||
### 1. `templates/parts/user-home/panel-follow.php`(核心)
|
|
||||||
- 列表循环改为 `aksrt_author_minicard((int) $row['user_id'])`,删除手写的 `.uh-follow__item/avatar/name` markup(渲染链路与搜索页用户列表完全一致)。
|
|
||||||
- 子切换复刻 Zibll 结构(`zib-author.php:407-410`):居中文字链接「粉丝 N | 关注 N」(`list-inline splitters` + 激活 `focus-color` / 非激活 `muted-color`),**顺序改为粉丝在前、默认子列表改为粉丝**(Zibll 逻辑:TAB 名"粉丝"、默认显示粉丝列表,`zib_get_follow_user_list` 默认 `type='followed'`)。链接仍走现有 `?follow_sub=&follow_page=` 路由。
|
|
||||||
- 空状态图标换心形 SVG(见第 6 条),继续用 `aksrt_render_profile_empty` 组件(对齐 Zibll null-love 爱心语义)。
|
|
||||||
- 分页保持 `paginate_links` 服务端翻页:Zibll 的 AJAX"加载更多"框架(post_ajax/ajax-next)未移植进 AKSRT(main.js 仅 27 行),且公开主页所有 TAB 统一为服务端渲染 + URL 参数翻页,不为此引入独立 AJAX 体系。
|
|
||||||
|
|
||||||
### 2. `inc/helpers/aksrt-graphic-card.php` — `aksrt_author_minicard()`
|
|
||||||
- 关注按钮改为**服务端渲染真实状态**(复刻 Zibll 逻辑):已登录且 `\AKSRT\User\Follow::is_following(current, target)` 为真时输出 `is-following` class +「已关注」文案。消除每次进页"全部显示关注→异步闪变"的问题。搜索页同样受益。
|
|
||||||
|
|
||||||
### 3. `templates/parts/user/author-hero.php`
|
|
||||||
- hero 关注按钮(L151-163)同样服务端渲染真实初始状态(`is-following` +「已关注」),与卡片按钮同一逻辑。
|
|
||||||
|
|
||||||
### 4. `assets/js/follow.js`
|
|
||||||
- 删除逐按钮 `/follow/status` 轮询(L54-69):服务端已输出真值,不留双路。保留点击 toggle、`updateFollowButton` 状态切换、用户中心取关移除行、计数更新。
|
|
||||||
|
|
||||||
### 5. `assets/css/routes/user-home.css` — follow 区样式替换
|
|
||||||
- `.uh-follow__list { text-align: center; }`:对齐 Zibll pane 的 `text-center`(卡片为 inline-block,自动 3 列居中换行,移动端单列规则 main.css 已有)。
|
|
||||||
- 子切换:删除旧 pill 按钮样式块(L486-519),替换为 Zibll 风格的少量布局规则(居中、间距),颜色/激活态复用 vendored 工具类。
|
|
||||||
- 同步删除不再被引用的 `__subtab-count` 等旧规则。
|
|
||||||
|
|
||||||
### 6. `inc/helpers/aksrt-user-home.php`
|
|
||||||
- context 数组新增 `svg_empty_follow`(心形 stroke SVG,风格同现有 `svg_empty_post` 等)。
|
|
||||||
|
|
||||||
## 不做的事(明确排除)
|
|
||||||
- 不引入 AJAX"加载更多"(无框架基础,破坏公开主页统一服务端分页架构)。
|
|
||||||
- 不动数据层 `src/User/Follow.php`(`get_user_list` 已提供所需 user_id;徽章/签名由 minicard 自行取)。
|
|
||||||
- 不写任何双路、兼容、迁移代码:旧的手写列表 markup 和 pill 子切换样式直接替换删除。
|
|
||||||
|
|
||||||
## 验证
|
|
||||||
- `php -l` 检查全部改动的 PHP 文件。
|
|
||||||
- `node --test tests/widget-system/*.test.cjs` 确认不破坏既有契约(无该面板专属测试)。
|
|
||||||
- 桌面 + 移动宽度下目视核对:卡片 3 列/单列、关注按钮初始状态、子切换、空状态、分页。
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
## 目标
|
|
||||||
将 `inc/core-hooks/` 下的三个分片收口为唯一实现文件 `inc/core-hooks.php`,不保留兼容 loader、旧分片 stub 或双路加载;同时清理有充分证据确认无效的代码,不做真实浏览器/视觉效果验收。
|
|
||||||
|
|
||||||
## 实施步骤
|
|
||||||
1. **合并实现文件**
|
|
||||||
- 将 `inc/core-hooks/part-1.php`、`part-2.php`、`part-3.php` 的有效实现按现有加载顺序拼接到 `inc/core-hooks.php`。
|
|
||||||
- 保留现有 `defined('ABSPATH') || exit;` 保护和原有 hook 注册顺序,避免改变 WordPress 回调优先级、字符串回调和匿名闭包的行为。
|
|
||||||
- 在合并文件中保留评论过滤、SMTP/邮件、分类 term meta、资源归档/搜索、Enlighter 白名单、后台互动列、Gutenberg、付费内容等仍被运行时代码或测试覆盖的实现。
|
|
||||||
- 去除原文件的 “Shell loader / split into sub-files” 说明和三个 `require_once`,将文件头改为描述单一核心 hooks 文件的注释,避免留下已不存在的拆分结构说明。
|
|
||||||
|
|
||||||
2. **清理明确死代码**
|
|
||||||
- 删除 `part-2.php` 中注册 `wp_head` 调用未定义 `aksrt_print_win_var()` 的 legacy runtime bridge;仓库内只有该调用,没有定义、生产者或测试,且 `function_exists()` 使其当前为空操作。
|
|
||||||
- 删除 `part-3.php` 中原样返回模板的 `single_template` no-op 过滤器;它不改变模板选择,且没有其他实现依赖。
|
|
||||||
- 删除随之失去运行时意义的纯注释占位块(评论提交说明),不触碰实际由 `Moderation` 等模块实现的评论校验。
|
|
||||||
- 不删除后台互动列,尽管它读取旧 meta key;这是可见后台行为,且证据只支持“过时/可能失配”,不足以在本次合并中安全移除或重写。也不删除分类旧字段、资源路由、块注册等看似重复但仍有明确消费者的代码。
|
|
||||||
|
|
||||||
3. **删除旧分片**
|
|
||||||
- 删除 `inc/core-hooks/part-1.php`、`inc/core-hooks/part-2.php`、`inc/core-hooks/part-3.php`,使 `inc/core-hooks.php` 成为唯一实现,不做迁移层或双路兼容。
|
|
||||||
|
|
||||||
4. **同步测试引用**
|
|
||||||
- 将以下契约测试中对分片的直接读取改为 `inc/core-hooks.php`:
|
|
||||||
- `tests/widget-system/email-system-contract.test.cjs`
|
|
||||||
- `tests/widget-system/pay-card-contract.test.cjs`
|
|
||||||
- `tests/widget-system/post-visual-contract.test.cjs`
|
|
||||||
- 不改测试断言语义,仅修正因文件收口导致的路径。
|
|
||||||
|
|
||||||
5. **验证**
|
|
||||||
- 对新的 `inc/core-hooks.php` 运行 `php -l`。
|
|
||||||
- 运行上述三个 Node 契约测试,并运行 `tests/widget-system/run.php` 作为已有回归合同检查(若环境依赖导致失败,如实报告)。
|
|
||||||
- 用 `rg` 检查生产代码和测试中不再残留 `core-hooks/part-*.php` 路径;排除 `.git`、`vendor`、`node_modules` 和历史 `.zcode/plans` 文件。
|
|
||||||
- 不启动浏览器、不执行真实视觉效果验收,符合请求约束。
|
|
||||||
|
|
||||||
## 工作区安全边界
|
|
||||||
当前未提交修改仅涉及 `assets/css/build/*`、`assets/js/runtime/*` 和 `.zcode/plans/*`,不涉及本次目标文件;实施时只修改上述 core hooks 文件及三个测试文件,不覆盖或清理现有其他改动。
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# assets/vendor 全量迁移 + 全仓去 zibll 化
|
|
||||||
|
|
||||||
## 目标布局(类型归位,`git mv` 保留历史)
|
|
||||||
|
|
||||||
| 源 | 目标 | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| `assets/vendor/fontawesome5/` | `assets/fontawesome5/` | css/ + webfonts/ 原结构,`../webfonts` 相对引用不变 |
|
|
||||||
| `assets/vendor/zibll/css/`(22 文件) | `assets/css/` 顶层 | bootstrap/animate/swiper/main 等,无命名冲突;`main(.min).css` 的 `url(../img/playing.svg)` 迁后恰好仍指向 `assets/img`,无需改 |
|
|
||||||
| `assets/vendor/zibll/js/**` | `assets/js/runtime/**` | **必须子目录**:assets/js 已有 14 个同名自有文件(author/sign-register/page-navs…)和一个 enlighter/ 目录 |
|
|
||||||
| `assets/vendor/zibll/img/captcha/` | `assets/img/captcha/` | slidercaptcha.js 运行时按 `_win.uri + '/img/captcha/'` 拼接,新 uri=assets 根后自然命中 |
|
|
||||||
| `assets/vendor/zibll/img/{medal,smilies}` | 与 `assets/img/` 哈希比对 | 相同跳过、缺失补齐;**内容不同时以当前实际渲染源为准**(medal/smilies 保现有,user_t.jpg/slider-bg.jpg 以 vendor 版覆盖,因 PHP 默认值当前渲染的是 vendor 版) |
|
|
||||||
| `assets/vendor/zibll/img/playing.svg` 等散图 | `assets/img/` | |
|
|
||||||
| `assets/vendor/zibll/fonts/`(孤儿 2 文件) | `assets/fonts/` | 按"全部迁移"保留 |
|
|
||||||
| `js/libs/{DPlayer,dash.all,flv,hls,vanilla-tilt}.min.js` | **不迁移**(智能去重) | 先哈希验证与 `assets/widgets/third-party/` 字节一致(探索已确认大小一致);loader.js paths 改指 `'../widgets/third-party/DPlayer.min'` 等(相对 baseUrl);若任一哈希不符则该文件照常迁入 `runtime/libs/` |
|
|
||||||
|
|
||||||
完成后删除 `assets/vendor/`。
|
|
||||||
|
|
||||||
## 运行时切换(核心链路 `_win.uri`)
|
|
||||||
|
|
||||||
PHP 将 `'uri'` 从 `…/assets/vendor/zibll` 改为 `…/assets`,loader(改造版 RequireJS)以 `baseUrl = _win.uri + '/js'` 解析懒加载模块:
|
|
||||||
|
|
||||||
1. `assets/js/runtime/loader.js`:`baseUrl` 改为 `_win.uri + '/js/runtime'`(paths 表 `'libs/swiper.min'`、`'enlighter/enlighterjs.min'` 等相对值全部随之命中,无需逐条改,除 5 个去重库改 `../widgets/third-party/`)
|
|
||||||
2. `main.js` + `main.min.js`:enlighter CSS 注入 `_win.uri + '/js/enlighter/…'` → `'/js/runtime/enlighter/…'`(swiper CSS 注入 `/css/swiper.min.css` 迁后自然命中,不改)
|
|
||||||
3. 已核实的死引用(`/css/dplayer.min.css` 404、`/action/action.php`、`/yiyan/…`、paths 中 `shop—main`)保持原状不修不扩
|
|
||||||
|
|
||||||
## PHP 引用切换(7 个文件)
|
|
||||||
|
|
||||||
- `src/Core/ScriptsPart1b.php:16-17,23,38-41`、`src/Core/ScriptsPart1c.php:35-42,53`:新变量 `$runtime_uri = $base_uri . '/assets'`、`$fa5_uri = $base_uri . '/assets/fontawesome5'`;jquery/bootstrap/loader/main 路径改 `assets/js/runtime/…`
|
|
||||||
- `inc/core-hooks/part-3.php:52`、`src/Admin/Admin.php:116`:FA5 新路径
|
|
||||||
- `src/Admin/PageConfigMetaBox.php:275,277`、`src/Settings/DisplayPageGroups.php:96,98`、`src/Admin/Schema/SchemaPageDisplay.php:77,123,138,163,190`:`assets/vendor/zibll/img/` → `assets/img/`
|
|
||||||
|
|
||||||
## 全仓去 zibll 清扫(用户已确认含功能性标识符)
|
|
||||||
|
|
||||||
先跑全仓盘点(排除 .git、composer vendor、node_modules、tests/visual、assets/admin/build):`zibll/Zibll、zib__、zib_ajax、zibllblock、wp-block-zibllblock、.zib-、_zb、Qinver、zibll.com、子比`,产出替换映射并核对目标名无既有冲突(如 `aksrt_ajax`)。统一替换规则:
|
|
||||||
|
|
||||||
- **品牌头注释**:`@Author: Qinver / @Url: zibll.com / @Project: Zibll子比主题` → `@package AKSRT`;swiper 内 `//zibll : 修改内容` 等注释中性化;`main.js:4866` 的 `console.log(' Zibll Theme …')` 及 min 等价物删除
|
|
||||||
- **功能性标识符重命名**(生产端+消费端原子同步,含 min 文件 sed,改后 diff 抽查只允许替换点变化):
|
|
||||||
- `zibllblock/*` → `aksrtblock/*`;`zibll_block_cat/var` → `aksrt_block_cat/var`;`.wp-block-zibllblock-*` → `.wp-block-aksrtblock-*`(⚠️ 数据库已发布内容中的旧块名将无法渲染,用户已接受且明确不写兼容层)
|
|
||||||
- `'zibll' + type` localStorage 键 → `'aksrt' + type`(游客点赞记录重置,已接受)
|
|
||||||
- `zib__()` i18n 函数 → `aksrt__()`(先定位定义处,全量替换所有调用,含各 .min.js)
|
|
||||||
- `zib_ajax` 等运行时函数按盘点结果同步改名
|
|
||||||
- **AKSRT 自有资产**:`assets/css/frontend/zibll-utilities.css → legacy-utilities.css`、`zibll-compat-vars.css → compat-vars.css`、`footer/zibll-shell.css → footer/shell.css`(含 enqueue_module 调用、handle 派生、测试引用);`.zib-widget`(19处)/`.zib-slider` → `.aksrt-widget`/`.aksrt-slider`(CSS + PHP 输出标记 + vendor js 选择器同步)
|
|
||||||
- **PHP 变量/注释**:`$zibll_uri` 等改名;所触及文件注释中 Zibll 措辞改写为"legacy runtime"
|
|
||||||
- **admin SPA**:Vue 源内用户可见 zibll 文案改写后 `cd assets/admin && npm run build`
|
|
||||||
- **例外保留**:`tests/visual/`(docker-compose.zibll.yml、zibll-Caddyfile、baseline)指向外部真实 Zibll 对照主题,属产品名引用,不改
|
|
||||||
|
|
||||||
## 测试同步
|
|
||||||
|
|
||||||
- `tests/navigation/zibll-navigation-contract.test.cjs`(本地未跟踪):所有 vendor 路径 → 新路径;walk 根改 `assets/js/runtime`(js)与 `assets/css` 顶层(css);新增断言 `assets/vendor` 目录不存在;文件更名 `legacy-navigation-contract.test.cjs`
|
|
||||||
- `tests/widget-system/{post-visual,toast,page-display,lazyload}-contract.test.cjs`:loader/main 读取路径更新;toast 的 `$zibll_loader_version`/`$zibll_runtime_version` 正则同步新变量名;lazyload `sourceFiles` skip 列表把 `'vendor'` 换成/加上 `'runtime'` 保持语义
|
|
||||||
- 其他 widget-system 测试中引用 zibll 路径/字样的断言同步更新
|
|
||||||
|
|
||||||
## 验证与收尾
|
|
||||||
|
|
||||||
1. `php -l` 逐个检查所有改动 PHP
|
|
||||||
2. `node --test tests/widget-system/post-visual-contract.test.cjs` 等全部受影响 contract 测试 + 更名后的 navigation 测试
|
|
||||||
3. grep 审计:全仓(按上述排除项)`zibll|Qinver|子比|zib__` 零残留;`assets/vendor` 零引用
|
|
||||||
4. 手工 QA 清单:首页/文章页/评论区、FA 图标、滑块验证码图、勋章、enlighter 高亮、DPlayer 视频播放
|
|
||||||
5. 不主动提交;建议单原子 commit:`refactor(assets): vendor 资产迁入标准目录并全仓去除 zibll 标识`(移动+切换+清扫+测试必须同 commit 才能保持可用)
|
|
||||||
|
|
||||||
## 风险提示(用户已确认接受)
|
|
||||||
|
|
||||||
- 数据库中已存 `zibllblock/*` 块的旧文章渲染损坏(无兼容层)
|
|
||||||
- 游客 localStorage 点赞/收藏记录重置
|
|
||||||
- min 文件 sed 属精细操作,逐文件 diff 验证
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
1. 余额页改为单一路径 DOM 记录切换
|
|
||||||
- 以 `inc/helpers/user-center-renderers/profile-credits.php` 现有 `data-aksrt-asset-tab` / `data-aksrt-asset-pane` 结构为唯一 UI 状态源,保持积分记录、余额记录、积分任务都通过按钮事件切换,不再依赖 URL hash 或 query。
|
|
||||||
- 在 `assets/js/credits.js` 扩展现有 `initAssetTabs()`:拦截记录分页链接,阻止整页导航;读取分页链接的页码/资产类型,调用当前已有的 `/user-center/panel/credits` REST 面板接口,通过响应 HTML 更新 `#panel-credits` 的 DOM;更新后重新绑定 tab,并清理地址栏中 `wallet_page`、`credits_page`、`points_page` 等历史参数。
|
|
||||||
- 将 PHP 记录渲染改为读取由 REST 请求传入的分页参数,而不是直接读取 `$_GET`;移除 `aksrt_render_profile_pagination()` 生成的 query 链接,改为输出带 `data-aksrt-asset-page`、`data-aksrt-asset-key` 的按钮/链接,供 `credits.js` 处理。
|
|
||||||
- 保留服务端 REST 面板的单一 HTML 输出,不新增 JSON/HTML 双协议;分页请求只使用现有 credits panel endpoint,并在响应后触发既有 `post_ajax.ed → auto_fun` 生命周期。
|
|
||||||
- 用户中心初始化时不再把 `reward_tab`、`commission_page`、`user_page` 等记录参数写回用户地址;对 `/user/balance/` 初次加载清理历史记录参数,避免切换后地址继续携带旧参数。
|
|
||||||
|
|
||||||
2. 首页排序固定为 DOM/AJAX,不保留参数刷新分支
|
|
||||||
- `assets/js/homepage-fixed.js` 删除 `SORT_QUERY_KEY`、`getSortFromUrl()`、`normalizeHomepageUrl()`、`replaceUrlSort()` 及对 `window.location.search` 的依赖;当前排序只从活动排序控件或 `aksrtData.homepageDefaultSort` 读取。
|
|
||||||
- `initSortbar()` 点击排序项后只更新 active 状态并调用现有 `requestHomepagePanel()`,不再调用 `history.replaceState()`,因此地址栏保持无 `home_sort` 参数。
|
|
||||||
- `inc/helpers/aksrt-homepage.php` 排序控件改为无参数链接(或按钮化标记,沿用现有 `data-sort`),删除 `add_query_arg('home_sort', ...)`、`data-sort-ajax` 双路开关和服务端 query override;服务端首页排序只使用 canonical 默认/运行时 override,避免生成旧的参数刷新 URL。
|
|
||||||
- 清理 `src/Core/ScriptsPart1c.php`、`src/Settings/HomeCms.php`、`src/Settings/HomepageReader.php` 中仅服务于可选排序 AJAX 开关的 `homepageSortAjax` / `homepage_sort_ajax_enabled` 死代码及数据注入。
|
|
||||||
- 删除首页排序请求中仅为 URL 刷新服务的 `page_url` 参数,或将其固定为当前无 query 的页面路径;保留 REST 请求内部用于确定上下文的必要字段。
|
|
||||||
|
|
||||||
3. 清理历史 query 分支和测试
|
|
||||||
- 通过 `rg` 找到并删除余额记录的 `wallet_page` / `credits_page` 旧 `$_GET` 分支、首页 `home_sort` query 读取/写入分支,以及相关旧注释/配置字段。
|
|
||||||
- 增加/更新 `tests/widget-system` 契约:余额记录控件必须是 DOM data 事件、分页不得生成 `add_query_arg`/query href;首页排序必须调用 `requestHomepagePanel` 且不得使用 `home_sort`、`replaceState`、`location.search`、`data-sort-ajax`。
|
|
||||||
- 只保留真正需要的 `user-center` REST 参数(例如 revenue 内部分页若仍有明确业务需求则保留在请求体/URL 内部,不写浏览器地址);不把现有其他用户中心 tab 的必要内部请求参数误删。
|
|
||||||
|
|
||||||
4. 验证
|
|
||||||
- 仅使用 `rg`、Node 契约测试和 PHP 语法检查,不做真实视觉效果验收。
|
|
||||||
- 运行余额/用户中心、首页、生命周期、资产账本相关测试;对所有改动 PHP/JS 执行 `php -l` / `node --check`。
|
|
||||||
- 运行 `git diff --check`,并确认不覆盖工作树中已有的 VIP、runtime、CSS、imgbox 或计划文件改动。
|
|
||||||
@@ -2,15 +2,15 @@
|
|||||||
/**
|
/**
|
||||||
* 404 template.
|
* 404 template.
|
||||||
*
|
*
|
||||||
* @package AKSRT
|
* @package Jio
|
||||||
*/
|
*/
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
get_header();
|
get_header();
|
||||||
|
|
||||||
$search_placeholder = function_exists('aksrt_get_search_placeholder') ? aksrt_get_search_placeholder() : __('开启精彩搜索', 'aksrt');
|
$search_placeholder = function_exists('jio_get_search_placeholder') ? jio_get_search_placeholder() : __('开启精彩搜索', 'jio');
|
||||||
$hot_keywords = function_exists('aksrt_get_search_hot_keywords') ? aksrt_get_search_hot_keywords() : [];
|
$hot_keywords = function_exists('jio_get_search_hot_keywords') ? jio_get_search_hot_keywords() : [];
|
||||||
|
|
||||||
$hot_query = new WP_Query([
|
$hot_query = new WP_Query([
|
||||||
'post_type' => 'post',
|
'post_type' => 'post',
|
||||||
@@ -39,33 +39,33 @@ $top_cats = get_terms([
|
|||||||
<i class="fa fas fa-compass"></i>
|
<i class="fa fas fa-compass"></i>
|
||||||
</p>
|
</p>
|
||||||
<p class="error-hero__code" id="error-title">404</p>
|
<p class="error-hero__code" id="error-title">404</p>
|
||||||
<h1 class="error-hero__title"><?php esc_html_e('页面未找到', 'aksrt'); ?></h1>
|
<h1 class="error-hero__title"><?php esc_html_e('页面未找到', 'jio'); ?></h1>
|
||||||
<p class="error-hero__desc">
|
<p class="error-hero__desc">
|
||||||
<?php esc_html_e('你访问的页面不存在或已被删除。试试搜索,或者从下方常用入口继续浏览。', 'aksrt'); ?>
|
<?php esc_html_e('你访问的页面不存在或已被删除。试试搜索,或者从下方常用入口继续浏览。', 'jio'); ?>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<?php if (function_exists('aksrt_render_admin_quick_link')) { aksrt_render_admin_quick_link('page-display/other', __('404 页设置', 'aksrt')); } ?>
|
<?php if (function_exists('jio_render_admin_quick_link')) { jio_render_admin_quick_link('page-display/other', __('404 页设置', 'jio')); } ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form role="search" method="get" class="error-hero__search" action="<?php echo esc_url(home_url('/')); ?>">
|
<form role="search" method="get" class="error-hero__search" action="<?php echo esc_url(home_url('/')); ?>">
|
||||||
<span class="error-hero__search-icon" aria-hidden="true">
|
<span class="error-hero__search-icon" aria-hidden="true">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||||
</span>
|
</span>
|
||||||
<label class="screen-reader-text" for="error-search-input"><?php esc_html_e('搜索', 'aksrt'); ?></label>
|
<label class="screen-reader-text" for="error-search-input"><?php esc_html_e('搜索', 'jio'); ?></label>
|
||||||
<input type="search" id="error-search-input" name="s" placeholder="<?php echo esc_attr($search_placeholder); ?>" autocomplete="off">
|
<input type="search" id="error-search-input" name="s" placeholder="<?php echo esc_attr($search_placeholder); ?>" autocomplete="off">
|
||||||
<button type="submit" class="btn-primary btn-sm"><?php esc_html_e('搜索', 'aksrt'); ?></button>
|
<button type="submit" class="btn-primary btn-sm"><?php esc_html_e('搜索', 'jio'); ?></button>
|
||||||
</form>
|
</form>
|
||||||
<?php aksrt_render_search_hot_keywords($hot_keywords, 'aksrt-search-hot-keywords'); ?>
|
<?php jio_render_search_hot_keywords($hot_keywords, 'jio-search-hot-keywords'); ?>
|
||||||
|
|
||||||
<div class="error-hero__actions">
|
<div class="error-hero__actions">
|
||||||
<a href="<?php echo esc_url(home_url('/')); ?>" class="btn-primary"><?php esc_html_e('返回首页', 'aksrt'); ?></a>
|
<a href="<?php echo esc_url(home_url('/')); ?>" class="btn-primary"><?php esc_html_e('返回首页', 'jio'); ?></a>
|
||||||
<a href="javascript:history.back()" class="btn-outline"><?php esc_html_e('返回上一页', 'aksrt'); ?></a>
|
<a href="javascript:history.back()" class="btn-outline"><?php esc_html_e('返回上一页', 'jio'); ?></a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<?php if (!empty($top_cats) && !is_wp_error($top_cats)) : ?>
|
<?php if (!empty($top_cats) && !is_wp_error($top_cats)) : ?>
|
||||||
<section class="error-section" aria-label="<?php esc_attr_e('热门分类', 'aksrt'); ?>">
|
<section class="error-section" aria-label="<?php esc_attr_e('热门分类', 'jio'); ?>">
|
||||||
<h2 class="error-section__title"><?php esc_html_e('热门分类', 'aksrt'); ?></h2>
|
<h2 class="error-section__title"><?php esc_html_e('热门分类', 'jio'); ?></h2>
|
||||||
<div class="error-section__chips">
|
<div class="error-section__chips">
|
||||||
<?php foreach ($top_cats as $cat) : ?>
|
<?php foreach ($top_cats as $cat) : ?>
|
||||||
<a class="error-section__chip" href="<?php echo esc_url(get_category_link($cat->term_id)); ?>">
|
<a class="error-section__chip" href="<?php echo esc_url(get_category_link($cat->term_id)); ?>">
|
||||||
@@ -78,8 +78,8 @@ $top_cats = get_terms([
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php if ($hot_query->have_posts()) : ?>
|
<?php if ($hot_query->have_posts()) : ?>
|
||||||
<section class="error-section" aria-label="<?php esc_attr_e('热门内容', 'aksrt'); ?>">
|
<section class="error-section" aria-label="<?php esc_attr_e('热门内容', 'jio'); ?>">
|
||||||
<h2 class="error-section__title"><?php esc_html_e('热门内容', 'aksrt'); ?></h2>
|
<h2 class="error-section__title"><?php esc_html_e('热门内容', 'jio'); ?></h2>
|
||||||
<ul class="error-section__list">
|
<ul class="error-section__list">
|
||||||
<?php while ($hot_query->have_posts()) : $hot_query->the_post(); ?>
|
<?php while ($hot_query->have_posts()) : $hot_query->the_post(); ?>
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# AKSRT WordPress Theme
|
# Jio WordPress Theme
|
||||||
|
|
||||||
> **版本**: 3.4.0-dev | **架构**: 原生 PHP + WordPress (非 headless)
|
> **版本**: 3.4.0-dev | **架构**: 原生 PHP + WordPress (非 headless)
|
||||||
> **要求**: WordPress >= 6.4 | PHP >= 8.1
|
> **要求**: WordPress >= 6.4 | PHP >= 8.1
|
||||||
@@ -7,17 +7,14 @@
|
|||||||
|
|
||||||
## 项目概述
|
## 项目概述
|
||||||
|
|
||||||
AKSRT 是一款现代化的 WordPress 主题,采用**原生 WordPress 主题架构**开发(PHP + HTML + CSS + JavaScript),不依赖 REST API 进行前端渲染。
|
Jio 是一款现代化的 WordPress 主题,采用**原生 WordPress 主题架构**开发(PHP + HTML + CSS + JavaScript),不依赖 REST API 进行前端渲染。
|
||||||
|
|
||||||
### 核心特性
|
### 核心特性
|
||||||
|
|
||||||
- **自定义管理面板 (SPA)** — Vue3 + Vite + Naive UI 的 12 个一级业务域后台
|
- **自定义管理面板 (SPA)** — Vue3 + Vite + Naive UI 的 12 个一级业务域后台
|
||||||
- **统一设置契约** — Schema 驱动表单、域控制器、导入导出、按域重置、迁移状态与健康检查
|
- **统一设置契约** — Schema 驱动表单、域控制器、导入导出
|
||||||
- **支付/下载主线** — 下载/付费内容中心、充值/VIP/分成/返佣/网关结构、10 分钟下载 token
|
|
||||||
- **用户与增长** — 统一登录注册、OAuth 登录、签到奖励、邀请奖励、等级/徽章/认证/举报结构
|
- **用户与增长** — 统一登录注册、OAuth 登录、签到奖励、邀请奖励、等级/徽章/认证/举报结构
|
||||||
- **内容与交互** — 首页模块化、归档/搜索/404、单篇资源页、评论审核与通知中心
|
- **内容与交互** — 首页模块化、归档/搜索/404、单篇资源页、评论审核与通知
|
||||||
- **运维工具** — 配置导出导入、dry-run 校验、备份、数据库与环境诊断
|
|
||||||
- **自定义页面模板**:About / Friends / Login / Profile / Register
|
|
||||||
- **响应式设计**
|
- **响应式设计**
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -25,14 +22,14 @@ AKSRT 是一款现代化的 WordPress 主题,采用**原生 WordPress 主题
|
|||||||
## 文件结构
|
## 文件结构
|
||||||
|
|
||||||
```
|
```
|
||||||
AKSRT-theme/
|
Jio/
|
||||||
├── functions.php # 入口:常量 + Composer autoload + bootstrap + 单例 init
|
├── functions.php # 入口:常量 + Composer autoload + bootstrap + 单例 init
|
||||||
├── style.css # WordPress 主题声明头
|
├── style.css # WordPress 主题声明头
|
||||||
├── composer.json # 第三方依赖 + 主题 PSR-4(AKSRT\ → src/)
|
├── composer.json # 第三方依赖 + 主题 PSR-4(Jio\ → src/)
|
||||||
│
|
│
|
||||||
├── *.php # WP 核心模板保留根目录
|
├── *.php # WP 核心模板保留根目录
|
||||||
│
|
│
|
||||||
├── src/ # Composer PSR-4 类型根(namespace AKSRT\)
|
├── src/ # Composer PSR-4 类型根(namespace Jio\)
|
||||||
│ ├── Admin/ # SPA 编排 + 域控制器 + Schema + Native ops
|
│ ├── Admin/ # SPA 编排 + 域控制器 + Schema + Native ops
|
||||||
│ ├── Auth/ # 登录 / OAuth / 审核 / 邮箱
|
│ ├── Auth/ # 登录 / OAuth / 审核 / 邮箱
|
||||||
│ ├── Content/ # 下载 token / 付费内容 / VIP / 可见性
|
│ ├── Content/ # 下载 token / 付费内容 / VIP / 可见性
|
||||||
@@ -41,7 +38,7 @@ AKSRT-theme/
|
|||||||
│ ├── Extensions/ # MeiliSearch / 推送 / IP / 内容审核
|
│ ├── Extensions/ # MeiliSearch / 推送 / IP / 内容审核
|
||||||
│ ├── Frontend/ # 前端渲染辅助
|
│ ├── Frontend/ # 前端渲染辅助
|
||||||
│ ├── Messages/ # 站内信 / 公告 / 举报
|
│ ├── Messages/ # 站内信 / 公告 / 举报
|
||||||
│ ├── Payment/ # 订单 / 网关 / 售后 / 物流 / 分成
|
│ ├── Payment/ # 订单 / 网关 / 物流 / 分成
|
||||||
│ │ └── Gateways/ # 支付宝 / 微信 / PayPal / Stripe
|
│ │ └── Gateways/ # 支付宝 / 微信 / PayPal / Stripe
|
||||||
│ ├── Settings/ # 设置门面 + 运行时 delegates
|
│ ├── Settings/ # 设置门面 + 运行时 delegates
|
||||||
│ ├── Shop/ # 商城服务 + meta box
|
│ ├── Shop/ # 商城服务 + meta box
|
||||||
@@ -92,7 +89,7 @@ AKSRT-theme/
|
|||||||
|
|
||||||
## 安装方式
|
## 安装方式
|
||||||
|
|
||||||
主题类型(`AKSRT\…`)通过 Composer PSR-4 从 `src/` 自动加载;`vendor/` 不进 Git。**部署或更新主题后必须在主题根目录生成 autoload**,否则会出现 `Class "AKSRT\Core\DomainModel" not found` 等致命错误。
|
主题类型(`Jio\…`)通过 Composer PSR-4 从 `src/` 自动加载;`vendor/` 不进 Git。**部署或更新主题后必须在主题根目录生成 autoload**,否则会出现 `Class "Jio\Core\DomainModel" not found` 等致命错误。
|
||||||
|
|
||||||
### 前置:Composer(必做)
|
### 前置:Composer(必做)
|
||||||
|
|
||||||
@@ -109,16 +106,16 @@ composer dump-autoload -o
|
|||||||
确认:
|
确认:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
php -r "require 'vendor/autoload.php'; echo class_exists('AKSRT\\\\Core\\\\DomainModel') ? \"ok\\n\" : \"missing\\n\";"
|
php -r "require 'vendor/autoload.php'; echo class_exists('Jio\\\\Core\\\\DomainModel') ? \"ok\\n\" : \"missing\\n\";"
|
||||||
```
|
```
|
||||||
|
|
||||||
应输出 `ok`。若 PHP 开启了 OPcache,重载 php-fpm / 重启 PHP 后再访问站点。
|
应输出 `ok`。若 PHP 开启了 OPcache,重载 php-fpm / 重启 PHP 后再访问站点。
|
||||||
|
|
||||||
### 方式一:直接上传(推荐)
|
### 方式一:直接上传(推荐)
|
||||||
|
|
||||||
1. 将整个 `AKSRT-theme` 文件夹上传到 `wp-content/themes/`
|
1. 将整个 `Jio` 文件夹上传到 `wp-content/themes/`
|
||||||
2. 在服务器主题目录执行上一节的 `composer install`(若打包时已带上完整 `vendor/` 且已 `dump-autoload`,可跳过)
|
2. 在服务器主题目录执行上一节的 `composer install`(若打包时已带上完整 `vendor/` 且已 `dump-autoload`,可跳过)
|
||||||
3. WordPress 后台 → **外观** → **主题** → 找到 **AKSRT** → **激活**
|
3. WordPress 后台 → **外观** → **主题** → 找到 **Jio** → **激活**
|
||||||
|
|
||||||
### 方式二:ZIP 打包上传
|
### 方式二:ZIP 打包上传
|
||||||
|
|
||||||
@@ -129,7 +126,7 @@ php -r "require 'vendor/autoload.php'; echo class_exists('AKSRT\\\\Core\\\\Domai
|
|||||||
SSH 进入主题目录后:
|
SSH 进入主题目录后:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /path/to/wp-content/themes/AKsrt-theme # 以实际路径为准
|
cd /path/to/wp-content/themes/Jio # 以实际路径为准
|
||||||
composer install --no-dev --optimize-autoloader
|
composer install --no-dev --optimize-autoloader
|
||||||
# 或仅刷新映射:composer dump-autoload -o
|
# 或仅刷新映射:composer dump-autoload -o
|
||||||
# 可选:reload php-fpm / 清 OPcache
|
# 可选:reload php-fpm / 清 OPcache
|
||||||
@@ -153,9 +150,9 @@ composer install --no-dev --optimize-autoloader
|
|||||||
|
|
||||||
### 2. 后台配置路径
|
### 2. 后台配置路径
|
||||||
|
|
||||||
WordPress 后台左侧 **Admin Bar** → ⚡ **AKSRT 主题设置**
|
WordPress 后台左侧 **Admin Bar** → ⚡ **Jio 主题设置**
|
||||||
|
|
||||||
或直接访问:`/wp-admin/admin.php?page=aksrt-settings`
|
或直接访问:`/wp-admin/admin.php?page=jio-settings`
|
||||||
|
|
||||||
### 3. 当前后台域
|
### 3. 当前后台域
|
||||||
|
|
||||||
@@ -170,13 +167,13 @@ WordPress 后台左侧 **Admin Bar** → ⚡ **AKSRT 主题设置**
|
|||||||
### 4. 推广、邀请与佣金(用户中心 → 收益)
|
### 4. 推广、邀请与佣金(用户中心 → 收益)
|
||||||
|
|
||||||
- **推广链接**:用户中心「收益」页展示带 `?ref=` 的推广链接、二维码(可下载 PNG)与推广海报(按「推广返佣」设置中的海报文案生成)。归因模式(首触/末触)在「支付&付费 → 推广返佣 → 推广归因模式」配置。
|
- **推广链接**:用户中心「收益」页展示带 `?ref=` 的推广链接、二维码(可下载 PNG)与推广海报(按「推广返佣」设置中的海报文案生成)。归因模式(首触/末触)在「支付&付费 → 推广返佣 → 推广归因模式」配置。
|
||||||
- **一次性邀请码**:后台「用户 → 邀请码管理」三视图运营页——列表(状态筛选/搜索/批量删除/排序/分页/点击复制)、添加(系统自动生成:数量/标识/四类奖励[经验值、积分、余额、VIP]/高级选项自定义位数;或 Zibll 兼容文本导入:一行一码 `邀请码 奖励 标识`,奖励参数串 `points=20&balance=30&vip=1&vip_time=5&level_integral=100`,分隔符可自定义)、导出(状态筛选 + 文本文档[可回流导入]/Excel表格 双格式)。注册门控模式在「用户&互动 → 登录注册 → 邀请码注册」。消费为原子操作,并发注册不会超发;开启注册验证时奖励延迟到邮箱验证后发放。
|
- **一次性邀请码**:后台「用户 → 邀请码管理」三视图运营页——列表(状态筛选/搜索/批量删除/排序/分页/点击复制)、添加(系统自动生成:数量/标识/四类奖励[经验值、积分、余额、VIP]/高级选项自定义位数;或行格式文本导入:一行一码 `邀请码 奖励 标识`,奖励参数串 `points=20&balance=30&vip=1&vip_time=5&level_integral=100`,分隔符可自定义)、导出(状态筛选 + 文本文档[可回流导入]/Excel表格 双格式)。注册门控模式在「用户&互动 → 登录注册 → 邀请码注册」。消费为原子操作,并发注册不会超发;开启注册验证时奖励延迟到邮箱验证后发放。
|
||||||
- **佣金**:订单支付成功后按推广者等级分档比例记录到佣金账本,经「结算观察期」后自动结算入钱包,支持提现与退款联动回滚。「推广码禁用后停止返佣」开关默认关闭。
|
- **佣金**:订单支付成功后按推广者等级分档比例记录到佣金账本,经「结算观察期」后自动结算入钱包,支持提现与退款联动回滚。「推广码禁用后停止返佣」开关默认关闭。
|
||||||
|
|
||||||
### 5. 部署注意事项
|
### 5. 部署注意事项
|
||||||
|
|
||||||
- **佣金结算时效**:结算依赖小时级 wp-cron,并有收益页访问触发的懒结算兜底。高流量站点建议禁用 `WP_CRON_LOCK` 场景下的默认 WP-Cron,改用系统 crontab 定时请求 `wp-cron.php`,保证结算与过期任务准时执行。
|
- **佣金结算时效**:结算依赖小时级 wp-cron,并有收益页访问触发的懒结算兜底。高流量站点建议禁用 `WP_CRON_LOCK` 场景下的默认 WP-Cron,改用系统 crontab 定时请求 `wp-cron.php`,保证结算与过期任务准时执行。
|
||||||
- **反代 / CDN 与 IP 风控**:主题默认按 Cloudflare → 通用代理头顺序识别真实 IP。若需收紧伪造风险,添加 `add_filter('aksrt_trusted_proxies', fn() => ['10.0.0.0/8', '173.245.48.0/20'])`(填入你的代理/CDN 网段);名单非空时仅当 `REMOTE_ADDR` 命中名单才采纳转发头,否则直接使用 `REMOTE_ADDR`(推广奖励的 IP 日上限、验证码等均依赖该函数)。
|
- **反代 / CDN 与 IP 风控**:主题默认按 Cloudflare → 通用代理头顺序识别真实 IP。若需收紧伪造风险,添加 `add_filter('jio_trusted_proxies', fn() => ['10.0.0.0/8', '173.245.48.0/20'])`(填入你的代理/CDN 网段);名单非空时仅当 `REMOTE_ADDR` 命中名单才采纳转发头,否则直接使用 `REMOTE_ADDR`(推广奖励的 IP 日上限、验证码等均依赖该函数)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -184,20 +181,20 @@ WordPress 后台左侧 **Admin Bar** → ⚡ **AKSRT 主题设置**
|
|||||||
|
|
||||||
### Settings Facade 分层
|
### Settings Facade 分层
|
||||||
|
|
||||||
- `AKSRT_Settings` 仍是主题设置的**唯一静态 facade**,外部调用方式保持 `AKSRT_Settings::*`
|
- `JIO_Settings` 仍是主题设置的**唯一静态 facade**,外部调用方式保持 `JIO_Settings::*`
|
||||||
- 具体运行时 getter 已按领域拆分到 `inc/classes/settings/`,例如:
|
- 具体运行时 getter 已按领域拆分到 `inc/classes/settings/`,例如:
|
||||||
- `class-aksrt-settings-theme-search.php`
|
- `class-jio-settings-theme-search.php`
|
||||||
- `class-aksrt-settings-post-display.php`
|
- `class-jio-settings-post-display.php`
|
||||||
- `class-aksrt-settings-home-cms.php`
|
- `class-jio-settings-home-cms.php`
|
||||||
- `class-aksrt-settings-user-runtime.php`
|
- `class-jio-settings-user-runtime.php`
|
||||||
- `class-aksrt-settings-integration.php`
|
- `class-jio-settings-integration.php`
|
||||||
- `class-aksrt-settings-display-runtime.php`
|
- `class-jio-settings-display-runtime.php`
|
||||||
- `class-aksrt-settings-extension-runtime.php`
|
- `class-jio-settings-extension-runtime.php`
|
||||||
- `class-aksrt-settings-credits-runtime.php`
|
- `class-jio-settings-credits-runtime.php`
|
||||||
- `class-aksrt-settings-category-runtime.php`
|
- `class-jio-settings-category-runtime.php`
|
||||||
- `class-aksrt-settings-capabilities.php`
|
- `class-jio-settings-capabilities.php`
|
||||||
- `inc/bootstrap.php` 会先加载 `settings/class-aksrt-settings.php`,再按领域加载这些 delegate 类
|
- `inc/bootstrap.php` 会先加载 `settings/class-jio-settings.php`,再按领域加载这些 delegate 类
|
||||||
- 新增设置运行时逻辑时,优先放入对应 settings 子类;仅把聚合输出、统一入口和跨域装配保留在 `AKSRT_Settings`
|
- 新增设置运行时逻辑时,优先放入对应 settings 子类;仅把聚合输出、统一入口和跨域装配保留在 `JIO_Settings`
|
||||||
|
|
||||||
### 添加新辅助函数
|
### 添加新辅助函数
|
||||||
|
|
||||||
@@ -205,14 +202,14 @@ WordPress 后台左侧 **Admin Bar** → ⚡ **AKSRT 主题设置**
|
|||||||
|
|
||||||
### 添加新页面模板
|
### 添加新页面模板
|
||||||
|
|
||||||
将 `page-{name}.php` 放入 `templates/pages/`,并在 `class-aksrt-templates.php` 中注册。
|
将 `page-{name}.php` 放入 `templates/pages/`,并在 `class-jio-templates.php` 中注册。
|
||||||
|
|
||||||
### CSS 组织规范
|
### CSS 组织规范
|
||||||
|
|
||||||
| 目录 | 用途 | 加载场景 |
|
| 目录 | 用途 | 加载场景 |
|
||||||
|------|------|---------|
|
|------|------|---------|
|
||||||
| `css/frontend/` | 前端公共样式 | 所有前台页面 |
|
| `css/frontend/` | 前端公共样式 | 所有前台页面 |
|
||||||
| `css/admin/` | 后台管理样式 | 仅 AKSRT 设置页面 |
|
| `css/admin/` | 后台管理样式 | 仅 Jio 设置页面 |
|
||||||
| `css/shared/` | 共用组件样式 | 前后端按需加载(如验证码) |
|
| `css/shared/` | 共用组件样式 | 前后端按需加载(如验证码) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
+26
-26
@@ -2,9 +2,9 @@
|
|||||||
/**
|
/**
|
||||||
* Resource Archive Template
|
* Resource Archive Template
|
||||||
*
|
*
|
||||||
* Virtual list at /resources/ — ordinary posts marked with _aksrt_is_resource.
|
* Virtual list at /resources/ — ordinary posts marked with _jio_is_resource.
|
||||||
*
|
*
|
||||||
* @package AKSRT
|
* @package Jio
|
||||||
*/
|
*/
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
@@ -25,18 +25,18 @@ if (!in_array($current_price, ['all', 'free', 'paid'], true)) {
|
|||||||
$price_min = isset($_GET['price_min']) && $_GET['price_min'] !== '' ? (float) $_GET['price_min'] : '';
|
$price_min = isset($_GET['price_min']) && $_GET['price_min'] !== '' ? (float) $_GET['price_min'] : '';
|
||||||
$price_max = isset($_GET['price_max']) && $_GET['price_max'] !== '' ? (float) $_GET['price_max'] : '';
|
$price_max = isset($_GET['price_max']) && $_GET['price_max'] !== '' ? (float) $_GET['price_max'] : '';
|
||||||
|
|
||||||
$archive_url = function_exists('aksrt_resource_archive_url') ? aksrt_resource_archive_url() : home_url('/resources/');
|
$archive_url = function_exists('jio_resource_archive_url') ? jio_resource_archive_url() : home_url('/resources/');
|
||||||
|
|
||||||
$sort_options = [
|
$sort_options = [
|
||||||
'date' => __('最新', 'aksrt'),
|
'date' => __('最新', 'jio'),
|
||||||
'downloads' => __('热门下载', 'aksrt'),
|
'downloads' => __('热门下载', 'jio'),
|
||||||
'views' => __('阅读最多', 'aksrt'),
|
'views' => __('阅读最多', 'jio'),
|
||||||
];
|
];
|
||||||
|
|
||||||
$price_options = [
|
$price_options = [
|
||||||
'all' => __('全部', 'aksrt'),
|
'all' => __('全部', 'jio'),
|
||||||
'free' => __('免费', 'aksrt'),
|
'free' => __('免费', 'jio'),
|
||||||
'paid' => __('付费', 'aksrt'),
|
'paid' => __('付费', 'jio'),
|
||||||
];
|
];
|
||||||
|
|
||||||
$preserve = [];
|
$preserve = [];
|
||||||
@@ -59,18 +59,18 @@ $has_filter = $current_orderby !== 'date' || $current_price !== 'all' || $price_
|
|||||||
<main id="main-content" class="site-main container" tabindex="-1">
|
<main id="main-content" class="site-main container" tabindex="-1">
|
||||||
<header class="resource-archive-header page-cover">
|
<header class="resource-archive-header page-cover">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<?php if (function_exists('aksrt_breadcrumbs')) : ?>
|
<?php if (function_exists('jio_breadcrumbs')) : ?>
|
||||||
<nav class="resource-archive-header__breadcrumbs" aria-label="<?php esc_attr_e('面包屑', 'aksrt'); ?>">
|
<nav class="resource-archive-header__breadcrumbs" aria-label="<?php esc_attr_e('面包屑', 'jio'); ?>">
|
||||||
<?php aksrt_breadcrumbs(); ?>
|
<?php jio_breadcrumbs(); ?>
|
||||||
</nav>
|
</nav>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<h1 class="resource-archive-header__title"><?php esc_html_e('资源', 'aksrt'); ?></h1>
|
<h1 class="resource-archive-header__title"><?php esc_html_e('资源', 'jio'); ?></h1>
|
||||||
<p class="resource-archive-header__sub">
|
<p class="resource-archive-header__sub">
|
||||||
<?php
|
<?php
|
||||||
global $wp_query;
|
global $wp_query;
|
||||||
/* translators: %s: resource count */
|
/* translators: %s: resource count */
|
||||||
printf(esc_html__('共 %s 个资源', 'aksrt'), esc_html(number_format_i18n((int) $wp_query->found_posts)));
|
printf(esc_html__('共 %s 个资源', 'jio'), esc_html(number_format_i18n((int) $wp_query->found_posts)));
|
||||||
?>
|
?>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,9 +79,9 @@ $has_filter = $current_orderby !== 'date' || $current_price !== 'all' || $price_
|
|||||||
<div class="content-wrap">
|
<div class="content-wrap">
|
||||||
<div class="content-layout content-layout--no-sidebar">
|
<div class="content-layout content-layout--no-sidebar">
|
||||||
<div class="content-layout__primary resource-archive">
|
<div class="content-layout__primary resource-archive">
|
||||||
<nav class="resource-archive__toolbar" aria-label="<?php esc_attr_e('筛选', 'aksrt'); ?>">
|
<nav class="resource-archive__toolbar" aria-label="<?php esc_attr_e('筛选', 'jio'); ?>">
|
||||||
<div class="resource-archive__filter-group">
|
<div class="resource-archive__filter-group">
|
||||||
<span class="resource-archive__filter-label"><?php esc_html_e('排序', 'aksrt'); ?></span>
|
<span class="resource-archive__filter-label"><?php esc_html_e('排序', 'jio'); ?></span>
|
||||||
<ul class="resource-archive__sort">
|
<ul class="resource-archive__sort">
|
||||||
<?php foreach ($sort_options as $key => $label) :
|
<?php foreach ($sort_options as $key => $label) :
|
||||||
$args = $preserve;
|
$args = $preserve;
|
||||||
@@ -105,7 +105,7 @@ $has_filter = $current_orderby !== 'date' || $current_price !== 'all' || $price_
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="resource-archive__filter-group">
|
<div class="resource-archive__filter-group">
|
||||||
<span class="resource-archive__filter-label"><?php esc_html_e('价格', 'aksrt'); ?></span>
|
<span class="resource-archive__filter-label"><?php esc_html_e('价格', 'jio'); ?></span>
|
||||||
<ul class="resource-archive__sort">
|
<ul class="resource-archive__sort">
|
||||||
<?php foreach ($price_options as $key => $label) :
|
<?php foreach ($price_options as $key => $label) :
|
||||||
$args = $preserve;
|
$args = $preserve;
|
||||||
@@ -139,43 +139,43 @@ $has_filter = $current_orderby !== 'date' || $current_price !== 'all' || $price_
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<label class="resource-archive__price-input">
|
<label class="resource-archive__price-input">
|
||||||
<span><?php esc_html_e('最低', 'aksrt'); ?></span>
|
<span><?php esc_html_e('最低', 'jio'); ?></span>
|
||||||
<input type="number" name="price_min" min="0" step="1" value="<?php echo esc_attr((string) $price_min); ?>">
|
<input type="number" name="price_min" min="0" step="1" value="<?php echo esc_attr((string) $price_min); ?>">
|
||||||
</label>
|
</label>
|
||||||
<span class="resource-archive__price-sep">—</span>
|
<span class="resource-archive__price-sep">—</span>
|
||||||
<label class="resource-archive__price-input">
|
<label class="resource-archive__price-input">
|
||||||
<span><?php esc_html_e('最高', 'aksrt'); ?></span>
|
<span><?php esc_html_e('最高', 'jio'); ?></span>
|
||||||
<input type="number" name="price_max" min="0" step="1" value="<?php echo esc_attr((string) $price_max); ?>">
|
<input type="number" name="price_max" min="0" step="1" value="<?php echo esc_attr((string) $price_max); ?>">
|
||||||
</label>
|
</label>
|
||||||
<button type="submit" class="btn-primary btn-sm"><?php esc_html_e('应用', 'aksrt'); ?></button>
|
<button type="submit" class="btn-primary btn-sm"><?php esc_html_e('应用', 'jio'); ?></button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<?php if ($has_filter) : ?>
|
<?php if ($has_filter) : ?>
|
||||||
<a class="resource-archive__reset" href="<?php echo esc_url($archive_url); ?>">
|
<a class="resource-archive__reset" href="<?php echo esc_url($archive_url); ?>">
|
||||||
<i class="fa fas fa-times-circle" aria-hidden="true"></i>
|
<i class="fa fas fa-times-circle" aria-hidden="true"></i>
|
||||||
<?php esc_html_e('清除筛选', 'aksrt'); ?>
|
<?php esc_html_e('清除筛选', 'jio'); ?>
|
||||||
</a>
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<?php if (have_posts()) : ?>
|
<?php if (have_posts()) : ?>
|
||||||
<?php
|
<?php
|
||||||
$render_config = aksrt_resolve_post_list_render_config([
|
$render_config = jio_resolve_post_list_render_config([
|
||||||
'context' => 'archive',
|
'context' => 'archive',
|
||||||
'overrides' => ['type' => 'card'],
|
'overrides' => ['type' => 'card'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
aksrt_render_post_list(array_merge($render_config, ['wrapper_class' => 'content-stream__list']));
|
jio_render_post_list(array_merge($render_config, ['wrapper_class' => 'content-stream__list']));
|
||||||
aksrt_render_pagination(null, [
|
jio_render_pagination(null, [
|
||||||
'query_context' => 'archive',
|
'query_context' => 'archive',
|
||||||
'render_config' => $render_config,
|
'render_config' => $render_config,
|
||||||
]);
|
]);
|
||||||
?>
|
?>
|
||||||
<?php else : ?>
|
<?php else : ?>
|
||||||
<div class="no-results">
|
<div class="no-results">
|
||||||
<p><?php esc_html_e('暂无资源。', 'aksrt'); ?></p>
|
<p><?php esc_html_e('暂无资源。', 'jio'); ?></p>
|
||||||
<?php if ($has_filter) : ?>
|
<?php if ($has_filter) : ?>
|
||||||
<p><a class="btn-outline btn-sm" href="<?php echo esc_url($archive_url); ?>"><?php esc_html_e('清除筛选条件', 'aksrt'); ?></a></p>
|
<p><a class="btn-outline btn-sm" href="<?php echo esc_url($archive_url); ?>"><?php esc_html_e('清除筛选条件', 'jio'); ?></a></p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
+19
-19
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* Generic archive template.
|
* Generic archive template.
|
||||||
*
|
*
|
||||||
* @package AKSRT
|
* @package Jio
|
||||||
*/
|
*/
|
||||||
|
|
||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
@@ -11,31 +11,31 @@ get_header();
|
|||||||
|
|
||||||
global $wp_query;
|
global $wp_query;
|
||||||
|
|
||||||
$render_config = function_exists('aksrt_archive_get_render_config')
|
$render_config = function_exists('jio_archive_get_render_config')
|
||||||
? aksrt_archive_get_render_config(['context' => 'archive'])
|
? jio_archive_get_render_config(['context' => 'archive'])
|
||||||
: ['context' => 'archive', 'mode' => 'list'];
|
: ['context' => 'archive', 'mode' => 'list'];
|
||||||
$has_sidebar = function_exists('aksrt_archive_has_sidebar')
|
$has_sidebar = function_exists('jio_archive_has_sidebar')
|
||||||
? aksrt_archive_has_sidebar()
|
? jio_archive_has_sidebar()
|
||||||
: (function_exists('aksrt_is_show_sidebar') ? aksrt_is_show_sidebar() : false);
|
: (function_exists('jio_is_show_sidebar') ? jio_is_show_sidebar() : false);
|
||||||
|
|
||||||
$archive_description = trim(wp_strip_all_tags((string) get_the_archive_description()));
|
$archive_description = trim(wp_strip_all_tags((string) get_the_archive_description()));
|
||||||
$archive_summary = '';
|
$archive_summary = '';
|
||||||
if (is_author()) {
|
if (is_author()) {
|
||||||
$author = get_queried_object();
|
$author = get_queried_object();
|
||||||
if ($author instanceof WP_User) {
|
if ($author instanceof WP_User) {
|
||||||
$archive_summary = sprintf(__('作者:%s', 'aksrt'), $author->display_name);
|
$archive_summary = sprintf(__('作者:%s', 'jio'), $author->display_name);
|
||||||
}
|
}
|
||||||
} elseif ($archive_description !== '') {
|
} elseif ($archive_description !== '') {
|
||||||
$archive_summary = $archive_description;
|
$archive_summary = $archive_description;
|
||||||
} elseif (isset($wp_query->found_posts)) {
|
} elseif (isset($wp_query->found_posts)) {
|
||||||
$archive_summary = sprintf(
|
$archive_summary = sprintf(
|
||||||
esc_html(_n('共 %d 篇内容', '共 %d 篇内容', (int) $wp_query->found_posts, 'aksrt')),
|
esc_html(_n('共 %d 篇内容', '共 %d 篇内容', (int) $wp_query->found_posts, 'jio')),
|
||||||
(int) $wp_query->found_posts
|
(int) $wp_query->found_posts
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (function_exists('aksrt_archive_open_shell')) {
|
if (function_exists('jio_archive_open_shell')) {
|
||||||
aksrt_archive_open_shell([
|
jio_archive_open_shell([
|
||||||
'main_class' => 'site-main container',
|
'main_class' => 'site-main container',
|
||||||
'has_sidebar' => $has_sidebar,
|
'has_sidebar' => $has_sidebar,
|
||||||
'page_kind' => 'archive',
|
'page_kind' => 'archive',
|
||||||
@@ -45,16 +45,16 @@ if (function_exists('aksrt_archive_open_shell')) {
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
<div class="content-main content-main--archive">
|
<div class="content-main content-main--archive">
|
||||||
<?php if (function_exists('aksrt_breadcrumbs')) { aksrt_breadcrumbs(); } ?>
|
<?php if (function_exists('jio_breadcrumbs')) { jio_breadcrumbs(); } ?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
ob_start();
|
ob_start();
|
||||||
if (function_exists('aksrt_render_admin_quick_link')) {
|
if (function_exists('jio_render_admin_quick_link')) {
|
||||||
aksrt_render_admin_quick_link('post-list/list', __('列表样式设置', 'aksrt'));
|
jio_render_admin_quick_link('post-list/list', __('列表样式设置', 'jio'));
|
||||||
}
|
}
|
||||||
$admin_link = trim((string) ob_get_clean());
|
$admin_link = trim((string) ob_get_clean());
|
||||||
get_template_part('templates/parts/page-header', null, [
|
get_template_part('templates/parts/page-header', null, [
|
||||||
'eyebrow' => __('内容归档', 'aksrt'),
|
'eyebrow' => __('内容归档', 'jio'),
|
||||||
'title' => get_the_archive_title(),
|
'title' => get_the_archive_title(),
|
||||||
'desc' => $archive_summary,
|
'desc' => $archive_summary,
|
||||||
'extra' => $admin_link,
|
'extra' => $admin_link,
|
||||||
@@ -62,19 +62,19 @@ if (function_exists('aksrt_archive_open_shell')) {
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
if (function_exists('aksrt_archive_render_stream')) {
|
if (function_exists('jio_archive_render_stream')) {
|
||||||
aksrt_archive_render_stream(null, $render_config, [
|
jio_archive_render_stream(null, $render_config, [
|
||||||
'page_kind' => 'archive',
|
'page_kind' => 'archive',
|
||||||
'query_context' => 'archive',
|
'query_context' => 'archive',
|
||||||
'stream_class' => 'content-stream--archive',
|
'stream_class' => 'content-stream--archive',
|
||||||
'empty_text' => __('暂无文章', 'aksrt'),
|
'empty_text' => __('暂无文章', 'jio'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
if (function_exists('aksrt_archive_close_shell')) {
|
if (function_exists('jio_archive_close_shell')) {
|
||||||
aksrt_archive_close_shell(['has_sidebar' => $has_sidebar]);
|
jio_archive_close_shell(['has_sidebar' => $has_sidebar]);
|
||||||
} else {
|
} else {
|
||||||
echo '</div></div>';
|
echo '</div></div>';
|
||||||
if ($has_sidebar) { get_sidebar(); }
|
if ($has_sidebar) { get_sidebar(); }
|
||||||
|
|||||||
@@ -27,8 +27,7 @@ const COMPONENT_STYLES = [
|
|||||||
// 内部基础组件
|
// 内部基础组件
|
||||||
['_internal/scrollbar/src/styles/index.cssr.mjs'],
|
['_internal/scrollbar/src/styles/index.cssr.mjs'],
|
||||||
// 布局
|
// 布局
|
||||||
['layout/src/styles/index.cssr.mjs'],
|
['layout/src/styles/layout.cssr.mjs'],
|
||||||
['layout-sider/src/styles/index.cssr.mjs'],
|
|
||||||
// 导航
|
// 导航
|
||||||
['menu/src/styles/index.cssr.mjs'],
|
['menu/src/styles/index.cssr.mjs'],
|
||||||
['breadcrumb/src/styles/index.cssr.mjs'],
|
['breadcrumb/src/styles/index.cssr.mjs'],
|
||||||
@@ -36,17 +35,19 @@ const COMPONENT_STYLES = [
|
|||||||
['steps/src/styles/index.cssr.mjs'],
|
['steps/src/styles/index.cssr.mjs'],
|
||||||
['pagination/src/styles/index.cssr.mjs'],
|
['pagination/src/styles/index.cssr.mjs'],
|
||||||
// 表单
|
// 表单
|
||||||
['form/src/styles/index.cssr.mjs'],
|
['form/src/styles/form.cssr.mjs'],
|
||||||
['form-item/src/styles/index.cssr.mjs'],
|
['form/src/styles/form-item.cssr.mjs'],
|
||||||
['input/src/styles/index.cssr.mjs'],
|
['input/src/styles/input.cssr.mjs'],
|
||||||
['input-number/src/styles/index.cssr.mjs'],
|
['input/src/styles/input-group.cssr.mjs'],
|
||||||
|
['input-number/src/styles/input-number.cssr.mjs'],
|
||||||
['switch/src/styles/index.cssr.mjs'],
|
['switch/src/styles/index.cssr.mjs'],
|
||||||
['select/src/styles/index.cssr.mjs'],
|
['select/src/styles/index.cssr.mjs'],
|
||||||
['radio/src/styles/index.cssr.mjs'],
|
['radio/src/styles/radio.cssr.mjs'],
|
||||||
|
['radio/src/styles/radio-group.cssr.mjs'],
|
||||||
['checkbox/src/styles/index.cssr.mjs'],
|
['checkbox/src/styles/index.cssr.mjs'],
|
||||||
['slider/src/styles/index.cssr.mjs'],
|
['slider/src/styles/index.cssr.mjs'],
|
||||||
['rate/src/styles/index.cssr.mjs'],
|
['rate/src/styles/index.cssr.mjs'],
|
||||||
['date-picker/src/styles/indexcode/index.cssr.mjs'],
|
['date-picker/src/styles/index.cssr.mjs'],
|
||||||
['time-picker/src/styles/index.cssr.mjs'],
|
['time-picker/src/styles/index.cssr.mjs'],
|
||||||
// 展示
|
// 展示
|
||||||
['card/src/styles/index.cssr.mjs'],
|
['card/src/styles/index.cssr.mjs'],
|
||||||
@@ -68,12 +69,12 @@ const COMPONENT_STYLES = [
|
|||||||
['notification/src/styles/index.cssr.mjs'],
|
['notification/src/styles/index.cssr.mjs'],
|
||||||
['popconfirm/src/styles/index.cssr.mjs'],
|
['popconfirm/src/styles/index.cssr.mjs'],
|
||||||
['popover/src/styles/index.cssr.mjs'],
|
['popover/src/styles/index.cssr.mjs'],
|
||||||
['tooltip/src/styles/index.cssr.mjs'],
|
// tooltip 复用 popover 的共享样式(自身没有独立 cssr 文件)。
|
||||||
['dropdown/src/styles/index.cssr.mjs'],
|
['dropdown/src/styles/index.cssr.mjs'],
|
||||||
// 其他
|
// 其他
|
||||||
['icon/src/styles/index.cssr.mjs'],
|
['icon/src/styles/index.cssr.mjs'],
|
||||||
['icon-wrapper/src/styles/index.cssr.mjs'],
|
['icon-wrapper/src/styles/index.cssr.mjs'],
|
||||||
['space/src/styles/index.cssr.mjs'],
|
// space 全部用内联 style 布局,没有 cssr 文件,故不在此列。
|
||||||
['divider/src/styles/index.cssr.mjs'],
|
['divider/src/styles/index.cssr.mjs'],
|
||||||
['gradient-text/src/styles/index.cssr.mjs'],
|
['gradient-text/src/styles/index.cssr.mjs'],
|
||||||
['ellipsis/src/styles/index.cssr.mjs'],
|
['ellipsis/src/styles/index.cssr.mjs'],
|
||||||
@@ -86,24 +87,24 @@ const COMPONENT_STYLES = [
|
|||||||
['tree-select/src/styles/index.cssr.mjs'],
|
['tree-select/src/styles/index.cssr.mjs'],
|
||||||
['tree/src/styles/index.cssr.mjs'],
|
['tree/src/styles/index.cssr.mjs'],
|
||||||
['thing/src/styles/index.cssr.mjs'],
|
['thing/src/styles/index.cssr.mjs'],
|
||||||
['color-picker/src/styles/indexcode/index.cssr.mjs'],
|
['color-picker/src/styles/index.cssr.mjs'],
|
||||||
['typography/src/styles/index.cssr.mjs'],
|
['typography/src/styles/p.cssr.mjs'],
|
||||||
|
['typography/src/styles/a.cssr.mjs'],
|
||||||
|
['typography/src/styles/blockquote.cssr.mjs'],
|
||||||
|
['typography/src/styles/list.cssr.mjs'],
|
||||||
|
['typography/src/styles/hr.cssr.mjs'],
|
||||||
|
['typography/src/styles/header.cssr.mjs'],
|
||||||
['auto-complete/src/styles/index.cssr.mjs'],
|
['auto-complete/src/styles/index.cssr.mjs'],
|
||||||
['cascader/src/styles/index.cssr.mjs'],
|
['cascader/src/styles/index.cssr.mjs'],
|
||||||
['dynamic-input/src/styles/index.cssr.mjs'],
|
['dynamic-input/src/styles/index.cssr.mjs'],
|
||||||
['dynamic-tags/src/styles/index.cssr.mjs'],
|
['dynamic-tags/src/styles/index.cssr.mjs'],
|
||||||
['input-otp/src/styles/indexcode/index.cssr.mjs'],
|
['input-otp/src/styles/index.cssr.mjs'],
|
||||||
// 过渡动画
|
|
||||||
['_styles/transitions/fade-in-width-expand.cssr.mjs'],
|
|
||||||
['_styles/transitions/fade-in-height-expand.cssr.mjs'],
|
|
||||||
['_styles/transitions/icon-switch.cssr.mjs'],
|
|
||||||
['_styles/transitions/scale-transition.cssr.mjs'],
|
|
||||||
// 全局
|
// 全局
|
||||||
['global-style/src/styles/index.cssr.mjs'],
|
['_styles/global/index.cssr.mjs'],
|
||||||
]
|
]
|
||||||
|
|
||||||
async function extractStyles() {
|
async function extractStyles() {
|
||||||
console.log('\n[AKSRT] 开始提取 NaiveUI 组件样式...\n')
|
console.log('\n[Jio] 开始提取 NaiveUI 组件样式...\n')
|
||||||
|
|
||||||
const allCss = []
|
const allCss = []
|
||||||
let successCount = 0
|
let successCount = 0
|
||||||
@@ -118,27 +119,28 @@ async function extractStyles() {
|
|||||||
|
|
||||||
const defaultExport = styleModule.default || styleModule
|
const defaultExport = styleModule.default || styleModule
|
||||||
|
|
||||||
if (typeof defaultExport === 'function') {
|
// cssr 模块有两种导出形态:函数本体,或带 render() 的 CNode 对象。
|
||||||
// 直接调用 render() 提取 CSS
|
// 两者都必须接收 props 对象(至少 {}),否则 render 内部读
|
||||||
const css = defaultExport({
|
// props.bPrefix 会抛错——card / data-table / dialog 等组件的样式
|
||||||
clsPrefix: 'n',
|
// 曾因漏传参数被整体跳过。
|
||||||
// 使用 light 主题变量
|
const css = typeof defaultExport === 'function'
|
||||||
})
|
? defaultExport({ clsPrefix: 'n' })
|
||||||
|
: defaultExport.render({ clsPrefix: 'n' })
|
||||||
|
|
||||||
if (typeof css === 'string' && css.trim()) {
|
if (typeof css === 'string' && css.trim()) {
|
||||||
allCss.push(`/* ${componentPath} */\n${css}`)
|
allCss.push(`/* ${componentPath} */\n${css}`)
|
||||||
successCount++
|
successCount++
|
||||||
}
|
}
|
||||||
} else if (defaultExport && typeof defaultExport.render === 'function') {
|
|
||||||
// 如果是 CNode 对象,调用 render()
|
|
||||||
const css = defaultExport.render()
|
|
||||||
if (typeof css === 'string' && css.trim()) {
|
|
||||||
allCss.push(`/* ${componentPath} */\n${css}`)
|
|
||||||
successCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 某些路径可能不存在或结构不同,跳过但不中断
|
const code = err && err.code
|
||||||
|
// 路径写错时 import 会直接抛 MODULE_NOT_FOUND。这类错误必须显式报出来:
|
||||||
|
// 静默跳过的后果是组件样式整体缺失(取色器曾因此没有尺寸和边框,糊成一团),
|
||||||
|
// 而页面不报错,只能靠肉眼发现。
|
||||||
|
if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') {
|
||||||
|
console.error(`[Jio] 样式路径不存在,已跳过: ${componentPath}`)
|
||||||
|
} else {
|
||||||
|
console.error(`[Jio] 样式提取异常,已跳过: ${componentPath}: ${err && err.message}`)
|
||||||
|
}
|
||||||
failCount++
|
failCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -146,7 +148,7 @@ async function extractStyles() {
|
|||||||
const cssOutput = allCss.join('\n\n')
|
const cssOutput = allCss.join('\n\n')
|
||||||
|
|
||||||
if (!cssOutput.trim()) {
|
if (!cssOutput.trim()) {
|
||||||
console.warn('[AKSRT] 警告:未提取到任何 NaiveUI 样式!')
|
console.warn('[Jio] 警告:未提取到任何 NaiveUI 样式!')
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +165,7 @@ ${cssOutput}`
|
|||||||
|
|
||||||
await writeFile(cssFile, fullContent, 'utf-8')
|
await writeFile(cssFile, fullContent, 'utf-8')
|
||||||
|
|
||||||
console.log(`[AKSRT] NaiveUI 样式提取完成:`)
|
console.log(`[Jio] NaiveUI 样式提取完成:`)
|
||||||
console.log(` 成功: ${successCount} 个组件`)
|
console.log(` 成功: ${successCount} 个组件`)
|
||||||
console.log(` 跳过: ${failCount} 个组件`)
|
console.log(` 跳过: ${failCount} 个组件`)
|
||||||
console.log(` 输出: ${cssFile}`)
|
console.log(` 输出: ${cssFile}`)
|
||||||
@@ -174,6 +176,6 @@ ${cssOutput}`
|
|||||||
|
|
||||||
// 执行
|
// 执行
|
||||||
extractStyles().catch((err) => {
|
extractStyles().catch((err) => {
|
||||||
console.error('[AKSRT] 样式提取失败:', err)
|
console.error('[Jio] 样式提取失败:', err)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
})
|
})
|
||||||
@@ -3,10 +3,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>AKSRT 主题设置</title>
|
<title>Jio 主题设置</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="aksrt-admin-app"></div>
|
<div id="jio-admin-app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Generated
+121
-94
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "aksrt-admin",
|
"name": "jio-admin",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "aksrt-admin",
|
"name": "jio-admin",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-css": "^6.3.1",
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
@@ -21,35 +21,36 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.1.0",
|
"@vitejs/plugin-vue": "^5.1.0",
|
||||||
|
"echarts": "^6.1.0",
|
||||||
"esbuild": "^0.25.12",
|
"esbuild": "^0.25.12",
|
||||||
"vite": "^6.0.0"
|
"vite": "^6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
"version": "7.27.1",
|
"version": "7.29.11",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz",
|
||||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
"integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.28.5",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.29.3",
|
"version": "7.29.12",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz",
|
||||||
"integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==",
|
"integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.29.0"
|
"@babel/types": "^7.29.11"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
@@ -59,13 +60,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.11",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz",
|
||||||
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
"integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.27.1",
|
"@babel/helper-string-parser": "^7.29.11",
|
||||||
"@babel/helper-validator-identifier": "^7.28.5"
|
"@babel/helper-validator-identifier": "^7.29.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -676,9 +677,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
|
||||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@juggle/resize-observer": {
|
"node_modules/@juggle/resize-observer": {
|
||||||
@@ -1137,53 +1138,53 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.5.33",
|
"version": "3.5.44",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.33.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.44.tgz",
|
||||||
"integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==",
|
"integrity": "sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.2",
|
"@babel/parser": "^7.29.9",
|
||||||
"@vue/shared": "3.5.33",
|
"@vue/shared": "3.5.44",
|
||||||
"entities": "^7.0.1",
|
"entities": "^7.0.1",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-dom": {
|
"node_modules/@vue/compiler-dom": {
|
||||||
"version": "3.5.33",
|
"version": "3.5.44",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.44.tgz",
|
||||||
"integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==",
|
"integrity": "sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-core": "3.5.33",
|
"@vue/compiler-core": "3.5.44",
|
||||||
"@vue/shared": "3.5.33"
|
"@vue/shared": "3.5.44"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-sfc": {
|
"node_modules/@vue/compiler-sfc": {
|
||||||
"version": "3.5.33",
|
"version": "3.5.44",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.44.tgz",
|
||||||
"integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==",
|
"integrity": "sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.2",
|
"@babel/parser": "^7.29.9",
|
||||||
"@vue/compiler-core": "3.5.33",
|
"@vue/compiler-core": "3.5.44",
|
||||||
"@vue/compiler-dom": "3.5.33",
|
"@vue/compiler-dom": "3.5.44",
|
||||||
"@vue/compiler-ssr": "3.5.33",
|
"@vue/compiler-ssr": "3.5.44",
|
||||||
"@vue/shared": "3.5.33",
|
"@vue/shared": "3.5.44",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"postcss": "^8.5.10",
|
"postcss": "^8.5.29",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-ssr": {
|
"node_modules/@vue/compiler-ssr": {
|
||||||
"version": "3.5.33",
|
"version": "3.5.44",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.44.tgz",
|
||||||
"integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==",
|
"integrity": "sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.33",
|
"@vue/compiler-dom": "3.5.44",
|
||||||
"@vue/shared": "3.5.33"
|
"@vue/shared": "3.5.44"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-api": {
|
"node_modules/@vue/devtools-api": {
|
||||||
@@ -1193,53 +1194,51 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.5.33",
|
"version": "3.5.44",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.33.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.44.tgz",
|
||||||
"integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==",
|
"integrity": "sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/shared": "3.5.33"
|
"@vue/shared": "3.5.44"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-core": {
|
"node_modules/@vue/runtime-core": {
|
||||||
"version": "3.5.33",
|
"version": "3.5.44",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.33.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.44.tgz",
|
||||||
"integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==",
|
"integrity": "sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.33",
|
"@vue/reactivity": "3.5.44",
|
||||||
"@vue/shared": "3.5.33"
|
"@vue/shared": "3.5.44"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-dom": {
|
"node_modules/@vue/runtime-dom": {
|
||||||
"version": "3.5.33",
|
"version": "3.5.44",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.44.tgz",
|
||||||
"integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==",
|
"integrity": "sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.33",
|
"@vue/reactivity": "3.5.44",
|
||||||
"@vue/runtime-core": "3.5.33",
|
"@vue/runtime-core": "3.5.44",
|
||||||
"@vue/shared": "3.5.33",
|
"@vue/shared": "3.5.44",
|
||||||
"csstype": "^3.2.3"
|
"csstype": "^3.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/server-renderer": {
|
"node_modules/@vue/server-renderer": {
|
||||||
"version": "3.5.33",
|
"version": "3.5.44",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.33.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.44.tgz",
|
||||||
"integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==",
|
"integrity": "sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-ssr": "3.5.33",
|
"@vue/compiler-ssr": "3.5.44",
|
||||||
"@vue/shared": "3.5.33"
|
"@vue/runtime-dom": "3.5.44",
|
||||||
},
|
"@vue/shared": "3.5.44"
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "3.5.33"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/shared": {
|
"node_modules/@vue/shared": {
|
||||||
"version": "3.5.33",
|
"version": "3.5.44",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.33.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.44.tgz",
|
||||||
"integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==",
|
"integrity": "sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/async-validator": {
|
"node_modules/async-validator": {
|
||||||
@@ -1310,6 +1309,17 @@
|
|||||||
"date-fns": "^3.0.0 || ^4.0.0"
|
"date-fns": "^3.0.0 || ^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/echarts": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0",
|
||||||
|
"zrender": "6.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||||
@@ -1472,9 +1482,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.12",
|
"version": "3.3.20",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
|
||||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
"integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -1531,9 +1541,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.13",
|
"version": "8.5.32",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz",
|
||||||
"integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==",
|
"integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -1550,9 +1560,9 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.20",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
@@ -1616,9 +1626,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -1653,6 +1663,13 @@
|
|||||||
"integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==",
|
"integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/vdirs": {
|
"node_modules/vdirs": {
|
||||||
"version": "0.1.8",
|
"version": "0.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz",
|
||||||
@@ -1666,9 +1683,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.4.2",
|
"version": "6.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.5.tgz",
|
||||||
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
|
"integrity": "sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1753,16 +1770,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "3.5.33",
|
"version": "3.5.44",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.44.tgz",
|
||||||
"integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==",
|
"integrity": "sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.33",
|
"@vue/compiler-dom": "3.5.44",
|
||||||
"@vue/compiler-sfc": "3.5.33",
|
"@vue/compiler-sfc": "3.5.44",
|
||||||
"@vue/runtime-dom": "3.5.33",
|
"@vue/runtime-dom": "3.5.44",
|
||||||
"@vue/server-renderer": "3.5.33",
|
"@vue/server-renderer": "3.5.44",
|
||||||
"@vue/shared": "3.5.33"
|
"@vue/shared": "3.5.44"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "*"
|
"typescript": "*"
|
||||||
@@ -1849,6 +1866,16 @@
|
|||||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/zrender": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
{"name":"aksrt-admin","version":"1.0.0","private":true,"type":"module","scripts":{"dev":"vite","build":"vite build && node extract-naive-styles.mjs","build:public":"node ../../tools/build-public-assets.mjs","preview":"vite preview"},"dependencies":{"@codemirror/lang-css":"^6.3.1","@codemirror/lang-html":"^6.4.11","@codemirror/lang-javascript":"^6.2.5","@codemirror/theme-one-dark":"^6.1.3","codemirror":"^6.0.2","naive-ui":"^2.40.0","pinia":"^2.2.0","vue":"^3.5.0","vue-router":"^4.4.0","vuedraggable":"^4.1.0"},"devDependencies":{"@vitejs/plugin-vue":"^5.1.0","esbuild":"^0.25.12","vite":"^6.0.0"}}
|
{"name":"jio-admin","version":"1.0.0","private":true,"type":"module","scripts":{"dev":"vite","build":"vite build && node extract-naive-styles.mjs","build:public":"node ../../tools/build-public-assets.mjs","build:pay-charts":"node ../../tools/build-pay-charts.mjs","preview":"vite preview"},"dependencies":{"@codemirror/lang-css":"^6.3.1","@codemirror/lang-html":"^6.4.11","@codemirror/lang-javascript":"^6.2.5","@codemirror/theme-one-dark":"^6.1.3","codemirror":"^6.0.2","naive-ui":"^2.40.0","pinia":"^2.2.0","vue":"^3.5.0","vue-router":"^4.4.0","vuedraggable":"^4.1.0"},"devDependencies":{"@vitejs/plugin-vue":"^5.1.0","echarts":"^6.1.0","esbuild":"^0.25.12","vite":"^6.0.0"},"allowScripts":{"esbuild@0.25.12":true,"vue-demi@0.14.10":true}}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
+45
-45
@@ -6,9 +6,9 @@
|
|||||||
<n-message-provider>
|
<n-message-provider>
|
||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<n-notification-provider>
|
<n-notification-provider>
|
||||||
<n-layout v-if="!isMobile" class="aksrt-admin-layout" has-sider>
|
<n-layout v-if="!isMobile" class="jio-admin-layout" has-sider>
|
||||||
<n-layout-sider
|
<n-layout-sider
|
||||||
class="aksrt-admin-sider"
|
class="jio-admin-sider"
|
||||||
bordered
|
bordered
|
||||||
collapse-mode="width"
|
collapse-mode="width"
|
||||||
:collapsed-width="64"
|
:collapsed-width="64"
|
||||||
@@ -19,16 +19,16 @@
|
|||||||
@collapse="collapsed = true"
|
@collapse="collapsed = true"
|
||||||
@expand="collapsed = false"
|
@expand="collapsed = false"
|
||||||
>
|
>
|
||||||
<div class="aksrt-logo">
|
<div class="jio-logo">
|
||||||
<span class="logo-icon">AK</span>
|
<span class="logo-icon">AK</span>
|
||||||
<span v-if="!collapsed" class="aksrt-logo__copy">
|
<span v-if="!collapsed" class="jio-logo__copy">
|
||||||
<span class="logo-text">AKSRT</span>
|
<span class="logo-text">Jio</span>
|
||||||
<span class="aksrt-logo__version">v{{ themeVersion }}</span>
|
<span class="jio-logo__version">v{{ themeVersion }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n-menu
|
<n-menu
|
||||||
class="aksrt-admin-menu"
|
class="jio-admin-menu"
|
||||||
:value="activeMenuKey"
|
:value="activeMenuKey"
|
||||||
:collapsed="collapsed"
|
:collapsed="collapsed"
|
||||||
:collapsed-width="64"
|
:collapsed-width="64"
|
||||||
@@ -37,39 +37,39 @@
|
|||||||
/>
|
/>
|
||||||
</n-layout-sider>
|
</n-layout-sider>
|
||||||
|
|
||||||
<n-layout :native-scrollbar="false" class="aksrt-admin-content">
|
<n-layout :native-scrollbar="false" class="jio-admin-content">
|
||||||
<div class="aksrt-admin-content__inner">
|
<div class="jio-admin-content__inner">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<transition name="fade-slide" mode="out-in">
|
<transition name="fade-slide" mode="out-in">
|
||||||
<div class="aksrt-page-wrapper" :key="route.fullPath">
|
<div class="jio-page-wrapper" :key="route.fullPath">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</router-view>
|
</router-view>
|
||||||
|
|
||||||
<div v-if="pageError" class="aksrt-error-fallback">
|
<div v-if="pageError" class="jio-error-fallback">
|
||||||
<p class="aksrt-error-title">页面渲染异常</p>
|
<p class="jio-error-title">页面渲染异常</p>
|
||||||
<p class="aksrt-error-msg">{{ pageError }}</p>
|
<p class="jio-error-msg">{{ pageError }}</p>
|
||||||
<n-button size="small" @click="pageError = null">重试</n-button>
|
<n-button size="small" @click="pageError = null">重试</n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-layout>
|
</n-layout>
|
||||||
</n-layout>
|
</n-layout>
|
||||||
|
|
||||||
<div v-else class="aksrt-admin-mobile">
|
<div v-else class="jio-admin-mobile">
|
||||||
<MobileTopBar :title="currentPageTitle" @toggle="drawerActive = true" />
|
<MobileTopBar :title="currentPageTitle" @toggle="drawerActive = true" />
|
||||||
|
|
||||||
<n-drawer v-model:show="drawerActive" placement="left" :width="280">
|
<n-drawer v-model:show="drawerActive" placement="left" :width="280">
|
||||||
<n-drawer-content :native-scrollbar="false" body-content-style="padding:0;">
|
<n-drawer-content :native-scrollbar="false" body-content-style="padding:0;">
|
||||||
<div class="aksrt-logo aksrt-logo--drawer">
|
<div class="jio-logo jio-logo--drawer">
|
||||||
<span class="logo-icon">AK</span>
|
<span class="logo-icon">AK</span>
|
||||||
<span class="aksrt-logo__copy">
|
<span class="jio-logo__copy">
|
||||||
<span class="logo-text">AKSRT</span>
|
<span class="logo-text">Jio</span>
|
||||||
<span class="aksrt-logo__version">v{{ themeVersion }}</span>
|
<span class="jio-logo__version">v{{ themeVersion }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<n-menu
|
<n-menu
|
||||||
class="aksrt-admin-menu"
|
class="jio-admin-menu"
|
||||||
:value="activeMenuKey"
|
:value="activeMenuKey"
|
||||||
:options="menuOptions"
|
:options="menuOptions"
|
||||||
@update:value="handleMenuClick"
|
@update:value="handleMenuClick"
|
||||||
@@ -77,18 +77,18 @@
|
|||||||
</n-drawer-content>
|
</n-drawer-content>
|
||||||
</n-drawer>
|
</n-drawer>
|
||||||
|
|
||||||
<div class="aksrt-admin-mobile__body">
|
<div class="jio-admin-mobile__body">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<transition name="fade-slide" mode="out-in">
|
<transition name="fade-slide" mode="out-in">
|
||||||
<div class="aksrt-page-wrapper" :key="route.fullPath">
|
<div class="jio-page-wrapper" :key="route.fullPath">
|
||||||
<component :is="Component" />
|
<component :is="Component" />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</router-view>
|
</router-view>
|
||||||
|
|
||||||
<div v-if="pageError" class="aksrt-error-fallback">
|
<div v-if="pageError" class="jio-error-fallback">
|
||||||
<p class="aksrt-error-title">页面渲染异常</p>
|
<p class="jio-error-title">页面渲染异常</p>
|
||||||
<p class="aksrt-error-msg">{{ pageError }}</p>
|
<p class="jio-error-msg">{{ pageError }}</p>
|
||||||
<n-button size="small" @click="pageError = null">重试</n-button>
|
<n-button size="small" @click="pageError = null">重试</n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,7 +113,7 @@ const collapsed = ref(false)
|
|||||||
const pageError = ref(null)
|
const pageError = ref(null)
|
||||||
const isMobile = ref(false)
|
const isMobile = ref(false)
|
||||||
const drawerActive = ref(false)
|
const drawerActive = ref(false)
|
||||||
const themeVersion = window.aksrtAdminData?.themeVersion || window.aksrtAdminData?.version || '9.0'
|
const themeVersion = window.jioAdminData?.themeVersion || window.jioAdminData?.version || '9.0'
|
||||||
const MOBILE_BREAKPOINT = 960
|
const MOBILE_BREAKPOINT = 960
|
||||||
|
|
||||||
const adminThemeOverrides = {
|
const adminThemeOverrides = {
|
||||||
@@ -184,7 +184,7 @@ onBeforeUnmount(() => {
|
|||||||
provide('collapsed', collapsed)
|
provide('collapsed', collapsed)
|
||||||
|
|
||||||
onErrorCaptured((err) => {
|
onErrorCaptured((err) => {
|
||||||
console.error('[AKSRT] 组件渲染错误:', err)
|
console.error('[Jio] 组件渲染错误:', err)
|
||||||
pageError.value = err?.message || String(err)
|
pageError.value = err?.message || String(err)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -224,7 +224,7 @@ const menuOptions = ADMIN_SECTIONS.filter((section) => !section.hidden).map((sec
|
|||||||
children: section.children.map((child) => ({
|
children: section.children.map((child) => ({
|
||||||
label: child.secondaryEntry ? `${child.label} · 次级` : child.label,
|
label: child.secondaryEntry ? `${child.label} · 次级` : child.label,
|
||||||
key: child.key,
|
key: child.key,
|
||||||
props: child.secondaryEntry ? { class: 'aksrt-menu-item--secondary' } : undefined,
|
props: child.secondaryEntry ? { class: 'jio-menu-item--secondary' } : undefined,
|
||||||
})),
|
})),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -254,7 +254,7 @@ const activeMenuKey = computed(() => {
|
|||||||
return ADMIN_SECTIONS[0]?.children?.[0]?.key || 'global-common'
|
return ADMIN_SECTIONS[0]?.children?.[0]?.key || 'global-common'
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentPageTitle = computed(() => route.meta?.title || 'AKSRT')
|
const currentPageTitle = computed(() => route.meta?.title || 'Jio')
|
||||||
|
|
||||||
function handleMenuClick(key) {
|
function handleMenuClick(key) {
|
||||||
if (keyToPath[key]) {
|
if (keyToPath[key]) {
|
||||||
@@ -267,17 +267,17 @@ function handleMenuClick(key) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-logo {
|
.jio-logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
gap: 11px;
|
gap: 11px;
|
||||||
padding: 0 18px;
|
padding: 0 18px;
|
||||||
border-bottom: 1px solid var(--aksrt-color-border);
|
border-bottom: 1px solid var(--jio-color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-logo--drawer {
|
.jio-logo--drawer {
|
||||||
height: 64px;
|
height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,14 +289,14 @@ function handleMenuClick(key) {
|
|||||||
height: 28px;
|
height: 28px;
|
||||||
flex: 0 0 28px;
|
flex: 0 0 28px;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
background: var(--aksrt-color-primary);
|
background: var(--jio-color-primary);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-logo__copy {
|
.jio-logo__copy {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
@@ -306,56 +306,56 @@ function handleMenuClick(key) {
|
|||||||
.logo-text {
|
.logo-text {
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--aksrt-color-text);
|
color: var(--jio-color-text);
|
||||||
letter-spacing: .08em;
|
letter-spacing: .08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-logo__version {
|
.jio-logo__version {
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
letter-spacing: .02em;
|
letter-spacing: .02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-admin-content__inner {
|
.jio-admin-content__inner {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1236px;
|
max-width: 1236px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 26px 28px 42px;
|
padding: 26px 28px 42px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-admin-mobile {
|
.jio-admin-mobile {
|
||||||
background: var(--aksrt-color-canvas);
|
background: var(--jio-color-canvas);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-admin-mobile__body {
|
.jio-admin-mobile__body {
|
||||||
padding: 14px 12px 32px;
|
padding: 14px 12px 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-page-wrapper {
|
.jio-page-wrapper {
|
||||||
min-height: calc(100vh - 120px);
|
min-height: calc(100vh - 120px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-error-fallback {
|
.jio-error-fallback {
|
||||||
padding: 48px 20px;
|
padding: 48px 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-error-title {
|
.jio-error-title {
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: #d03050;
|
color: #d03050;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-error-msg {
|
.jio-error-msg {
|
||||||
margin: 0 0 16px;
|
margin: 0 0 16px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #777;
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.aksrt-admin-content__inner {
|
.jio-admin-content__inner {
|
||||||
padding: 22px 20px 38px;
|
padding: 22px 20px 38px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
/**
|
/**
|
||||||
* AKSRT Admin API Client
|
* Jio Admin API Client
|
||||||
*
|
*
|
||||||
* Communicates with the WordPress REST API at /wp-json/aksrt/v1
|
* Communicates with the WordPress REST API at /wp-json/jio/v1
|
||||||
* (or index.php?rest_route=/aksrt/v1 when pretty permalinks are off).
|
* (or index.php?rest_route=/jio/v1 when pretty permalinks are off).
|
||||||
* Methods are organized into domain modules while the client keeps the shared
|
* Methods are organized into domain modules while the client keeps the shared
|
||||||
* request pipeline and schema-domain normalization contract.
|
* request pipeline and schema-domain normalization contract.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { apiMethods } from './modules/index.js'
|
import { apiMethods } from './modules/index.js'
|
||||||
|
|
||||||
const API_ROOT = window.aksrtAdminData?.apiUrl || '/wp-json/aksrt/v1'
|
const API_ROOT = window.jioAdminData?.apiUrl || '/wp-json/jio/v1'
|
||||||
const NONCE = window.aksrtAdminData?.nonce || ''
|
const NONCE = window.jioAdminData?.nonce || ''
|
||||||
|
|
||||||
function createIdempotencyKey() {
|
function createIdempotencyKey() {
|
||||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||||
@@ -34,11 +34,11 @@ function normalizeDomainKey(domain) {
|
|||||||
/**
|
/**
|
||||||
* Join REST base + endpoint correctly for both pretty and plain permalinks.
|
* Join REST base + endpoint correctly for both pretty and plain permalinks.
|
||||||
*
|
*
|
||||||
* Pretty: https://site/wp-json/aksrt/v1 + /settings/schema/global/common
|
* Pretty: https://site/wp-json/jio/v1 + /settings/schema/global/common
|
||||||
* → https://site/wp-json/aksrt/v1/settings/schema/global/common
|
* → https://site/wp-json/jio/v1/settings/schema/global/common
|
||||||
*
|
*
|
||||||
* Plain: https://site/index.php?rest_route=/aksrt/v1 + /settings/schema/global/common
|
* Plain: https://site/index.php?rest_route=/jio/v1 + /settings/schema/global/common
|
||||||
* → https://site/index.php?rest_route=/aksrt/v1/settings/schema/global/common
|
* → https://site/index.php?rest_route=/jio/v1/settings/schema/global/common
|
||||||
*
|
*
|
||||||
* @param {string} baseUrl
|
* @param {string} baseUrl
|
||||||
* @param {string} endpoint path starting with /, may include ?query
|
* @param {string} endpoint path starting with /, may include ?query
|
||||||
@@ -56,7 +56,7 @@ function joinRestUrl(baseUrl, endpoint) {
|
|||||||
path = '/' + path
|
path = '/' + path
|
||||||
}
|
}
|
||||||
|
|
||||||
const base = String(baseUrl || '').trim() || '/wp-json/aksrt/v1'
|
const base = String(baseUrl || '').trim() || '/wp-json/jio/v1'
|
||||||
|
|
||||||
// Plain permalinks: rest_route query mode
|
// Plain permalinks: rest_route query mode
|
||||||
if (base.includes('rest_route=')) {
|
if (base.includes('rest_route=')) {
|
||||||
@@ -68,11 +68,11 @@ function joinRestUrl(baseUrl, endpoint) {
|
|||||||
return base.replace(/\/?$/, '') + path + (extraQuery ? (base.includes('?') ? '&' : '?') + extraQuery : '')
|
return base.replace(/\/?$/, '') + path + (extraQuery ? (base.includes('?') ? '&' : '?') + extraQuery : '')
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseRoute = (u.searchParams.get('rest_route') || '/aksrt/v1').replace(/\/$/, '') || '/aksrt/v1'
|
const baseRoute = (u.searchParams.get('rest_route') || '/jio/v1').replace(/\/$/, '') || '/jio/v1'
|
||||||
let fullRoute
|
let fullRoute
|
||||||
if (path === '/') {
|
if (path === '/') {
|
||||||
fullRoute = baseRoute
|
fullRoute = baseRoute
|
||||||
} else if (path.startsWith('/aksrt/')) {
|
} else if (path.startsWith('/jio/')) {
|
||||||
fullRoute = path
|
fullRoute = path
|
||||||
} else {
|
} else {
|
||||||
fullRoute = baseRoute + path
|
fullRoute = baseRoute + path
|
||||||
@@ -110,7 +110,7 @@ class ApiClient {
|
|||||||
async request(endpointOrConfig, options = {}) {
|
async request(endpointOrConfig, options = {}) {
|
||||||
// Dual-signature support:
|
// Dual-signature support:
|
||||||
// request('/users/1/restrictions')
|
// request('/users/1/restrictions')
|
||||||
// request({ url: '/aksrt/v1/users/1/restrictions', method: 'GET' })
|
// request({ url: '/jio/v1/users/1/restrictions', method: 'GET' })
|
||||||
let endpoint, method, headers, params, body, rest
|
let endpoint, method, headers, params, body, rest
|
||||||
|
|
||||||
if (typeof endpointOrConfig === 'object' && endpointOrConfig !== null) {
|
if (typeof endpointOrConfig === 'object' && endpointOrConfig !== null) {
|
||||||
@@ -134,10 +134,10 @@ class ApiClient {
|
|||||||
delete rest.data; delete rest.headers
|
delete rest.data; delete rest.headers
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endpoint.startsWith('/wp-json/aksrt/v1')) {
|
if (endpoint.startsWith('/wp-json/jio/v1')) {
|
||||||
endpoint = endpoint.replace('/wp-json/aksrt/v1', '') || '/'
|
endpoint = endpoint.replace('/wp-json/jio/v1', '') || '/'
|
||||||
} else if (endpoint.startsWith('/aksrt/v1')) {
|
} else if (endpoint.startsWith('/jio/v1')) {
|
||||||
endpoint = endpoint.replace('/aksrt/v1', '') || '/'
|
endpoint = endpoint.replace('/jio/v1', '') || '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = joinRestUrl(this.baseUrl, endpoint)
|
let url = joinRestUrl(this.baseUrl, endpoint)
|
||||||
|
|||||||
@@ -5,11 +5,4 @@ export const extensionsApiMethods = {
|
|||||||
|
|
||||||
testIpGeo(ip) { return this.request('/extensions/ip-test', { method: 'POST', data: { ip } }) },
|
testIpGeo(ip) { return this.request('/extensions/ip-test', { method: 'POST', data: { ip } }) },
|
||||||
testContentAudit(text) { return this.request('/extensions/audit-test', { method: 'POST', data: { text } }) },
|
testContentAudit(text) { return this.request('/extensions/audit-test', { method: 'POST', data: { text } }) },
|
||||||
|
|
||||||
/* AI 辅助 */
|
|
||||||
getAiStatus() { return this.request('/ai/status') },
|
|
||||||
updateAiSettings(data) { return this.request('/ai/settings', { method: 'POST', data }) },
|
|
||||||
generateSeo(data) { return this.request('/ai/seo-generate', { method: 'POST', data }) },
|
|
||||||
suggestSummary(data) { return this.request('/ai/summary-suggest', { method: 'POST', data }) },
|
|
||||||
suggestTags(data) { return this.request('/ai/tags-suggest', { method: 'POST', data }) },
|
|
||||||
}
|
}
|
||||||
@@ -11,12 +11,6 @@ export const globalApiMethods = {
|
|||||||
|
|
||||||
getFloatingButtons() { return this.request('/settings/floating-buttons') },
|
getFloatingButtons() { return this.request('/settings/floating-buttons') },
|
||||||
saveFloatingButtons(data) { return this.request('/settings/floating-buttons', { method: 'POST', body: JSON.stringify(data) }) },
|
saveFloatingButtons(data) { return this.request('/settings/floating-buttons', { method: 'POST', body: JSON.stringify(data) }) },
|
||||||
reorderFloatingButtons(ids) { return this.request('/settings/floating-buttons/reorder', { method: 'POST', body: JSON.stringify({ ids }) }) },
|
|
||||||
|
|
||||||
getFriendLinks() { return this.request('/friend-links') },
|
|
||||||
addFriendLink(data) { return this.request('/friend-links', { method: 'POST', body: JSON.stringify(data) }) },
|
|
||||||
deleteFriendLink(id) { return this.request(`/friend-links/${id}`, { method: 'DELETE' }) },
|
|
||||||
reorderFriendLinks(ids) { return this.request('/friend-links/reorder', { method: 'POST', body: JSON.stringify({ ids }) }) },
|
|
||||||
|
|
||||||
getCmsFeatures() { return this.request('/settings/cms-features') },
|
getCmsFeatures() { return this.request('/settings/cms-features') },
|
||||||
saveCmsFeatures(cms_features) { return this.request('/settings/cms-features', { method: 'POST', body: JSON.stringify({ cms_features }) }) },
|
saveCmsFeatures(cms_features) { return this.request('/settings/cms-features', { method: 'POST', body: JSON.stringify({ cms_features }) }) },
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { extensionsApiMethods } from './extensions.js'
|
|||||||
import { toolsApiMethods } from './tools.js'
|
import { toolsApiMethods } from './tools.js'
|
||||||
import { schemaApiMethods } from './schema.js'
|
import { schemaApiMethods } from './schema.js'
|
||||||
import { iconsApiMethods } from './icons.js'
|
import { iconsApiMethods } from './icons.js'
|
||||||
|
import { ticketsApiMethods } from './tickets.js'
|
||||||
|
|
||||||
export const apiMethods = {
|
export const apiMethods = {
|
||||||
...globalApiMethods,
|
...globalApiMethods,
|
||||||
@@ -16,4 +17,5 @@ export const apiMethods = {
|
|||||||
...toolsApiMethods,
|
...toolsApiMethods,
|
||||||
...schemaApiMethods,
|
...schemaApiMethods,
|
||||||
...iconsApiMethods,
|
...iconsApiMethods,
|
||||||
|
...ticketsApiMethods,
|
||||||
}
|
}
|
||||||
@@ -3,14 +3,6 @@ export const permissionsApiMethods = {
|
|||||||
* 功能&权限 (Permissions)
|
* 功能&权限 (Permissions)
|
||||||
* ================================================================ */
|
* ================================================================ */
|
||||||
|
|
||||||
getPermissionsAttachmentSettings() { return this.getSchemaPage('permissions', 'attachment') },
|
|
||||||
savePermissionsAttachmentSettings(data) { return this.saveSchemaPage('permissions', 'attachment', data) },
|
|
||||||
|
|
||||||
getPermissionsLoginSettings() { return this.getSchemaPage('permissions', 'login') },
|
|
||||||
savePermissionsLoginSettings(data) { return this.saveSchemaPage('permissions', 'login', data) },
|
|
||||||
|
|
||||||
getPermissionsModerationSettings() { return this.getSchemaPage('permissions', 'moderation') },
|
|
||||||
|
|
||||||
getCapabilities() { return this.request('/permissions/capabilities') },
|
getCapabilities() { return this.request('/permissions/capabilities') },
|
||||||
saveCapabilities(user_caps) { return this.request('/permissions/capabilities', { method: 'POST', body: JSON.stringify({ user_caps }) }) },
|
saveCapabilities(user_caps) { return this.request('/permissions/capabilities', { method: 'POST', body: JSON.stringify({ user_caps }) }) },
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* 工单管理端 REST(命名空间 jio/v1/admin)。
|
||||||
|
*
|
||||||
|
* TicketRestAdmin 把三块字典 CRUD(工单状态 / 部门 / 预设回复)挂在
|
||||||
|
* jio/v1/admin 下,全部要求 manage_options。api 客户端的 baseUrl 只到
|
||||||
|
* jio/v1,因此这里必须用 /jio/ 全路径形式(joinRestUrl 的
|
||||||
|
* startsWith('/jio/') 分支会原样拼接,不再追加 base 前缀)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ADMIN = '/jio/v1/admin/tickets'
|
||||||
|
|
||||||
|
export const ticketsApiMethods = {
|
||||||
|
/* ---------------- 工单状态 ---------------- */
|
||||||
|
|
||||||
|
listTicketStatuses() { return this.request(`${ADMIN}/statuses`) },
|
||||||
|
createTicketStatus(data) {
|
||||||
|
return this.request(`${ADMIN}/statuses`, { method: 'POST', body: JSON.stringify(data) })
|
||||||
|
},
|
||||||
|
updateTicketStatus(id, data) {
|
||||||
|
return this.request(`${ADMIN}/statuses/${id}`, { method: 'POST', body: JSON.stringify(data) })
|
||||||
|
},
|
||||||
|
deleteTicketStatus(id) {
|
||||||
|
return this.request(`${ADMIN}/statuses/${id}/delete`, { method: 'POST', body: '{}' })
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ---------------- 工单部门 ---------------- */
|
||||||
|
|
||||||
|
listTicketTypes() { return this.request(`${ADMIN}/types`) },
|
||||||
|
createTicketType(data) {
|
||||||
|
return this.request(`${ADMIN}/types`, { method: 'POST', body: JSON.stringify(data) })
|
||||||
|
},
|
||||||
|
updateTicketType(id, data) {
|
||||||
|
return this.request(`${ADMIN}/types/${id}`, { method: 'POST', body: JSON.stringify(data) })
|
||||||
|
},
|
||||||
|
deleteTicketType(id) {
|
||||||
|
return this.request(`${ADMIN}/types/${id}/delete`, { method: 'POST', body: '{}' })
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ---------------- 预设回复 ---------------- */
|
||||||
|
|
||||||
|
listTicketPrereplies() { return this.request(`${ADMIN}/prereplies`) },
|
||||||
|
createTicketPrereply(data) {
|
||||||
|
return this.request(`${ADMIN}/prereplies`, { method: 'POST', body: JSON.stringify(data) })
|
||||||
|
},
|
||||||
|
updateTicketPrereply(id, data) {
|
||||||
|
return this.request(`${ADMIN}/prereplies/${id}`, { method: 'POST', body: JSON.stringify(data) })
|
||||||
|
},
|
||||||
|
deleteTicketPrereply(id) {
|
||||||
|
return this.request(`${ADMIN}/prereplies/${id}/delete`, { method: 'POST', body: '{}' })
|
||||||
|
},
|
||||||
|
/** 排序入参必须是全量 ID,数量不符后端直接拒绝。 */
|
||||||
|
reorderTicketPrereplies(ids) {
|
||||||
|
return this.request(`${ADMIN}/prereplies/order`, { method: 'POST', body: JSON.stringify({ ids }) })
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ export const usersApiMethods = {
|
|||||||
|
|
||||||
getAuthSettings() { return this.request('/settings/auth') },
|
getAuthSettings() { return this.request('/settings/auth') },
|
||||||
saveAuthSettings(data) { return this.request('/settings/auth', { method: 'POST', body: JSON.stringify(data) }) },
|
saveAuthSettings(data) { return this.request('/settings/auth', { method: 'POST', body: JSON.stringify(data) }) },
|
||||||
|
testSms(phone) { return this.request('/sms/test', { method: 'POST', body: JSON.stringify({ phone }) }) },
|
||||||
|
|
||||||
getNotificationSettings() { return this.request('/settings/notifications') },
|
getNotificationSettings() { return this.request('/settings/notifications') },
|
||||||
saveNotificationSettings(data) { return this.request('/settings/notifications', { method: 'POST', body: JSON.stringify(data) }) },
|
saveNotificationSettings(data) { return this.request('/settings/notifications', { method: 'POST', body: JSON.stringify(data) }) },
|
||||||
@@ -17,14 +18,6 @@ export const usersApiMethods = {
|
|||||||
deleteNotification(id) { return this.request(`/notifications/${id}`, { method: 'DELETE' }) },
|
deleteNotification(id) { return this.request(`/notifications/${id}`, { method: 'DELETE' }) },
|
||||||
runNotificationCleanup() { return this.request('/notifications/cleanup', { method: 'POST', body: JSON.stringify({}) }) },
|
runNotificationCleanup() { return this.request('/notifications/cleanup', { method: 'POST', body: JSON.stringify({}) }) },
|
||||||
|
|
||||||
getIdentitySettings() { return this.request('/identity/settings') },
|
|
||||||
saveIdentitySettings(data) {
|
|
||||||
return this.request('/identity/settings', { method: 'POST', body: JSON.stringify(data) })
|
|
||||||
},
|
|
||||||
testIdentitySettings(data = {}) {
|
|
||||||
return this.request('/identity/settings/test', { method: 'POST', body: JSON.stringify(data) })
|
|
||||||
},
|
|
||||||
|
|
||||||
getCreditSettings() { return this.request('/assets/settings') },
|
getCreditSettings() { return this.request('/assets/settings') },
|
||||||
saveCreditSettings(data) { return this.request('/assets/settings', { method: 'POST', body: JSON.stringify(data) }) },
|
saveCreditSettings(data) { return this.request('/assets/settings', { method: 'POST', body: JSON.stringify(data) }) },
|
||||||
listCreditUsers(params = {}) {
|
listCreditUsers(params = {}) {
|
||||||
@@ -34,8 +27,4 @@ export const usersApiMethods = {
|
|||||||
adjustUserCredits(id, asset, delta, remark) {
|
adjustUserCredits(id, asset, delta, remark) {
|
||||||
return this.request(`/assets/accounts/${id}/adjust`, { method: 'POST', body: JSON.stringify({ asset, delta, remark }) })
|
return this.request(`/assets/accounts/${id}/adjust`, { method: 'POST', body: JSON.stringify({ asset, delta, remark }) })
|
||||||
},
|
},
|
||||||
getCreditLog(params = {}) {
|
|
||||||
const qs = new URLSearchParams(params).toString()
|
|
||||||
return this.request('/assets/transactions' + (qs ? `?${qs}` : ''))
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
@@ -1,35 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-deferred-page">
|
<div class="jio-deferred-page">
|
||||||
<div class="aksrt-deferred-page__icon">
|
<div class="jio-deferred-page__icon">
|
||||||
<FaIcon :icon="resolvedIcon" :size="48" />
|
<FaIcon :icon="resolvedIcon" :size="48" />
|
||||||
</div>
|
</div>
|
||||||
<h2 class="aksrt-deferred-page__title">{{ resolvedTitle }}</h2>
|
<h2 class="jio-deferred-page__title">{{ resolvedTitle }}</h2>
|
||||||
<p class="aksrt-deferred-page__desc">{{ resolvedDesc }}</p>
|
<p class="jio-deferred-page__desc">{{ resolvedDesc }}</p>
|
||||||
<div class="aksrt-deferred-page__meta">
|
<div class="jio-deferred-page__meta">
|
||||||
<n-tag size="small" type="info">{{ resolvedStatus }}</n-tag>
|
<n-tag size="small" type="info">{{ resolvedStatus }}</n-tag>
|
||||||
<n-tag v-if="resolvedPhase" size="small" type="warning">Phase {{ resolvedPhase }}</n-tag>
|
<n-tag v-if="resolvedPhase" size="small" type="warning">Phase {{ resolvedPhase }}</n-tag>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="resolvedDependsOn.length" class="aksrt-deferred-page__depends">
|
<div v-if="resolvedDependsOn.length" class="jio-deferred-page__depends">
|
||||||
<span class="aksrt-deferred-page__label">依赖</span>
|
<span class="jio-deferred-page__label">依赖</span>
|
||||||
<div class="aksrt-deferred-page__depends-list">
|
<div class="jio-deferred-page__depends-list">
|
||||||
<n-tag v-for="item in resolvedDependsOn" :key="item" size="small" type="default">{{ item }}</n-tag>
|
<n-tag v-for="item in resolvedDependsOn" :key="item" size="small" type="default">{{ item }}</n-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="resolvedReason" class="aksrt-deferred-page__reason">
|
<div v-if="resolvedReason" class="jio-deferred-page__reason">
|
||||||
<span class="aksrt-deferred-page__label">原因</span>
|
<span class="jio-deferred-page__label">原因</span>
|
||||||
<p>{{ resolvedReason }}</p>
|
<p>{{ resolvedReason }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="resolvedFeatures && resolvedFeatures.length" class="aksrt-deferred-page__features">
|
<div v-if="resolvedFeatures && resolvedFeatures.length" class="jio-deferred-page__features">
|
||||||
<div
|
<div
|
||||||
v-for="(feat, i) in resolvedFeatures"
|
v-for="(feat, i) in resolvedFeatures"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="aksrt-deferred-page__feature"
|
class="jio-deferred-page__feature"
|
||||||
>
|
>
|
||||||
<FaIcon icon="fa fas fa-check-circle" :size="14" />
|
<FaIcon icon="fa fas fa-check-circle" :size="14" />
|
||||||
<span>{{ feat }}</span>
|
<span>{{ feat }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="aksrt-deferred-page__badge">
|
<div class="jio-deferred-page__badge">
|
||||||
<n-tag type="info" size="small">即将推出</n-tag>
|
<n-tag type="info" size="small">即将推出</n-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,7 +78,7 @@ const resolvedFeatures = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-deferred-page {
|
.jio-deferred-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -88,75 +88,75 @@ const resolvedFeatures = computed(() => {
|
|||||||
padding: 40px 20px;
|
padding: 40px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-deferred-page__icon {
|
.jio-deferred-page__icon {
|
||||||
color: var(--aksrt-color-border-strong);
|
color: var(--jio-color-border-strong);
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-deferred-page__title {
|
.jio-deferred-page__title {
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--aksrt-color-text);
|
color: var(--jio-color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-deferred-page__desc {
|
.jio-deferred-page__desc {
|
||||||
margin: 0 0 20px;
|
margin: 0 0 20px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-deferred-page__meta {
|
.jio-deferred-page__meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-deferred-page__depends,
|
.jio-deferred-page__depends,
|
||||||
.aksrt-deferred-page__reason {
|
.jio-deferred-page__reason {
|
||||||
width: min(520px, 100%);
|
width: min(520px, 100%);
|
||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-deferred-page__label {
|
.jio-deferred-page__label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--aksrt-color-text-secondary);
|
color: var(--jio-color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-deferred-page__depends-list {
|
.jio-deferred-page__depends-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-deferred-page__reason p {
|
.jio-deferred-page__reason p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-deferred-page__features {
|
.jio-deferred-page__features {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-deferred-page__feature {
|
.jio-deferred-page__feature {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-deferred-page__feature :deep(i) {
|
.jio-deferred-page__feature :deep(i) {
|
||||||
color: #52c41a;
|
color: #52c41a;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-empty-state">
|
<div class="jio-empty-state">
|
||||||
<div class="aksrt-empty-state__icon">
|
<div class="jio-empty-state__icon">
|
||||||
<i :class="icon" style="font-size: 48px;"></i>
|
<i :class="icon" style="font-size: 48px;"></i>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="aksrt-empty-state__title">{{ title }}</h3>
|
<h3 class="jio-empty-state__title">{{ title }}</h3>
|
||||||
<p class="aksrt-empty-state__desc">{{ desc }}</p>
|
<p class="jio-empty-state__desc">{{ desc }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ defineProps({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-empty-state {
|
.jio-empty-state {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -26,22 +26,22 @@ defineProps({
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-empty-state__icon {
|
.jio-empty-state__icon {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
color: var(--aksrt-color-border-strong);
|
color: var(--jio-color-border-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-empty-state__title {
|
.jio-empty-state__title {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--aksrt-color-text);
|
color: var(--jio-color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-empty-state__desc {
|
.jio-empty-state__desc {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
max-width: 360px;
|
max-width: 360px;
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="aksrt-mobile-topbar">
|
<header class="jio-mobile-topbar">
|
||||||
<button
|
<button
|
||||||
class="aksrt-mobile-topbar__menu"
|
class="jio-mobile-topbar__menu"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="打开菜单"
|
aria-label="打开菜单"
|
||||||
@click="$emit('toggle')"
|
@click="$emit('toggle')"
|
||||||
@@ -13,35 +13,35 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="aksrt-mobile-topbar__title">{{ title }}</div>
|
<div class="jio-mobile-topbar__title">{{ title }}</div>
|
||||||
|
|
||||||
<div class="aksrt-mobile-topbar__brand">AK</div>
|
<div class="jio-mobile-topbar__brand">AK</div>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
defineProps({
|
||||||
title: { type: String, default: 'AKSRT' },
|
title: { type: String, default: 'Jio' },
|
||||||
})
|
})
|
||||||
|
|
||||||
defineEmits(['toggle'])
|
defineEmits(['toggle'])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-mobile-topbar {
|
.jio-mobile-topbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
background: var(--aksrt-color-surface);
|
background: var(--jio-color-surface);
|
||||||
border-bottom: 1px solid var(--aksrt-color-border);
|
border-bottom: 1px solid var(--jio-color-border);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-mobile-topbar__menu {
|
.jio-mobile-topbar__menu {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -50,26 +50,26 @@ defineEmits(['toggle'])
|
|||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--aksrt-color-text);
|
color: var(--jio-color-text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-left: -6px;
|
margin-left: -6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-mobile-topbar__menu:hover {
|
.jio-mobile-topbar__menu:hover {
|
||||||
background: var(--aksrt-color-primary-soft);
|
background: var(--jio-color-primary-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-mobile-topbar__title {
|
.jio-mobile-topbar__title {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--aksrt-color-text);
|
color: var(--jio-color-text);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-mobile-topbar__brand {
|
.jio-mobile-topbar__brand {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -77,7 +77,7 @@ defineEmits(['toggle'])
|
|||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: var(--aksrt-color-primary);
|
background: var(--jio-color-primary);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<header class="aksrt-page-header">
|
<header class="jio-page-header">
|
||||||
<div class="aksrt-page-header__main">
|
<div class="jio-page-header__main">
|
||||||
<h2 class="aksrt-page-header__title">{{ title }}</h2>
|
<h2 class="jio-page-header__title">{{ title }}</h2>
|
||||||
<p v-if="desc" class="aksrt-page-header__desc">
|
<p v-if="desc" class="jio-page-header__desc">
|
||||||
<slot name="desc">{{ desc }}</slot>
|
<slot name="desc">{{ desc }}</slot>
|
||||||
</p>
|
</p>
|
||||||
<div v-else-if="$slots.desc" class="aksrt-page-header__desc">
|
<div v-else-if="$slots.desc" class="jio-page-header__desc">
|
||||||
<slot name="desc" />
|
<slot name="desc" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="$slots.actions" class="aksrt-page-header__actions">
|
<div v-if="$slots.actions" class="jio-page-header__actions">
|
||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -23,7 +23,7 @@ defineProps({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-page-header {
|
.jio-page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -32,28 +32,28 @@ defineProps({
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-page-header__main {
|
.jio-page-header__main {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-page-header__title {
|
.jio-page-header__title {
|
||||||
margin: 0 0 6px;
|
margin: 0 0 6px;
|
||||||
color: var(--aksrt-color-text);
|
color: var(--jio-color-text);
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-page-header__desc {
|
.jio-page-header__desc {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-page-header__actions {
|
.jio-page-header__actions {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -61,20 +61,20 @@ defineProps({
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.aksrt-page-header {
|
.jio-page-header {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-page-header__title {
|
.jio-page-header__title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-page-header__desc {
|
.jio-page-header__desc {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-page-header__actions {
|
.jio-page-header__actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,9 @@
|
|||||||
<n-form-item label="AccessKey Secret">
|
<n-form-item label="AccessKey Secret">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="model.access_key_secret"
|
v-model:value="model.access_key_secret"
|
||||||
type="password"
|
type="text"
|
||||||
show-password-on="click"
|
show-password-on="click"
|
||||||
|
class="jio-secret-input"
|
||||||
autocomplete="new-password"
|
autocomplete="new-password"
|
||||||
placeholder="阿里云 RAM 用户 AccessKey Secret"
|
placeholder="阿里云 RAM 用户 AccessKey Secret"
|
||||||
@update:value="onSecretInput('access_key_secret', $event)"
|
@update:value="onSecretInput('access_key_secret', $event)"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<n-input v-model:value="model.site_key" placeholder="Cloudflare Turnstile Site Key" autocomplete="off" />
|
<n-input v-model:value="model.site_key" placeholder="Cloudflare Turnstile Site Key" autocomplete="off" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="Secret Key">
|
<n-form-item label="Secret Key">
|
||||||
<n-input v-model:value="model.secret_key" type="password" show-password-on="click" autocomplete="new-password" @update:value="onSecretInput('secret_key', $event)" />
|
<n-input v-model:value="model.secret_key" type="text" show-password-on="click" class="jio-secret-input" autocomplete="new-password" @update:value="onSecretInput('secret_key', $event)" />
|
||||||
<n-button v-if="model.secret_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('secret_key')">清除</n-button>
|
<n-button v-if="model.secret_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('secret_key')">清除</n-button>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="主题">
|
<n-form-item label="主题">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<n-input v-model:value="model.captcha_id" placeholder="极验 GeeTest CaptchaID" autocomplete="off" />
|
<n-input v-model:value="model.captcha_id" placeholder="极验 GeeTest CaptchaID" autocomplete="off" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="Captcha Key">
|
<n-form-item label="Captcha Key">
|
||||||
<n-input v-model:value="model.captcha_key" type="password" show-password-on="click" autocomplete="new-password" @update:value="onSecretInput('captcha_key', $event)" />
|
<n-input v-model:value="model.captcha_key" type="text" show-password-on="click" class="jio-secret-input" autocomplete="new-password" @update:value="onSecretInput('captcha_key', $event)" />
|
||||||
<n-button v-if="model.captcha_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('captcha_key')">清除</n-button>
|
<n-button v-if="model.captcha_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('captcha_key')">清除</n-button>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-tag v-if="isConfigured" type="success" size="small">✓ 已完整配置</n-tag>
|
<n-tag v-if="isConfigured" type="success" size="small">✓ 已完整配置</n-tag>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<n-input v-model:value="model.secret_id" placeholder="腾讯云 API SecretId" autocomplete="off" />
|
<n-input v-model:value="model.secret_id" placeholder="腾讯云 API SecretId" autocomplete="off" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="SecretKey">
|
<n-form-item label="SecretKey">
|
||||||
<n-input v-model:value="model.secret_key" type="password" placeholder="腾讯云 API SecretKey" autocomplete="new-password" show-password-on="click" @update:value="onSecretInput('secret_key', $event)" />
|
<n-input v-model:value="model.secret_key" type="text" class="jio-secret-input" placeholder="腾讯云 API SecretKey" autocomplete="off" @update:value="onSecretInput('secret_key', $event)" />
|
||||||
<n-button v-if="model.secret_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('secret_key')">清除</n-button>
|
<n-button v-if="model.secret_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('secret_key')">清除</n-button>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<n-input v-model:value="model.app_id" placeholder="从验证码控制台获取 CaptchaAppId" autocomplete="off" />
|
<n-input v-model:value="model.app_id" placeholder="从验证码控制台获取 CaptchaAppId" autocomplete="off" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="AppSecretKey">
|
<n-form-item label="AppSecretKey">
|
||||||
<n-input v-model:value="model.app_secret_key" type="password" placeholder="从验证码控制台获取 AppSecretKey" autocomplete="new-password" show-password-on="click" @update:value="onSecretInput('app_secret_key', $event)" />
|
<n-input v-model:value="model.app_secret_key" type="text" class="jio-secret-input" placeholder="从验证码控制台获取 AppSecretKey" autocomplete="off" @update:value="onSecretInput('app_secret_key', $event)" />
|
||||||
<n-button v-if="model.app_secret_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('app_secret_key')">清除</n-button>
|
<n-button v-if="model.app_secret_key === '***'" size="tiny" type="error" ghost style="margin-left:8px" @click="clearSecret('app_secret_key')">清除</n-button>
|
||||||
<template #feedback>请从「验证管理 → 基础配置」完整复制,注意区分 I/l/1/O/0</template>
|
<template #feedback>请从「验证管理 → 基础配置」完整复制,注意区分 I/l/1/O/0</template>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<template>
|
||||||
|
<n-form label-placement="left" label-width="110" class="jio-form float-common-fields">
|
||||||
|
<n-form-item label="PC端显示" :show-feedback="false">
|
||||||
|
<n-switch :value="!!modelValue.pc_s" @update:value="update('pc_s', $event)" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="移动端显示" :show-feedback="false">
|
||||||
|
<n-switch :value="!!modelValue.m_s" @update:value="update('m_s', $event)" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="颜色" :show-feedback="false">
|
||||||
|
<div class="float-common-fields__colors">
|
||||||
|
<div class="float-common-fields__color">
|
||||||
|
<span>自定义图标颜色</span>
|
||||||
|
<JioColorPicker
|
||||||
|
:model-value="modelValue.color?.color || ''"
|
||||||
|
alpha
|
||||||
|
clearable
|
||||||
|
@update:model-value="updateColor('color', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="float-common-fields__color">
|
||||||
|
<span>自定义背景颜色</span>
|
||||||
|
<JioColorPicker
|
||||||
|
:model-value="modelValue.color?.bg || ''"
|
||||||
|
alpha
|
||||||
|
clearable
|
||||||
|
@update:model-value="updateColor('bg', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
<div class="float-common-fields__desc">自定义颜色请注意日间、夜间模式的适配</div>
|
||||||
|
</n-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import JioColorPicker from '@/components/form/JioColorPicker.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Object, required: true },
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
function update(key, value) {
|
||||||
|
emit('update:modelValue', { ...props.modelValue, [key]: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateColor(key, value) {
|
||||||
|
emit('update:modelValue', {
|
||||||
|
...props.modelValue,
|
||||||
|
color: { ...(props.modelValue.color || { color: '', bg: '' }), [key]: value || '' },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.float-common-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-common-fields__colors {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-common-fields__color {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 180px;
|
||||||
|
flex: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--n-text-color-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-common-fields__desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--n-text-color-3);
|
||||||
|
padding-left: 110px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<div class="float-btn-panel">
|
||||||
|
<div class="jio-sortable__head" v-bind="headAttrs">
|
||||||
|
<JioSortHandle @click.stop />
|
||||||
|
<div class="jio-sortable__main">
|
||||||
|
<div class="jio-sortable__title-row">
|
||||||
|
<span class="jio-sortable__title">{{ meta.title }}</span>
|
||||||
|
<template v-if="modelValue.type !== 'more'">
|
||||||
|
<n-tag v-if="modelValue.pc_s" size="small" type="info" :bordered="false">PC</n-tag>
|
||||||
|
<n-tag v-if="modelValue.m_s" size="small" type="warning" :bordered="false">移动</n-tag>
|
||||||
|
<n-tag v-if="!modelValue.pc_s && !modelValue.m_s" size="small" :bordered="false">未显示</n-tag>
|
||||||
|
</template>
|
||||||
|
<n-tag v-else size="small" :bordered="false">{{ modelValue.buttons.length }} 个</n-tag>
|
||||||
|
</div>
|
||||||
|
<span v-if="meta.warn" class="jio-sortable__desc float-btn-panel__warn">
|
||||||
|
<i class="fa fas fa-info-circle" aria-hidden="true" /> {{ meta.warn }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<i
|
||||||
|
class="fa fas jio-sortable__toggle"
|
||||||
|
:class="expanded ? 'fa-chevron-down' : 'fa-chevron-right'"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="expanded" class="jio-sortable__body">
|
||||||
|
<div class="float-btn-panel__subtitle">{{ meta.panel }}</div>
|
||||||
|
|
||||||
|
<FloatMoreButtonsEditor
|
||||||
|
v-if="modelValue.type === 'more'"
|
||||||
|
:model-value="modelValue.buttons"
|
||||||
|
@update:model-value="patch({ buttons: $event })"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<FloatBtnCommonFields :model-value="modelValue" @update:model-value="emit('update:modelValue', $event)" />
|
||||||
|
|
||||||
|
<n-form label-placement="left" label-width="110" class="jio-form float-btn-panel__extra">
|
||||||
|
<n-form-item v-if="modelValue.type === 'qrcode'" label="一句话简介">
|
||||||
|
<n-input :value="modelValue.desc" @update:value="patch({ desc: $event })" />
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item v-if="modelValue.type === 'add'" label="按钮图标">
|
||||||
|
<FaIconPicker v-model="addIcon" placeholder="选择图标" />
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item v-if="modelValue.type === 'service_qq'" label="QQ号码">
|
||||||
|
<n-input :value="modelValue.qq" placeholder="仅填写数字" @update:value="patch({ qq: $event })" />
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<n-form-item v-if="modelValue.type === 'service_wechat'" label="微信二维码">
|
||||||
|
<JioMediaField
|
||||||
|
:model-value="modelValue.wechat_img"
|
||||||
|
placeholder="选择二维码图片"
|
||||||
|
@update:model-value="patch({ wechat_img: $event })"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/** 悬浮按钮排序列表中的单个卡片;外层 .jio-sortable__item 由 FloatingButtonsSettings 提供。 */
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import FaIconPicker from '@/components/form/FaIconPicker.vue'
|
||||||
|
import JioMediaField from '@/components/form/JioMediaField.vue'
|
||||||
|
import JioSortHandle from '@/components/form/JioSortHandle.vue'
|
||||||
|
import { collapseHeadAttrs } from '@/components/form/useCollapsibleItems.js'
|
||||||
|
import FloatBtnCommonFields from './FloatBtnCommonFields.vue'
|
||||||
|
import FloatMoreButtonsEditor from './FloatMoreButtonsEditor.vue'
|
||||||
|
import { FLOAT_BTN_META } from './floating-defaults.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Object, required: true },
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const expanded = ref(false)
|
||||||
|
const headAttrs = computed(() => collapseHeadAttrs(expanded.value, () => { expanded.value = !expanded.value }))
|
||||||
|
const meta = computed(() => FLOAT_BTN_META[props.modelValue.type] || { title: props.modelValue.type, panel: '' })
|
||||||
|
|
||||||
|
const addIcon = computed({
|
||||||
|
get: () => props.modelValue.icon || '',
|
||||||
|
set: (value) => patch({ icon: value || '' }),
|
||||||
|
})
|
||||||
|
|
||||||
|
function patch(partial) {
|
||||||
|
emit('update:modelValue', { ...props.modelValue, ...partial })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.float-btn-panel__warn {
|
||||||
|
color: var(--jio-color-warning, #D97706);
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-btn-panel__subtitle {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: var(--jio-color-text);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-btn-panel__extra {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<template>
|
||||||
|
<div class="float-more-editor">
|
||||||
|
<n-collapse :default-expanded-names="[]" accordion>
|
||||||
|
<n-collapse-item
|
||||||
|
v-for="(btn, index) in modelValue"
|
||||||
|
:key="index"
|
||||||
|
:name="index"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<span class="float-more-editor__head">
|
||||||
|
<IconValue v-if="btn.icon" :value="btn.icon" />
|
||||||
|
<span>{{ btn.desc || `按钮 ${index + 1}` }}</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template #header-extra>
|
||||||
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
quaternary
|
||||||
|
type="error"
|
||||||
|
:disabled="modelValue.length <= 1"
|
||||||
|
@click.stop="remove(index)"
|
||||||
|
>
|
||||||
|
<template #icon><i class="fa fas fa-trash-alt" /></template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<n-form label-placement="left" label-width="110" class="jio-form">
|
||||||
|
<n-form-item label="按钮简介">
|
||||||
|
<n-input :value="btn.desc" @update:value="patch(index, { desc: $event })" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="按钮图标">
|
||||||
|
<FaIconPicker :model-value="btn.icon" placeholder="选择图标" @update:model-value="patch(index, { icon: $event || '' })" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item v-if="!btn.hover" label="按钮链接">
|
||||||
|
<div class="float-more-editor__link">
|
||||||
|
<n-input
|
||||||
|
:value="btn.link.url"
|
||||||
|
placeholder="https://"
|
||||||
|
@update:value="patch(index, { link: { ...btn.link, url: $event } })"
|
||||||
|
/>
|
||||||
|
<n-checkbox
|
||||||
|
:checked="btn.link.target === '_blank'"
|
||||||
|
@update:checked="patch(index, { link: { ...btn.link, target: $event ? '_blank' : '' } })"
|
||||||
|
>
|
||||||
|
新窗口打开
|
||||||
|
</n-checkbox>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="悬浮内容">
|
||||||
|
<div class="float-more-editor__block">
|
||||||
|
<div class="float-more-editor__subtitle">鼠标移到此处时显示内容</div>
|
||||||
|
<n-input
|
||||||
|
type="textarea"
|
||||||
|
:value="btn.hover"
|
||||||
|
:autosize="{ minRows: 2, maxRows: 8 }"
|
||||||
|
@update:value="patch(index, { hover: $event })"
|
||||||
|
/>
|
||||||
|
<div class="float-more-editor__desc">设置此处后,按钮链接将失效<br>支持HTML代码,请注意代码规范及标签闭合</div>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item v-if="btn.hover" label="悬浮内容宽度">
|
||||||
|
<div class="float-more-editor__block">
|
||||||
|
<n-input-number
|
||||||
|
:value="btn.hover_width"
|
||||||
|
:min="50"
|
||||||
|
:max="500"
|
||||||
|
:step="10"
|
||||||
|
style="width: 180px;"
|
||||||
|
@update:value="patch(index, { hover_width: $event ?? 200 })"
|
||||||
|
>
|
||||||
|
<template #suffix>PX</template>
|
||||||
|
</n-input-number>
|
||||||
|
<div class="float-more-editor__desc">移动端宽度最大240px</div>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<FloatBtnCommonFields :model-value="btn" @update:model-value="replace(index, $event)" />
|
||||||
|
</n-collapse-item>
|
||||||
|
</n-collapse>
|
||||||
|
|
||||||
|
<div class="float-more-editor__footer">
|
||||||
|
<n-button size="small" type="primary" @click="add">
|
||||||
|
<template #icon><i class="fa fas fa-plus" /></template>
|
||||||
|
添加按钮
|
||||||
|
</n-button>
|
||||||
|
<span class="float-more-editor__desc">此处内容至少保留一个,如一个都无需显示请关闭PC端和移动端显示即可</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import FaIconPicker from '@/components/form/FaIconPicker.vue'
|
||||||
|
import IconValue from '@/components/form/IconValue.vue'
|
||||||
|
import FloatBtnCommonFields from './FloatBtnCommonFields.vue'
|
||||||
|
import { defaultMoreButton } from './floating-defaults.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Array, required: true },
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
function replace(index, value) {
|
||||||
|
const next = props.modelValue.slice()
|
||||||
|
next.splice(index, 1, value)
|
||||||
|
emit('update:modelValue', next)
|
||||||
|
}
|
||||||
|
|
||||||
|
function patch(index, partial) {
|
||||||
|
replace(index, { ...props.modelValue[index], ...partial })
|
||||||
|
}
|
||||||
|
|
||||||
|
function add() {
|
||||||
|
emit('update:modelValue', [...props.modelValue, defaultMoreButton()])
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(index) {
|
||||||
|
if (props.modelValue.length <= 1) return
|
||||||
|
const next = props.modelValue.slice()
|
||||||
|
next.splice(index, 1)
|
||||||
|
emit('update:modelValue', next)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.float-more-editor__head {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-more-editor__link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-more-editor__block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-more-editor__subtitle,
|
||||||
|
.float-more-editor__desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--n-text-color-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-more-editor__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* 右侧悬浮按钮:类型元数据与默认值(与 src/Settings/FloatButtons.php 保持一致)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const emptyColor = () => ({ color: '', bg: '' })
|
||||||
|
|
||||||
|
/** 类型元数据:面板标题、折叠标题、黄色提示。 */
|
||||||
|
export const FLOAT_BTN_META = {
|
||||||
|
back_top: { title: '返回顶部', panel: '返回顶部', warn: '此按钮只能放置在最顶部或者最底部!' },
|
||||||
|
qrcode: { title: '当前页面二维码', panel: '当前页面二维码' },
|
||||||
|
add: { title: 'Add发布内容', panel: '发布投稿', warn: '此按钮尽量放在上方,否则会显示遮挡' },
|
||||||
|
theme_mode: { title: '日夜主题切换', panel: '日夜主题切换' },
|
||||||
|
pay_vip: { title: 'VIP购买', panel: 'VIP购买' },
|
||||||
|
service_qq: { title: 'QQ客服', panel: 'QQ客服' },
|
||||||
|
service_wechat: { title: '微信客服', panel: '微信客服' },
|
||||||
|
more: { title: '更多按钮', panel: '添加更多自定义按钮' },
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 「更多按钮」出厂默认项:不预设任何第三方指向,只给一个图标按钮。 */
|
||||||
|
export function giftMoreButton() {
|
||||||
|
return {
|
||||||
|
desc: '',
|
||||||
|
icon: 'jiosvg-gift-color',
|
||||||
|
link: { url: '', target: '' },
|
||||||
|
hover: '',
|
||||||
|
hover_width: 200,
|
||||||
|
pc_s: true,
|
||||||
|
m_s: true,
|
||||||
|
color: { color: '', bg: 'rgba(255, 111, 6, 0.2)' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 「添加按钮」新增项默认值。 */
|
||||||
|
export function defaultMoreButton() {
|
||||||
|
return {
|
||||||
|
desc: '',
|
||||||
|
icon: 'fa fas fa-heart',
|
||||||
|
link: { url: '', target: '' },
|
||||||
|
hover: '',
|
||||||
|
hover_width: 200,
|
||||||
|
pc_s: true,
|
||||||
|
m_s: true,
|
||||||
|
color: { color: '', bg: '' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultItems() {
|
||||||
|
return [
|
||||||
|
{ type: 'more', buttons: [giftMoreButton()] },
|
||||||
|
{ type: 'pay_vip', pc_s: true, m_s: true, color: { color: '#f2c97d', bg: 'rgba(62,62,67,0.9)' } },
|
||||||
|
{ type: 'add', pc_s: true, m_s: false, color: emptyColor(), icon: 'fa fas fa-pencil-alt' },
|
||||||
|
{ type: 'service_qq', pc_s: false, m_s: false, color: emptyColor(), qq: '' },
|
||||||
|
{ type: 'service_wechat', pc_s: true, m_s: true, color: emptyColor(), wechat_img: '' },
|
||||||
|
{ type: 'theme_mode', pc_s: false, m_s: false, color: emptyColor() },
|
||||||
|
{ type: 'qrcode', pc_s: true, m_s: false, color: emptyColor(), desc: '在手机上浏览此页面' },
|
||||||
|
{ type: 'back_top', pc_s: true, m_s: true, color: emptyColor() },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultSettings() {
|
||||||
|
return {
|
||||||
|
style: 'round',
|
||||||
|
position: 'bottom',
|
||||||
|
filter_blur: ['m_s'],
|
||||||
|
scroll_hide: false,
|
||||||
|
scroll_down_hide: true,
|
||||||
|
items: defaultItems(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 规范化颜色组:确保 color/bg 都为字符串。 */
|
||||||
|
export function normalizeColor(color) {
|
||||||
|
const c = color && typeof color === 'object' ? color : {}
|
||||||
|
return { color: c.color || '', bg: c.bg || '' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用默认值补齐服务端返回的按钮(按服务端顺序)。 */
|
||||||
|
export function normalizeItems(items) {
|
||||||
|
const defaults = Object.fromEntries(defaultItems().map((item) => [item.type, item]))
|
||||||
|
const source = Array.isArray(items) && items.length ? items : defaultItems()
|
||||||
|
const out = []
|
||||||
|
const seen = new Set()
|
||||||
|
for (const raw of source) {
|
||||||
|
const base = defaults[raw?.type]
|
||||||
|
if (!base || seen.has(raw.type)) continue
|
||||||
|
seen.add(raw.type)
|
||||||
|
if (raw.type === 'more') {
|
||||||
|
const buttons = (Array.isArray(raw.buttons) && raw.buttons.length ? raw.buttons : base.buttons).map((btn) => ({
|
||||||
|
...defaultMoreButton(),
|
||||||
|
...btn,
|
||||||
|
link: { url: btn?.link?.url || '', target: btn?.link?.target || '' },
|
||||||
|
color: normalizeColor(btn?.color),
|
||||||
|
}))
|
||||||
|
out.push({ type: 'more', buttons })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out.push({ ...base, ...raw, color: normalizeColor(raw.color) })
|
||||||
|
}
|
||||||
|
for (const item of defaultItems()) {
|
||||||
|
if (!seen.has(item.type)) out.push(item)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="aksrt-array-table">
|
|
||||||
<n-data-table
|
|
||||||
:columns="tableColumns"
|
|
||||||
:data="modelValue || []"
|
|
||||||
:bordered="false"
|
|
||||||
size="small"
|
|
||||||
max-height="400"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<n-button
|
|
||||||
size="small"
|
|
||||||
dashed
|
|
||||||
block
|
|
||||||
class="aksrt-array-table__add"
|
|
||||||
@click="addItem"
|
|
||||||
>
|
|
||||||
<FaIcon icon="fa fas fa-plus" :size="12" />
|
|
||||||
{{ addText || '添加行' }}
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
/**
|
|
||||||
* AksArrayTable — table-style editor for array data.
|
|
||||||
* Renders rows in a data table with inline editing.
|
|
||||||
*/
|
|
||||||
import { computed, h } from 'vue'
|
|
||||||
import { NInput, NSwitch, NButton, NSelect, NInputNumber } from 'naive-ui'
|
|
||||||
import FaIcon from './FaIcon.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: { type: Array, default: () => [] },
|
|
||||||
/** Column definitions: [{ key, label, type?, options?, width? }] */
|
|
||||||
columns: { type: Array, required: true },
|
|
||||||
defaults: { type: Object, default: () => ({}) },
|
|
||||||
addText: { type: String, default: '' },
|
|
||||||
maxRows: { type: Number, default: Infinity },
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
|
||||||
|
|
||||||
function updateCell(rowIndex, key, value) {
|
|
||||||
const list = [...(props.modelValue || [])]
|
|
||||||
list[rowIndex] = { ...list[rowIndex], [key]: value }
|
|
||||||
emit('update:modelValue', list)
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeItem(index) {
|
|
||||||
const list = (props.modelValue || []).filter((_, i) => i !== index)
|
|
||||||
emit('update:modelValue', list)
|
|
||||||
}
|
|
||||||
|
|
||||||
function addItem() {
|
|
||||||
if ((props.modelValue || []).length >= props.maxRows) return
|
|
||||||
const item = {}
|
|
||||||
for (const col of props.columns) {
|
|
||||||
item[col.key] = col.default ?? (col.type === 'switch' ? false : '')
|
|
||||||
}
|
|
||||||
emit('update:modelValue', [...(props.modelValue || []), { ...item, ...props.defaults }])
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveUp(index) {
|
|
||||||
if (index <= 0) return
|
|
||||||
const list = [...(props.modelValue || [])]
|
|
||||||
;[list[index - 1], list[index]] = [list[index], list[index - 1]]
|
|
||||||
emit('update:modelValue', list)
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveDown(index) {
|
|
||||||
const list = [...(props.modelValue || [])]
|
|
||||||
if (index >= list.length - 1) return
|
|
||||||
;[list[index], list[index + 1]] = [list[index + 1], list[index]]
|
|
||||||
emit('update:modelValue', list)
|
|
||||||
}
|
|
||||||
|
|
||||||
const tableColumns = computed(() => {
|
|
||||||
const cols = props.columns.map(col => ({
|
|
||||||
title: col.label,
|
|
||||||
key: col.key,
|
|
||||||
width: col.width || 150,
|
|
||||||
render(row, rowIndex) {
|
|
||||||
const val = row[col.key]
|
|
||||||
const update = (v) => updateCell(rowIndex, col.key, v)
|
|
||||||
|
|
||||||
if (col.type === 'switch') {
|
|
||||||
return h(NSwitch, { value: val, size: 'small', 'onUpdate:value': update })
|
|
||||||
}
|
|
||||||
if (col.type === 'select') {
|
|
||||||
return h(NSelect, { value: val, options: col.options || [], size: 'small', 'onUpdate:value': update })
|
|
||||||
}
|
|
||||||
if (col.type === 'number') {
|
|
||||||
return h(NInputNumber, { value: val, size: 'small', style: 'width:100%', 'onUpdate:value': update })
|
|
||||||
}
|
|
||||||
return h(NInput, { value: val, size: 'small', 'onUpdate:value': update })
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Add actions column
|
|
||||||
cols.push({
|
|
||||||
title: '操作',
|
|
||||||
key: '_actions',
|
|
||||||
width: 100,
|
|
||||||
render(row, rowIndex) {
|
|
||||||
return h('div', { style: 'display:flex;gap:2px' }, [
|
|
||||||
rowIndex > 0 ? h(NButton, { size: 'tiny', quaternary: true, onClick: () => moveUp(rowIndex) }, {
|
|
||||||
default: () => h(FaIcon, { icon: 'fa fas fa-arrow-up', size: 12 }),
|
|
||||||
}) : null,
|
|
||||||
rowIndex < (props.modelValue?.length || 0) - 1 ? h(NButton, { size: 'tiny', quaternary: true, onClick: () => moveDown(rowIndex) }, {
|
|
||||||
default: () => h(FaIcon, { icon: 'fa fas fa-arrow-down', size: 12 }),
|
|
||||||
}) : null,
|
|
||||||
h(NButton, { size: 'tiny', quaternary: true, type: 'error', onClick: () => removeItem(rowIndex) }, {
|
|
||||||
default: () => h(FaIcon, { icon: 'fa fas fa-trash-alt', size: 12 }),
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return cols
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.aksrt-array-table__add {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,185 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="aksrt-repeater">
|
|
||||||
<div v-if="modelValue && modelValue.length" class="aksrt-repeater__list">
|
|
||||||
<div
|
|
||||||
v-for="(item, index) in modelValue"
|
|
||||||
:key="index"
|
|
||||||
class="aksrt-repeater__item"
|
|
||||||
>
|
|
||||||
<div class="aksrt-repeater__item-header">
|
|
||||||
<span class="aksrt-repeater__item-index">#{{ index + 1 }}</span>
|
|
||||||
<div class="aksrt-repeater__item-actions">
|
|
||||||
<n-button
|
|
||||||
v-if="index > 0"
|
|
||||||
size="tiny"
|
|
||||||
quaternary
|
|
||||||
@click="moveUp(index)"
|
|
||||||
>
|
|
||||||
<FaIcon icon="fa fas fa-arrow-up" :size="12" />
|
|
||||||
</n-button>
|
|
||||||
<n-button
|
|
||||||
v-if="index < modelValue.length - 1"
|
|
||||||
size="tiny"
|
|
||||||
quaternary
|
|
||||||
@click="moveDown(index)"
|
|
||||||
>
|
|
||||||
<FaIcon icon="fa fas fa-arrow-down" :size="12" />
|
|
||||||
</n-button>
|
|
||||||
<n-button
|
|
||||||
size="tiny"
|
|
||||||
quaternary
|
|
||||||
type="error"
|
|
||||||
@click="removeItem(index)"
|
|
||||||
>
|
|
||||||
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-repeater__item-fields">
|
|
||||||
<AksField
|
|
||||||
v-for="field in fields"
|
|
||||||
:key="field.key"
|
|
||||||
:schema="field"
|
|
||||||
:model-value="item[field.key]"
|
|
||||||
@update:model-value="updateField(index, field.key, $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="aksrt-repeater__empty">
|
|
||||||
<FaIcon icon="fa fas fa-inbox" :size="20" />
|
|
||||||
<span>{{ emptyText || '暂无数据' }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<n-button
|
|
||||||
size="small"
|
|
||||||
dashed
|
|
||||||
block
|
|
||||||
class="aksrt-repeater__add"
|
|
||||||
@click="addItem"
|
|
||||||
>
|
|
||||||
<FaIcon icon="fa fas fa-plus" :size="12" />
|
|
||||||
{{ addText || '添加项目' }}
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
/**
|
|
||||||
* AksRepeaterField — dynamic add/remove rows of structured data.
|
|
||||||
* Each row renders fields based on the provided field schemas.
|
|
||||||
*/
|
|
||||||
import AksField from './AksField.vue'
|
|
||||||
import FaIcon from './FaIcon.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
modelValue: { type: Array, default: () => [] },
|
|
||||||
/** Array of field schemas for each row */
|
|
||||||
fields: { type: Array, required: true },
|
|
||||||
/** Default value for a new item */
|
|
||||||
defaults: { type: Object, default: () => ({}) },
|
|
||||||
emptyText: { type: String, default: '' },
|
|
||||||
addText: { type: String, default: '' },
|
|
||||||
maxItems: { type: Number, default: Infinity },
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
|
||||||
|
|
||||||
function createItem() {
|
|
||||||
const item = {}
|
|
||||||
for (const field of props.fields) {
|
|
||||||
item[field.key] = field.default ?? (field.type === 'switch' ? false : '')
|
|
||||||
}
|
|
||||||
return { ...item, ...props.defaults }
|
|
||||||
}
|
|
||||||
|
|
||||||
function addItem() {
|
|
||||||
if (props.modelValue.length >= props.maxItems) return
|
|
||||||
const list = [...props.modelValue, createItem()]
|
|
||||||
emit('update:modelValue', list)
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeItem(index) {
|
|
||||||
const list = props.modelValue.filter((_, i) => i !== index)
|
|
||||||
emit('update:modelValue', list)
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveUp(index) {
|
|
||||||
if (index <= 0) return
|
|
||||||
const list = [...props.modelValue]
|
|
||||||
;[list[index - 1], list[index]] = [list[index], list[index - 1]]
|
|
||||||
emit('update:modelValue', list)
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveDown(index) {
|
|
||||||
if (index >= props.modelValue.length - 1) return
|
|
||||||
const list = [...props.modelValue]
|
|
||||||
;[list[index], list[index + 1]] = [list[index + 1], list[index]]
|
|
||||||
emit('update:modelValue', list)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateField(index, key, value) {
|
|
||||||
const list = [...props.modelValue]
|
|
||||||
list[index] = { ...list[index], [key]: value }
|
|
||||||
emit('update:modelValue', list)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.aksrt-repeater__list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-repeater__item {
|
|
||||||
border: 1px solid var(--aksrt-color-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
background: var(--aksrt-color-canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-repeater__item-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-repeater__item-index {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--aksrt-color-primary);
|
|
||||||
background: var(--aksrt-color-primary-soft);
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-repeater__item-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-repeater__item-fields {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-repeater__empty {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 24px;
|
|
||||||
color: var(--aksrt-color-border-strong);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-repeater__add {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-fa-icon-picker">
|
<div class="jio-fa-icon-picker">
|
||||||
<n-popover v-model:show="popoverShow" trigger="click" placement="bottom-start" :width="460" :style="{ padding: 0 }">
|
<n-popover v-model:show="popoverShow" trigger="click" placement="bottom-start" :width="460" :style="{ padding: 0 }">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-input
|
<n-input
|
||||||
@@ -19,8 +19,8 @@
|
|||||||
</n-input>
|
</n-input>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="aksrt-icon-picker__panel" @click="onPanelClick">
|
<div class="jio-icon-picker__panel" @click="onPanelClick">
|
||||||
<div class="aksrt-icon-picker__search">
|
<div class="jio-icon-picker__search">
|
||||||
<n-input v-model:value="search" placeholder="搜索图标..." size="small" clearable>
|
<n-input v-model:value="search" placeholder="搜索图标..." size="small" clearable>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<FaIcon icon="fa fas fa-search" :size="12" />
|
<FaIcon icon="fa fas fa-search" :size="12" />
|
||||||
@@ -28,12 +28,12 @@
|
|||||||
</n-input>
|
</n-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="aksrt-icon-picker__tabs">
|
<div class="jio-icon-picker__tabs">
|
||||||
<button
|
<button
|
||||||
v-for="tab in tabs"
|
v-for="tab in tabs"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
type="button"
|
type="button"
|
||||||
class="aksrt-icon-picker__tab"
|
class="jio-icon-picker__tab"
|
||||||
:class="{ 'is-active': activeTab === tab.id }"
|
:class="{ 'is-active': activeTab === tab.id }"
|
||||||
@click="activeTab = tab.id"
|
@click="activeTab = tab.id"
|
||||||
>
|
>
|
||||||
@@ -42,23 +42,23 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Custom SVG tab -->
|
<!-- Custom SVG tab -->
|
||||||
<div v-if="activeTab === 'custom'" class="aksrt-icon-picker__custom">
|
<div v-if="activeTab === 'custom'" class="jio-icon-picker__custom">
|
||||||
<n-input v-model:value="customCode" type="textarea" placeholder='<svg viewBox="0 0 1024 1024"><path d="…"></path></svg>' :rows="4" />
|
<n-input v-model:value="customCode" type="textarea" placeholder='<svg viewBox="0 0 1024 1024"><path d="…"></path></svg>' :rows="4" />
|
||||||
<div class="aksrt-icon-picker__custom-actions">
|
<div class="jio-icon-picker__custom-actions">
|
||||||
<span class="aksrt-icon-picker__custom-preview" v-html="customPreview"></span>
|
<span class="jio-icon-picker__custom-preview" v-html="customPreview"></span>
|
||||||
<n-button size="small" type="primary" :disabled="!customPreview" @click="insertCustom">
|
<n-button size="small" type="primary" :disabled="!customPreview" @click="insertCustom">
|
||||||
确认插入
|
确认插入
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="sessionCustom.length"
|
v-if="sessionCustom.length"
|
||||||
class="aksrt-icon-picker__grid aksrt-icon-picker__grid--session"
|
class="jio-icon-picker__grid jio-icon-picker__grid--session"
|
||||||
data-aksrt-icon-grid="custom"
|
data-jio-icon-grid="custom"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(svg, i) in sessionCustom"
|
v-for="(svg, i) in sessionCustom"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="aksrt-icon-picker__item"
|
class="jio-icon-picker__item"
|
||||||
:class="{ 'is-active': modelValue === `customsvg-${svg}` }"
|
:class="{ 'is-active': modelValue === `customsvg-${svg}` }"
|
||||||
title="customsvg"
|
title="customsvg"
|
||||||
:data-icon="`customsvg-${svg}`"
|
:data-icon="`customsvg-${svg}`"
|
||||||
@@ -68,11 +68,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Catalog tab -->
|
<!-- Catalog tab -->
|
||||||
<div v-else class="aksrt-icon-picker__grid" data-aksrt-icon-grid="catalog">
|
<div v-else class="jio-icon-picker__grid" data-jio-icon-grid="catalog">
|
||||||
<div
|
<div
|
||||||
v-for="icon in filteredIcons"
|
v-for="icon in filteredIcons"
|
||||||
:key="icon"
|
:key="icon"
|
||||||
class="aksrt-icon-picker__item"
|
class="jio-icon-picker__item"
|
||||||
:class="{ 'is-active': modelValue === composeIconValue(activeGroup, icon) }"
|
:class="{ 'is-active': modelValue === composeIconValue(activeGroup, icon) }"
|
||||||
:title="icon"
|
:title="icon"
|
||||||
:data-icon="composeIconValue(activeGroup, icon)"
|
:data-icon="composeIconValue(activeGroup, icon)"
|
||||||
@@ -80,11 +80,11 @@
|
|||||||
<i v-if="activeGroup.type === 'fa'" :class="[activeTab, 'fa-' + icon]"></i>
|
<i v-if="activeGroup.type === 'fa'" :class="[activeTab, 'fa-' + icon]"></i>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="aksrt-icon-picker__preview-svg"
|
class="jio-icon-picker__preview-svg"
|
||||||
v-html="zibsvgMarkup(`zibsvg-${icon}`)"
|
v-html="jiosvgMarkup(`jiosvg-${icon}`)"
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="filteredIcons.length === 0" class="aksrt-icon-picker__empty">
|
<div v-if="filteredIcons.length === 0" class="jio-icon-picker__empty">
|
||||||
未找到匹配图标
|
未找到匹配图标
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
/**
|
/**
|
||||||
* FaIconPicker — universal icon picker backed by the server catalog
|
* FaIconPicker — universal icon picker backed by the server catalog
|
||||||
* (GET /aksrt/v1/icons/catalog): full FA5 free set, the theme's built-in
|
* (GET /jio/v1/icons/catalog): full FA5 free set, the theme's built-in
|
||||||
* SVG sprite keys and inline custom SVG values.
|
* SVG sprite keys and inline custom SVG values.
|
||||||
*/
|
*/
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
@@ -105,7 +105,7 @@ import IconValue from './IconValue.vue'
|
|||||||
import api from '@/api/index.js'
|
import api from '@/api/index.js'
|
||||||
import {
|
import {
|
||||||
cleanSvg,
|
cleanSvg,
|
||||||
zibsvgMarkup,
|
jiosvgMarkup,
|
||||||
composeIconValue,
|
composeIconValue,
|
||||||
} from '@/utils/icons.js'
|
} from '@/utils/icons.js'
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ const FALLBACK_TABS = [
|
|||||||
{ id: 'fas', title: 'Solid', type: 'fa', icons: [] },
|
{ id: 'fas', title: 'Solid', type: 'fa', icons: [] },
|
||||||
{ id: 'far', title: 'Regular', type: 'fa', icons: [] },
|
{ id: 'far', title: 'Regular', type: 'fa', icons: [] },
|
||||||
{ id: 'fab', title: 'Brands', type: 'fa', icons: [] },
|
{ id: 'fab', title: 'Brands', type: 'fa', icons: [] },
|
||||||
{ id: 'zibsvg', title: '内置 SVG', type: 'svg', icons: [] },
|
{ id: 'jiosvg', title: '内置 SVG', type: 'svg', icons: [] },
|
||||||
{ id: 'custom', title: '自定义', type: 'custom', icons: [] },
|
{ id: 'custom', title: '自定义', type: 'custom', icons: [] },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -176,53 +176,53 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-icon-picker__panel {
|
.jio-icon-picker__panel {
|
||||||
/* The popover teleports to <body>, outside #aksrt-admin-app where the
|
/* The popover teleports to <body>, outside #jio-admin-app where the
|
||||||
palette is declared — re-declare every token the panel consumes. */
|
palette is declared — re-declare every token the panel consumes. */
|
||||||
--aksrt-color-primary: #18A058;
|
--jio-color-primary: #18A058;
|
||||||
--aksrt-color-primary-hover: #36AD6A;
|
--jio-color-primary-hover: #36AD6A;
|
||||||
--aksrt-color-primary-soft: rgba(24, 160, 88, 0.12);
|
--jio-color-primary-soft: rgba(24, 160, 88, 0.12);
|
||||||
--aksrt-color-text-secondary: #465149;
|
--jio-color-text-secondary: #465149;
|
||||||
--aksrt-color-muted: #6F7A72;
|
--jio-color-muted: #6F7A72;
|
||||||
--aksrt-color-border: #E2E8E3;
|
--jio-color-border: #E2E8E3;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__search {
|
.jio-icon-picker__search {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__tabs {
|
.jio-icon-picker__tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__tab {
|
.jio-icon-picker__tab {
|
||||||
padding: 3px 10px;
|
padding: 3px 10px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
border: 1px solid var(--aksrt-color-border);
|
border: 1px solid var(--jio-color-border);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--aksrt-color-text-secondary);
|
color: var(--jio-color-text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__tab:hover {
|
.jio-icon-picker__tab:hover {
|
||||||
border-color: var(--aksrt-color-primary);
|
border-color: var(--jio-color-primary);
|
||||||
color: var(--aksrt-color-primary);
|
color: var(--jio-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__tab.is-active {
|
.jio-icon-picker__tab.is-active {
|
||||||
border-color: var(--aksrt-color-primary);
|
border-color: var(--jio-color-primary);
|
||||||
background: var(--aksrt-color-primary);
|
background: var(--jio-color-primary);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__grid {
|
.jio-icon-picker__grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(8, 1fr);
|
grid-template-columns: repeat(8, 1fr);
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
@@ -230,7 +230,7 @@ onMounted(async () => {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__item {
|
.jio-icon-picker__item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -239,60 +239,60 @@ onMounted(async () => {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--aksrt-color-text-secondary);
|
color: var(--jio-color-text-secondary);
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__item:hover {
|
.jio-icon-picker__item:hover {
|
||||||
background: var(--aksrt-color-primary-soft);
|
background: var(--jio-color-primary-soft);
|
||||||
color: var(--aksrt-color-primary);
|
color: var(--jio-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__item.is-active {
|
.jio-icon-picker__item.is-active {
|
||||||
background: var(--aksrt-color-primary);
|
background: var(--jio-color-primary);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__item :deep(svg),
|
.jio-icon-picker__item :deep(svg),
|
||||||
.aksrt-icon-picker__preview-svg :deep(svg),
|
.jio-icon-picker__preview-svg :deep(svg),
|
||||||
.aksrt-icon-picker__preview-svg svg {
|
.jio-icon-picker__preview-svg svg {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__preview-svg {
|
.jio-icon-picker__preview-svg {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__empty {
|
.jio-icon-picker__empty {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__custom-actions {
|
.jio-icon-picker__custom-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__custom-preview {
|
.jio-icon-picker__custom-preview {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border: 1px dashed var(--aksrt-color-muted);
|
border: 1px dashed var(--jio-color-muted);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-picker__grid--session {
|
.jio-icon-picker__grid--session {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
border-top: 1px solid var(--aksrt-color-muted);
|
border-top: 1px solid var(--jio-color-muted);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -3,25 +3,25 @@
|
|||||||
<i v-if="isFaValue(value)" :class="faClass" aria-hidden="true"></i>
|
<i v-if="isFaValue(value)" :class="faClass" aria-hidden="true"></i>
|
||||||
<!-- Built-in sprite value -->
|
<!-- Built-in sprite value -->
|
||||||
<span
|
<span
|
||||||
v-else-if="isZibsvgValue(value)"
|
v-else-if="isJiosvgValue(value)"
|
||||||
class="aksrt-icon-value__svg"
|
class="jio-icon-value__svg"
|
||||||
v-html="zibsvgMarkup(value)"
|
v-html="jiosvgMarkup(value)"
|
||||||
></span>
|
></span>
|
||||||
<!-- Inline custom SVG value -->
|
<!-- Inline custom SVG value -->
|
||||||
<span
|
<span
|
||||||
v-else-if="isCustomSvgValue(value)"
|
v-else-if="isCustomSvgValue(value)"
|
||||||
class="aksrt-icon-value__svg"
|
class="jio-icon-value__svg"
|
||||||
v-html="cleanSvg(value.slice(10))"
|
v-html="cleanSvg(value.slice(10))"
|
||||||
></span>
|
></span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
/**
|
/**
|
||||||
* IconValue — renders any icon-picker value ("fa fas fa-star", "zibsvg-key",
|
* IconValue — renders any icon-picker value ("fa fas fa-star", "jiosvg-key",
|
||||||
* "customsvg-<svg>") as inline preview markup.
|
* "customsvg-<svg>") as inline preview markup.
|
||||||
*/
|
*/
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { cleanSvg, isFaValue, isZibsvgValue, isCustomSvgValue, zibsvgMarkup } from '@/utils/icons.js'
|
import { cleanSvg, isFaValue, isJiosvgValue, isCustomSvgValue, jiosvgMarkup } from '@/utils/icons.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
value: { type: String, default: '' },
|
value: { type: String, default: '' },
|
||||||
@@ -35,12 +35,12 @@ const faClass = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-icon-value__svg {
|
.jio-icon-value__svg {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-icon-value__svg :deep(svg),
|
.jio-icon-value__svg :deep(svg),
|
||||||
.aksrt-icon-value__svg svg {
|
.jio-icon-value__svg svg {
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
|
|||||||
+5
-5
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="editorRef" class="aks-code-editor" :class="{ 'is-disabled': disabled }" />
|
<div ref="editorRef" class="jio-code-editor" :class="{ 'is-disabled': disabled }" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -86,19 +86,19 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aks-code-editor {
|
.jio-code-editor {
|
||||||
border: 1px solid var(--n-border-color, #e0e0e6);
|
border: 1px solid var(--n-border-color, #e0e0e6);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: calc(var(--rows, 8) * 22px);
|
min-height: calc(var(--rows, 8) * 22px);
|
||||||
}
|
}
|
||||||
.aks-code-editor :deep(.cm-editor) {
|
.jio-code-editor :deep(.cm-editor) {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
.aks-code-editor :deep(.cm-editor.cm-focused) {
|
.jio-code-editor :deep(.cm-editor.cm-focused) {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
.aks-code-editor.is-disabled {
|
.jio-code-editor.is-disabled {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<n-color-picker
|
||||||
|
:value="pickerValue"
|
||||||
|
:modes="modes"
|
||||||
|
:show-alpha="alpha"
|
||||||
|
:swatches="swatches"
|
||||||
|
:actions="clearable ? ['clear'] : undefined"
|
||||||
|
:disabled="disabled"
|
||||||
|
@update:value="onUpdate"
|
||||||
|
>
|
||||||
|
<template v-if="$slots.trigger" #trigger="slotProps">
|
||||||
|
<slot name="trigger" v-bind="slotProps" />
|
||||||
|
</template>
|
||||||
|
</n-color-picker>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 后台唯一取色器:所有颜色字段都经由此组件,输出与
|
||||||
|
* PHP `Jio\Core\Color::sanitize($value, $allow_alpha)` 同一规则的规范值。
|
||||||
|
* 清空 / 非法值统一回传空串。
|
||||||
|
*/
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { normalizeColor } from '@/utils/color.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: String, default: '' },
|
||||||
|
alpha: { type: Boolean, default: false },
|
||||||
|
clearable: { type: Boolean, default: false },
|
||||||
|
swatches: { type: Array, default: undefined },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const modes = computed(() => (props.alpha ? ['hex', 'rgb'] : ['hex']))
|
||||||
|
const pickerValue = computed(() => normalizeColor(props.modelValue, props.alpha) || null)
|
||||||
|
|
||||||
|
function onUpdate(value) {
|
||||||
|
emit('update:modelValue', normalizeColor(value, props.alpha))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Between number (min/max pair) -->
|
||||||
|
<div class="jio-composite jio-composite--between">
|
||||||
|
<n-input-number
|
||||||
|
:value="betweenValue('min')"
|
||||||
|
:min="schema.min"
|
||||||
|
:max="schema.max"
|
||||||
|
:step="schema.step || 1"
|
||||||
|
:placeholder="schema.minPlaceholder || '最小'"
|
||||||
|
:disabled="schema.disabled"
|
||||||
|
style="flex: 1"
|
||||||
|
@update:value="update('min', $event)"
|
||||||
|
/>
|
||||||
|
<span class="jio-composite__sep">—</span>
|
||||||
|
<n-input-number
|
||||||
|
:value="betweenValue('max')"
|
||||||
|
:min="schema.min"
|
||||||
|
:max="schema.max"
|
||||||
|
:step="schema.step || 1"
|
||||||
|
:placeholder="schema.maxPlaceholder || '最大'"
|
||||||
|
:disabled="schema.disabled"
|
||||||
|
style="flex: 1"
|
||||||
|
@update:value="update('max', $event)"
|
||||||
|
/>
|
||||||
|
<span v-if="schema.unit" class="jio-composite__unit">{{ schema.unit }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/** 对象型复合字段:between_number(min / max 数值区间)。 */
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
schema: { type: Object, required: true },
|
||||||
|
modelValue: { default: null },
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const current = computed(() => (
|
||||||
|
props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
|
||||||
|
? props.modelValue
|
||||||
|
: {}
|
||||||
|
))
|
||||||
|
|
||||||
|
function update(part, value) {
|
||||||
|
emit('update:modelValue', { ...current.value, [part]: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
function betweenValue(part) {
|
||||||
|
return current.value[part] !== undefined ? current.value[part] : (props.schema.default?.[part] ?? 0)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.jio-composite {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-composite--between {
|
||||||
|
max-width: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-composite__sep {
|
||||||
|
color: var(--jio-color-border-strong);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-composite__unit {
|
||||||
|
color: var(--jio-color-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large.
Load diff
+44
-45
@@ -1,26 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-field-group" :class="{ 'is-collapsible': collapsible, 'is-collapsed': collapsible && collapsed }">
|
<div class="jio-field-group" :class="{ 'is-collapsible': collapsible, 'is-collapsed': collapsible && collapsed }">
|
||||||
<div
|
<div
|
||||||
v-if="title"
|
v-if="title"
|
||||||
class="aksrt-field-group__header"
|
class="jio-field-group__header"
|
||||||
:class="{ 'is-clickable': collapsible }"
|
:class="{ 'is-clickable': collapsible }"
|
||||||
@click="collapsible ? toggleCollapse() : undefined"
|
@click="collapsible ? toggleCollapse() : undefined"
|
||||||
>
|
>
|
||||||
<div class="aksrt-field-group__header-left">
|
<div class="jio-field-group__header-left">
|
||||||
<FaIcon v-if="icon" :icon="icon" :size="16" class="aksrt-field-group__icon" />
|
<FaIcon v-if="icon" :icon="icon" :size="16" class="jio-field-group__icon" />
|
||||||
<div>
|
<div>
|
||||||
<h3 class="aksrt-field-group__title">{{ title }}</h3>
|
<h3 class="jio-field-group__title">{{ title }}</h3>
|
||||||
<p v-if="description" class="aksrt-field-group__desc">{{ description }}</p>
|
<p v-if="description" class="jio-field-group__desc">{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="aksrt-field-group__header-right">
|
<div class="jio-field-group__header-right">
|
||||||
<div v-if="$slots.actions" class="aksrt-field-group__actions" @click.stop>
|
<div v-if="$slots.actions" class="jio-field-group__actions" @click.stop>
|
||||||
<slot name="actions" />
|
<slot name="actions" />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="collapsible"
|
v-if="collapsible"
|
||||||
type="button"
|
type="button"
|
||||||
class="aksrt-field-group__collapse-btn"
|
class="jio-field-group__collapse-btn"
|
||||||
:aria-expanded="(!collapsed).toString()"
|
:aria-expanded="(!collapsed).toString()"
|
||||||
:title="collapsed ? '展开配置' : '收起配置'"
|
:title="collapsed ? '展开配置' : '收起配置'"
|
||||||
@click.stop="toggleCollapse"
|
@click.stop="toggleCollapse"
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<transition name="aksrt-collapse">
|
<transition name="jio-collapse">
|
||||||
<div v-show="!collapsed" class="aksrt-field-group__body">
|
<div v-show="!collapsed" class="jio-field-group__body">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -44,11 +44,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
/**
|
/**
|
||||||
* AksFieldGroup — card-style container for grouping related form fields.
|
* JioFieldGroup — card-style container for grouping related form fields.
|
||||||
* Supports collapsible sections, icon headers, and description text.
|
* Supports collapsible sections, icon headers, and description text.
|
||||||
*
|
*
|
||||||
* defaultCollapsed only seeds initial state. Parent should remount the group
|
* defaultCollapsed only seeds the initial state; afterwards the user toggles freely.
|
||||||
* (e.g. via :key when a channel turns on) if it wants to force expand again.
|
|
||||||
*/
|
*/
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import FaIcon from './FaIcon.vue'
|
import FaIcon from './FaIcon.vue'
|
||||||
@@ -70,16 +69,16 @@ function toggleCollapse() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-field-group {
|
.jio-field-group {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
border: 1px solid var(--aksrt-color-border);
|
border: 1px solid var(--jio-color-border);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: var(--aksrt-color-surface);
|
background: var(--jio-color-surface);
|
||||||
box-shadow: var(--aksrt-shadow-card);
|
box-shadow: var(--jio-shadow-card);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-field-group__header {
|
.jio-field-group__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -87,66 +86,66 @@ function toggleCollapse() {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-field-group__header.is-clickable {
|
.jio-field-group__header.is-clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-field-group__header.is-clickable:hover .aksrt-field-group__title {
|
.jio-field-group__header.is-clickable:hover .jio-field-group__title {
|
||||||
color: var(--aksrt-color-primary);
|
color: var(--jio-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-field-group.is-collapsed .aksrt-field-group__header {
|
.jio-field-group.is-collapsed .jio-field-group__header {
|
||||||
padding-bottom: 18px;
|
padding-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-field-group__header-left {
|
.jio-field-group__header-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-field-group__header-right {
|
.jio-field-group__header-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-field-group__icon {
|
.jio-field-group__icon {
|
||||||
color: var(--aksrt-color-primary);
|
color: var(--jio-color-primary);
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-field-group__title {
|
.jio-field-group__title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--aksrt-color-text);
|
color: var(--jio-color-text);
|
||||||
transition: color 0.15s;
|
transition: color 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-field-group__desc {
|
.jio-field-group__desc {
|
||||||
margin: 4px 0 0;
|
margin: 4px 0 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-field-group__actions {
|
.jio-field-group__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-field-group__collapse-btn {
|
.jio-field-group__collapse-btn {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
border: 1px solid var(--aksrt-color-border);
|
border: 1px solid var(--jio-color-border);
|
||||||
background: var(--aksrt-color-canvas);
|
background: var(--jio-color-canvas);
|
||||||
color: var(--aksrt-color-text-secondary);
|
color: var(--jio-color-text-secondary);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -154,25 +153,25 @@ function toggleCollapse() {
|
|||||||
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-field-group__collapse-btn:hover {
|
.jio-field-group__collapse-btn:hover {
|
||||||
color: var(--aksrt-color-primary);
|
color: var(--jio-color-primary);
|
||||||
border-color: var(--aksrt-color-primary);
|
border-color: var(--jio-color-primary);
|
||||||
background: var(--aksrt-color-primary-soft);
|
background: var(--jio-color-primary-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-field-group__body {
|
.jio-field-group__body {
|
||||||
padding: 16px 20px 20px;
|
padding: 16px 20px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Collapse transition */
|
/* Collapse transition */
|
||||||
.aksrt-collapse-enter-active,
|
.jio-collapse-enter-active,
|
||||||
.aksrt-collapse-leave-active {
|
.jio-collapse-leave-active {
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-collapse-enter-from,
|
.jio-collapse-enter-from,
|
||||||
.aksrt-collapse-leave-to {
|
.jio-collapse-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
+78
-134
@@ -1,20 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-form-renderer">
|
<div class="jio-form-renderer">
|
||||||
<!-- Page header with title and description -->
|
<!-- Page header with title and description -->
|
||||||
<div v-if="title" class="aksrt-form-renderer__header">
|
<div v-if="title" class="jio-form-renderer__header">
|
||||||
<div class="aksrt-form-renderer__header-left">
|
<div class="jio-form-renderer__header-left">
|
||||||
<FaIcon v-if="icon" :icon="icon" :size="22" class="aksrt-form-renderer__header-icon" />
|
<FaIcon v-if="icon" :icon="icon" :size="22" class="jio-form-renderer__header-icon" />
|
||||||
<div>
|
<div>
|
||||||
<h1 class="aksrt-form-renderer__title">{{ title }}</h1>
|
<h1 class="jio-form-renderer__title">{{ title }}</h1>
|
||||||
<p v-if="description" class="aksrt-form-renderer__desc">{{ description }}</p>
|
<p v-if="description" class="jio-form-renderer__desc">{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showSearch" class="aksrt-form-renderer__search">
|
<div v-if="showSearch" class="jio-form-renderer__search">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="searchQuery"
|
v-model:value="searchQuery"
|
||||||
placeholder="搜索设置项..."
|
placeholder="搜索设置项..."
|
||||||
size="small"
|
size="small"
|
||||||
clearable
|
clearable
|
||||||
|
name="jio-settings-search"
|
||||||
|
:input-props="searchInputProps"
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<FaIcon icon="fa fas fa-search" :size="12" />
|
<FaIcon icon="fa fas fa-search" :size="12" />
|
||||||
@@ -24,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading state -->
|
<!-- Loading state -->
|
||||||
<div v-if="loading" class="aksrt-form-renderer__loading">
|
<div v-if="loading" class="jio-form-renderer__loading">
|
||||||
<n-spin size="medium" />
|
<n-spin size="medium" />
|
||||||
<span>加载中...</span>
|
<span>加载中...</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,7 +47,7 @@
|
|||||||
<!-- Form content -->
|
<!-- Form content -->
|
||||||
<template v-else-if="schema && schema.length">
|
<template v-else-if="schema && schema.length">
|
||||||
<template v-for="group in filteredSchema" :key="groupKey(group)">
|
<template v-for="group in filteredSchema" :key="groupKey(group)">
|
||||||
<AksFieldGroup
|
<JioFieldGroup
|
||||||
v-if="isGroupVisible(group)"
|
v-if="isGroupVisible(group)"
|
||||||
:title="group.title"
|
:title="group.title"
|
||||||
:description="group.description"
|
:description="group.description"
|
||||||
@@ -59,20 +61,21 @@
|
|||||||
<!-- Tabbed: renders tabs whose sub-fields are flat top-level keys -->
|
<!-- Tabbed: renders tabs whose sub-fields are flat top-level keys -->
|
||||||
<div
|
<div
|
||||||
v-if="field.type === 'tabbed' && (field.tabs || []).length"
|
v-if="field.type === 'tabbed' && (field.tabs || []).length"
|
||||||
class="aksrt-form-renderer__tabbed"
|
class="jio-form-renderer__tabbed"
|
||||||
>
|
>
|
||||||
<n-tabs type="line" animated>
|
<n-tabs type="line" animated>
|
||||||
<n-tab-pane
|
<n-tab-pane
|
||||||
v-for="tab in field.tabs"
|
v-for="tab in field.tabs"
|
||||||
:key="tab.title || tab.key"
|
:key="tab.key || tab.title"
|
||||||
:name="tab.title || tab.key"
|
:name="tabName(tab)"
|
||||||
:tab="tab.title || tab.key"
|
:tab="tabName(tab)"
|
||||||
>
|
>
|
||||||
<template v-for="sub in (tab.fields || [])" :key="sub.key">
|
<template v-for="sub in (tab.fields || [])" :key="sub.key">
|
||||||
<AksField
|
<JioField
|
||||||
v-if="isFieldVisible(sub)"
|
v-if="isFieldVisible(sub)"
|
||||||
:schema="sub"
|
:schema="sub"
|
||||||
:model-value="getFieldValue(sub.key)"
|
:model-value="getFieldValue(sub.key)"
|
||||||
|
:form-data="formData"
|
||||||
:error-message="errors?.[sub.key]"
|
:error-message="errors?.[sub.key]"
|
||||||
@update:model-value="setFieldValue(sub.key, $event)"
|
@update:model-value="setFieldValue(sub.key, $event)"
|
||||||
@action="emit('field-action', $event)"
|
@action="emit('field-action', $event)"
|
||||||
@@ -81,23 +84,31 @@
|
|||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
<JioSorterField
|
||||||
|
v-else-if="field.type === 'custom' && field.renderer === 'sorter'"
|
||||||
|
:model-value="getFieldValue(field.key) || {}"
|
||||||
|
:options="field.options || []"
|
||||||
|
@update:model-value="setFieldValue(field.key, $event)"
|
||||||
|
/>
|
||||||
<slot
|
<slot
|
||||||
v-else-if="field.type === 'custom'"
|
v-else-if="field.type === 'custom'"
|
||||||
:name="`field-${field.key}`"
|
:name="`field-${field.key}`"
|
||||||
v-bind="{ field, value: getFieldValue(field.key), setValue: (value) => setFieldValue(field.key, value), formData }"
|
v-bind="{ field, value: getFieldValue(field.key), setValue: (value) => setFieldValue(field.key, value), formData }"
|
||||||
>
|
>
|
||||||
<AksField
|
<JioField
|
||||||
:schema="field"
|
:schema="field"
|
||||||
:model-value="getFieldValue(field.key)"
|
:model-value="getFieldValue(field.key)"
|
||||||
|
:form-data="formData"
|
||||||
:error-message="errors?.[field.key]"
|
:error-message="errors?.[field.key]"
|
||||||
@update:model-value="setFieldValue(field.key, $event)"
|
@update:model-value="setFieldValue(field.key, $event)"
|
||||||
@action="emit('field-action', $event)"
|
@action="emit('field-action', $event)"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
<AksField
|
<JioField
|
||||||
v-else
|
v-else
|
||||||
:schema="field"
|
:schema="field"
|
||||||
:model-value="getFieldValue(field.key)"
|
:model-value="getFieldValue(field.key)"
|
||||||
|
:form-data="formData"
|
||||||
:error-message="errors?.[field.key]"
|
:error-message="errors?.[field.key]"
|
||||||
@update:model-value="setFieldValue(field.key, $event)"
|
@update:model-value="setFieldValue(field.key, $event)"
|
||||||
@action="emit('field-action', $event)"
|
@action="emit('field-action', $event)"
|
||||||
@@ -108,43 +119,24 @@
|
|||||||
|
|
||||||
<!-- Custom group content slot -->
|
<!-- Custom group content slot -->
|
||||||
<slot :name="`group-${group.key}`" v-bind="{ group, formData }" />
|
<slot :name="`group-${group.key}`" v-bind="{ group, formData }" />
|
||||||
</AksFieldGroup>
|
</JioFieldGroup>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Empty state -->
|
<!-- Empty state -->
|
||||||
<div v-else class="aksrt-form-renderer__empty">
|
<div v-else class="jio-form-renderer__empty">
|
||||||
<FaIcon icon="fa fas fa-cog" :size="32" />
|
<FaIcon icon="fa fas fa-cog" :size="32" />
|
||||||
<p>暂无配置项</p>
|
<p>暂无配置项</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sticky save bar -->
|
<!-- Sticky save bar -->
|
||||||
<div v-if="dirty" class="aksrt-form-renderer__save-bar">
|
<JioSaveBar :dirty="dirty" :saving="saving" @save="$emit('save')" @reload="$emit('reload')" />
|
||||||
<div class="aksrt-form-renderer__save-bar__inner">
|
|
||||||
<div class="aksrt-form-renderer__save-bar__info">
|
|
||||||
<FaIcon icon="fa fas fa-exclamation-circle" :size="14" />
|
|
||||||
<span>您有未保存的更改</span>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-renderer__save-bar__actions">
|
|
||||||
<n-button size="small" @click="$emit('reload')">放弃更改</n-button>
|
|
||||||
<n-button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
:loading="saving"
|
|
||||||
@click="$emit('save')"
|
|
||||||
>
|
|
||||||
<FaIcon v-if="!saving" icon="fa fas fa-save" :size="12" />
|
|
||||||
保存设置
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
/**
|
/**
|
||||||
* AksFormRenderer — the core schema-driven form engine.
|
* JioFormRenderer — the core schema-driven form engine.
|
||||||
*
|
*
|
||||||
* Takes a schema definition (array of field groups) and a reactive form data object,
|
* Takes a schema definition (array of field groups) and a reactive form data object,
|
||||||
* and renders a complete settings page with:
|
* and renders a complete settings page with:
|
||||||
@@ -156,8 +148,10 @@
|
|||||||
*/
|
*/
|
||||||
import { ref, computed, useSlots } from 'vue'
|
import { ref, computed, useSlots } from 'vue'
|
||||||
import FaIcon from './FaIcon.vue'
|
import FaIcon from './FaIcon.vue'
|
||||||
import AksFieldGroup from './AksFieldGroup.vue'
|
import JioSaveBar from './JioSaveBar.vue'
|
||||||
import AksField from './AksField.vue'
|
import JioFieldGroup from './JioFieldGroup.vue'
|
||||||
|
import JioField from './JioField.vue'
|
||||||
|
import JioSorterField from './JioSorterField.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
/** Page title */
|
/** Page title */
|
||||||
@@ -193,6 +187,18 @@ const slots = useSlots()
|
|||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
// 同一页面上存在大量 type="password" 的凭证字段,Chrome 会把整页当成登录
|
||||||
|
// 表单,并把保存的账号灌进第一个文本框(也就是这个搜索框)。改名成搜索语义,
|
||||||
|
// 并显式关掉各类自动填充,才能让浏览器不再识别它。
|
||||||
|
const searchInputProps = {
|
||||||
|
autocomplete: 'off',
|
||||||
|
autocorrect: 'off',
|
||||||
|
autocapitalize: 'off',
|
||||||
|
spellcheck: 'false',
|
||||||
|
'data-lpignore': 'true',
|
||||||
|
'data-1p-ignore': 'true',
|
||||||
|
}
|
||||||
|
|
||||||
/** Evaluate dependsOn rule against current form data */
|
/** Evaluate dependsOn rule against current form data */
|
||||||
function matchDependsOn(dependsOn) {
|
function matchDependsOn(dependsOn) {
|
||||||
if (!dependsOn) return true
|
if (!dependsOn) return true
|
||||||
@@ -239,31 +245,30 @@ function isFieldVisible(field) {
|
|||||||
return matchDependsOn(field.dependsOn)
|
return matchDependsOn(field.dependsOn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab label. `titleKey` binds the label to a live form value (e.g. the VIP level
|
||||||
|
* display name) so renaming a level renames its tab; `titleFallback` is used
|
||||||
|
* while that value is still empty.
|
||||||
|
*/
|
||||||
|
function tabName(tab) {
|
||||||
|
if (!tab) return ''
|
||||||
|
const dynamic = tab.titleKey ? props.formData?.[tab.titleKey] : ''
|
||||||
|
const label = typeof dynamic === 'string' ? dynamic.trim() : ''
|
||||||
|
return label || tab.titleFallback || tab.title || tab.key || ''
|
||||||
|
}
|
||||||
|
|
||||||
/** Whole group can be hidden (e.g. optional advanced blocks). */
|
/** Whole group can be hidden (e.g. optional advanced blocks). */
|
||||||
function isGroupVisible(group) {
|
function isGroupVisible(group) {
|
||||||
return matchDependsOn(group.dependsOn)
|
return matchDependsOn(group.dependsOn)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 可折叠分组的初始状态:defaultCollapsed 为真时默认收起。 */
|
||||||
* Collapse preference for a group.
|
|
||||||
* - expandWhen: form key; when truthy, start expanded (channel just enabled → open form)
|
|
||||||
* - defaultCollapsed: static default
|
|
||||||
*/
|
|
||||||
function resolveGroupCollapsed(group) {
|
function resolveGroupCollapsed(group) {
|
||||||
if (group.expandWhen) {
|
|
||||||
const on = !!props.formData?.[group.expandWhen]
|
|
||||||
return !on
|
|
||||||
}
|
|
||||||
return !!group.defaultCollapsed
|
return !!group.defaultCollapsed
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stable key; remount when expandWhen flips so newly enabled channels open expanded. */
|
|
||||||
function groupKey(group) {
|
function groupKey(group) {
|
||||||
const base = group.key || group.title || 'group'
|
return group.key || group.title || 'group'
|
||||||
if (group.expandWhen) {
|
|
||||||
return `${base}:${props.formData?.[group.expandWhen] ? 'on' : 'off'}`
|
|
||||||
}
|
|
||||||
return base
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get field value from formData */
|
/** Get field value from formData */
|
||||||
@@ -317,7 +322,7 @@ const filteredSchema = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-form-renderer__header {
|
.jio-form-renderer__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -326,136 +331,75 @@ const filteredSchema = computed(() => {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-form-renderer__header-left {
|
.jio-form-renderer__header-left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-form-renderer__header-icon {
|
.jio-form-renderer__header-icon {
|
||||||
color: var(--aksrt-color-primary);
|
color: var(--jio-color-primary);
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-form-renderer__title {
|
.jio-form-renderer__title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--aksrt-color-text);
|
color: var(--jio-color-text);
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-form-renderer__desc {
|
.jio-form-renderer__desc {
|
||||||
margin: 4px 0 0;
|
margin: 4px 0 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-form-renderer__tabbed {
|
.jio-form-renderer__tabbed {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-form-renderer__search {
|
.jio-form-renderer__search {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
max-width: 280px;
|
max-width: 280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-form-renderer__loading {
|
.jio-form-renderer__loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 60px 0;
|
padding: 60px 0;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-form-renderer__empty {
|
.jio-form-renderer__empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 60px 0;
|
padding: 60px 0;
|
||||||
color: var(--aksrt-color-border-strong);
|
color: var(--jio-color-border-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-form-renderer__empty p {
|
.jio-form-renderer__empty p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sticky save bar */
|
|
||||||
.aksrt-form-renderer__save-bar {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 1000;
|
|
||||||
background: var(--aksrt-color-surface);
|
|
||||||
border-top: 1px solid var(--aksrt-color-border);
|
|
||||||
box-shadow: var(--aksrt-shadow-floating);
|
|
||||||
animation: slideUp 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-form-renderer__save-bar__inner {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
max-width: 1100px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 12px 28px;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-form-renderer__save-bar__info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
color: #d48806;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-form-renderer__save-bar__actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideUp {
|
|
||||||
from {
|
|
||||||
transform: translateY(100%);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile */
|
/* Mobile */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.aksrt-form-renderer__header {
|
.jio-form-renderer__header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-form-renderer__search {
|
.jio-form-renderer__search {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-form-renderer__save-bar__inner {
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 10px 14px;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-form-renderer__save-bar__actions {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-form-renderer__save-bar__actions .n-button {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
+49
-40
@@ -1,23 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-gallery-field">
|
<div class="jio-gallery-field">
|
||||||
<div v-if="attachmentIds.length" class="aksrt-gallery-field__grid">
|
<div v-if="attachmentIds.length" class="jio-gallery-field__grid">
|
||||||
<article
|
<article
|
||||||
v-for="(attachmentId, index) in attachmentIds"
|
v-for="(attachmentId, index) in attachmentIds"
|
||||||
:key="attachmentId"
|
:key="attachmentId"
|
||||||
class="aksrt-gallery-field__item"
|
class="jio-gallery-field__item"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="previews[attachmentId]?.url"
|
v-if="previews[attachmentId]?.url"
|
||||||
:src="previews[attachmentId].url"
|
:src="previews[attachmentId].url"
|
||||||
:alt="previews[attachmentId].alt || `附件 ${attachmentId}`"
|
:alt="previews[attachmentId].alt || `附件 ${attachmentId}`"
|
||||||
class="aksrt-gallery-field__image"
|
class="jio-gallery-field__image"
|
||||||
/>
|
/>
|
||||||
<div v-else class="aksrt-gallery-field__placeholder">
|
<div v-else class="jio-gallery-field__placeholder">
|
||||||
<FaIcon icon="fa fas fa-image" :size="22" />
|
<FaIcon icon="fa fas fa-image" :size="22" />
|
||||||
<span>#{{ attachmentId }}</span>
|
<span>#{{ attachmentId }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="aksrt-gallery-field__actions">
|
<div class="jio-gallery-field__actions">
|
||||||
<n-button
|
<n-button
|
||||||
size="tiny"
|
size="tiny"
|
||||||
quaternary
|
quaternary
|
||||||
@@ -49,12 +49,12 @@
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="aksrt-gallery-field__empty">
|
<div v-else class="jio-gallery-field__empty">
|
||||||
<FaIcon icon="fa fas fa-images" :size="28" />
|
<FaIcon icon="fa fas fa-images" :size="28" />
|
||||||
<span>尚未选择图片</span>
|
<span>尚未选择图片</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="aksrt-gallery-field__buttons">
|
<div class="jio-gallery-field__buttons">
|
||||||
<n-button type="primary" secondary @click="openMediaLibrary">
|
<n-button type="primary" secondary @click="openMediaLibrary">
|
||||||
<FaIcon icon="fa fas fa-plus" />
|
<FaIcon icon="fa fas fa-plus" />
|
||||||
{{ attachmentIds.length ? '编辑图片' : '选择图片' }}
|
{{ attachmentIds.length ? '编辑图片' : '选择图片' }}
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="mediaError" class="aksrt-gallery-field__error">{{ mediaError }}</p>
|
<p v-if="mediaError" class="jio-gallery-field__error">{{ mediaError }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ async function hydratePreviews() {
|
|||||||
|
|
||||||
const sizes = attachment.get('sizes') || {}
|
const sizes = attachment.get('sizes') || {}
|
||||||
return [attachmentId, {
|
return [attachmentId, {
|
||||||
url: sizes.thumbnail?.url || sizes.medium?.url || attachment.get('url') || '',
|
url: sizes.medium?.url || attachment.get('url') || '',
|
||||||
alt: attachment.get('alt') || attachment.get('title') || '',
|
alt: attachment.get('alt') || attachment.get('title') || '',
|
||||||
}]
|
}]
|
||||||
}))
|
}))
|
||||||
@@ -172,61 +172,70 @@ watch(attachmentIds, hydratePreviews, { immediate: true })
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-gallery-field__grid {
|
.jio-gallery-field__grid {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-gallery-field__item {
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--aksrt-color-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--aksrt-color-canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-gallery-field__image,
|
|
||||||
.aksrt-gallery-field__placeholder {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
flex-wrap: wrap;
|
||||||
aspect-ratio: 4 / 3;
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-gallery-field__item {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 4px 4px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--jio-color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--jio-color-canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-gallery-field__image {
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 90px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-gallery-field__placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-gallery-field__placeholder {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-gallery-field__actions {
|
.jio-gallery-field__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border-top: 1px solid var(--aksrt-color-border);
|
border-top: 1px solid var(--jio-color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-gallery-field__empty {
|
.jio-gallery-field__empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 116px;
|
min-height: 116px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
border: 1px dashed var(--aksrt-color-border-strong);
|
border: 1px dashed var(--jio-color-border-strong);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-gallery-field__buttons {
|
.jio-gallery-field__buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-gallery-field__error {
|
.jio-gallery-field__error {
|
||||||
margin: 8px 0 0;
|
margin: 8px 0 0;
|
||||||
color: #d03050;
|
color: #d03050;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<div class="jio-image-select" role="radiogroup">
|
||||||
|
<button
|
||||||
|
v-for="opt in options"
|
||||||
|
:key="opt.value"
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
class="jio-image-select__item"
|
||||||
|
:class="{ 'is-active': modelValue === opt.value }"
|
||||||
|
:aria-checked="modelValue === opt.value"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="emit('update:modelValue', opt.value)"
|
||||||
|
>
|
||||||
|
<img v-if="opt.image" :src="opt.image" :alt="opt.label" class="jio-image-select__img" :style="imgStyle(opt)" />
|
||||||
|
<span v-else class="jio-image-select__placeholder" :style="imgStyle(opt)">
|
||||||
|
<FaIcon :icon="opt.icon || 'fa fas fa-image'" :size="20" />
|
||||||
|
</span>
|
||||||
|
<span class="jio-image-select__label">{{ opt.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/** image_select:带预览图的单选。 */
|
||||||
|
import FaIcon from './FaIcon.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { default: null },
|
||||||
|
options: { type: Array, default: () => [] },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
// Preview width in px. Layout strips (header/footer) are very wide and only
|
||||||
|
// need enough width to be legible; near-square previews want more room.
|
||||||
|
previewWidth: { type: [Number, String], default: 140 },
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
// An option may override the group width via its own `width`.
|
||||||
|
function imgStyle(opt) {
|
||||||
|
const width = opt && opt.width ? opt.width : props.previewWidth
|
||||||
|
return { '--jio-image-select-max': typeof width === 'number' ? `${width}px` : width }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.jio-image-select {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-image-select__item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 80px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 2px solid var(--jio-color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--jio-color-surface);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color .15s ease, background-color .15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-image-select__item:hover,
|
||||||
|
.jio-image-select__item:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--jio-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-image-select__item.is-active {
|
||||||
|
border-color: var(--jio-color-primary);
|
||||||
|
background: var(--jio-color-primary-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-image-select__item:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-image-select__img {
|
||||||
|
display: block;
|
||||||
|
/* Preview images are wide layout strips (header 597x57, footer 800x190, slide
|
||||||
|
align 200x101), so let them keep their own aspect ratio inside the card.
|
||||||
|
A fixed square box with object-fit:cover crops the layout to an
|
||||||
|
unrecognisable sliver — the width is capped instead and the height follows. */
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--jio-image-select-max, 140px);
|
||||||
|
height: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--jio-color-canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-image-select__placeholder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: var(--jio-image-select-max, 140px);
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--jio-color-canvas);
|
||||||
|
color: var(--jio-color-border-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-image-select__label {
|
||||||
|
color: var(--jio-color-text-secondary);
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.jio-image-select__item {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+43
-34
@@ -1,27 +1,27 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-media-field">
|
<div class="jio-media-field">
|
||||||
<div v-if="modelValue" class="aksrt-media-field__preview">
|
<div v-if="modelValue" class="jio-media-field__preview">
|
||||||
<img
|
<img
|
||||||
v-if="isImage"
|
v-if="isImage"
|
||||||
:src="modelValue"
|
:src="modelValue"
|
||||||
:alt="placeholder"
|
:alt="placeholder"
|
||||||
class="aksrt-media-field__image"
|
class="jio-media-field__image"
|
||||||
/>
|
/>
|
||||||
<div v-else class="aksrt-media-field__file">
|
<div v-else class="jio-media-field__file">
|
||||||
<FaIcon icon="fa fas fa-file" :size="24" />
|
<FaIcon icon="fa fas fa-file" :size="24" />
|
||||||
<span class="aksrt-media-field__filename">{{ fileName }}</span>
|
<span class="jio-media-field__filename">{{ fileName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="aksrt-media-field__overlay">
|
<div class="jio-media-field__overlay">
|
||||||
<n-button size="tiny" quaternary @click="openMediaLibrary">
|
<n-button size="tiny" secondary circle title="更换" @click="openMediaLibrary">
|
||||||
<FaIcon icon="fa fas fa-exchange-alt" /> 更换
|
<FaIcon icon="fa fas fa-exchange-alt" />
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button size="tiny" quaternary type="error" @click="clearMedia">
|
<n-button size="tiny" secondary circle type="error" title="移除" @click="clearMedia">
|
||||||
<FaIcon icon="fa fas fa-trash-alt" /> 移除
|
<FaIcon icon="fa fas fa-trash-alt" />
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="aksrt-media-field__empty" @click="openMediaLibrary">
|
<div v-else class="jio-media-field__empty" @click="openMediaLibrary">
|
||||||
<FaIcon icon="fa fas fa-cloud-upload-alt" :size="28" />
|
<FaIcon icon="fa fas fa-cloud-upload-alt" :size="28" />
|
||||||
<span>{{ placeholder }}</span>
|
<span>{{ placeholder }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
:placeholder="'或直接输入 URL'"
|
:placeholder="'或直接输入 URL'"
|
||||||
size="small"
|
size="small"
|
||||||
class="aksrt-media-field__url"
|
class="jio-media-field__url"
|
||||||
@update:value="emit('update:modelValue', $event)"
|
@update:value="emit('update:modelValue', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
/**
|
/**
|
||||||
* AksMediaField — image/file selector with preview.
|
* JioMediaField — image/file selector with preview.
|
||||||
*
|
*
|
||||||
* In the WordPress admin context, this can open the WP Media Library.
|
* In the WordPress admin context, this can open the WP Media Library.
|
||||||
* For now, it supports direct URL input with preview.
|
* For now, it supports direct URL input with preview.
|
||||||
@@ -95,73 +95,82 @@ function clearMedia() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-media-field__preview {
|
.jio-media-field__preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 72px;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 4px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--aksrt-color-border);
|
border: 1px solid var(--jio-color-border);
|
||||||
|
background: var(--jio-color-canvas);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-media-field__image {
|
.jio-media-field__image {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: auto;
|
||||||
max-height: 200px;
|
height: auto;
|
||||||
object-fit: cover;
|
max-width: 100%;
|
||||||
|
max-height: 120px;
|
||||||
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-media-field__file {
|
.jio-media-field__file {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: var(--aksrt-color-canvas);
|
background: var(--jio-color-canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-media-field__filename {
|
.jio-media-field__filename {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--aksrt-color-text-secondary);
|
color: var(--jio-color-text-secondary);
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-media-field__overlay {
|
.jio-media-field__overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 6px;
|
padding: 4px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-media-field__preview:hover .aksrt-media-field__overlay {
|
.jio-media-field__preview:hover .jio-media-field__overlay {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-media-field__empty {
|
.jio-media-field__empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
border: 2px dashed var(--aksrt-color-border-strong);
|
border: 2px dashed var(--jio-color-border-strong);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-media-field__empty:hover {
|
.jio-media-field__empty:hover {
|
||||||
border-color: var(--aksrt-color-primary);
|
border-color: var(--jio-color-primary);
|
||||||
color: var(--aksrt-color-primary);
|
color: var(--jio-color-primary);
|
||||||
background: var(--aksrt-color-primary-soft);
|
background: var(--jio-color-primary-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-media-field__url {
|
.jio-media-field__url {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
<template>
|
||||||
|
<div class="jio-repeater-list">
|
||||||
|
<draggable
|
||||||
|
v-if="rows.length"
|
||||||
|
v-model="rows"
|
||||||
|
v-bind="SORTABLE_LIST_ATTRS"
|
||||||
|
:item-key="rowKey"
|
||||||
|
:disabled="schema.disabled"
|
||||||
|
class="jio-sortable"
|
||||||
|
>
|
||||||
|
<template #item="{ element, index }">
|
||||||
|
<div class="jio-sortable__item">
|
||||||
|
<div class="jio-sortable__head">
|
||||||
|
<JioSortHandle />
|
||||||
|
<span class="jio-sortable__icon" :class="preview(element).iconClass">
|
||||||
|
<span v-if="preview(element).customIcon" class="jio-repeater-list__icon-html" v-html="preview(element).customIcon" />
|
||||||
|
<i v-else-if="preview(element).icon" :class="preview(element).icon" aria-hidden="true" />
|
||||||
|
<FaIcon v-else icon="fa fas fa-star" :size="16" />
|
||||||
|
</span>
|
||||||
|
<div class="jio-sortable__main">
|
||||||
|
<span class="jio-sortable__title">{{ preview(element).title }}</span>
|
||||||
|
<span class="jio-sortable__desc">{{ preview(element).desc }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="jio-sortable__actions">
|
||||||
|
<n-button size="tiny" quaternary type="primary" :disabled="schema.disabled" @click="openModal(index)">
|
||||||
|
<template #icon><FaIcon icon="fa fas fa-edit" :size="12" /></template>
|
||||||
|
编辑
|
||||||
|
</n-button>
|
||||||
|
<n-button size="tiny" quaternary type="error" :disabled="schema.disabled || reachedMinimum" @click="remove(index)">
|
||||||
|
<template #icon><FaIcon icon="fa fas fa-trash-alt" :size="12" /></template>
|
||||||
|
删除
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
<div v-else class="jio-sortable__empty">
|
||||||
|
<FaIcon icon="fa fas fa-inbox" :size="18" />
|
||||||
|
<span>{{ schema.emptyText || '暂无数据' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-button size="small" dashed block :disabled="schema.disabled || reachedMaximum" @click="openModal(-1)">
|
||||||
|
<template #icon><FaIcon icon="fa fas fa-plus" :size="12" /></template>
|
||||||
|
{{ schema.button_title || schema.addText || '添加一项' }}
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-modal
|
||||||
|
v-model:show="modalShow"
|
||||||
|
:mask-closable="false"
|
||||||
|
preset="card"
|
||||||
|
:title="editingIndex >= 0 ? (schema.editText || '编辑项目') : (schema.addText || '添加项目')"
|
||||||
|
class="jio-modal-card"
|
||||||
|
style="max-width: 640px; width: 92vw;"
|
||||||
|
>
|
||||||
|
<div class="jio-repeater-list__modal-fields">
|
||||||
|
<JioField
|
||||||
|
v-for="field in draftFields"
|
||||||
|
:key="field.key"
|
||||||
|
:schema="field"
|
||||||
|
:model-value="draft[field.key]"
|
||||||
|
:form-data="formData"
|
||||||
|
@update:model-value="draft = { ...draft, [field.key]: $event }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="jio-modal-footer">
|
||||||
|
<n-button @click="closeModal">取消</n-button>
|
||||||
|
<n-button type="primary" @click="confirmModal">确定</n-button>
|
||||||
|
</div>
|
||||||
|
</n-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/** repeater / group 的 list-modal 形态:可拖拽卡片列表 + 弹窗编辑单行。 */
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
import FaIcon from './FaIcon.vue'
|
||||||
|
import JioField from './JioField.vue'
|
||||||
|
import JioSortHandle from './JioSortHandle.vue'
|
||||||
|
import { matchDependency } from './field-dependency.js'
|
||||||
|
import { SORTABLE_LIST_ATTRS } from './sortable-options.js'
|
||||||
|
import { createRepeaterRow, createRepeaterRowId, repeaterRowPreview } from './repeater-rows.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
schema: { type: Object, required: true },
|
||||||
|
modelValue: { default: null },
|
||||||
|
formData: { type: Object, default: () => ({}) },
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const rows = computed({
|
||||||
|
get: () => (Array.isArray(props.modelValue) ? props.modelValue : []),
|
||||||
|
set: (value) => emit('update:modelValue', Array.isArray(value) ? value : []),
|
||||||
|
})
|
||||||
|
const reachedMinimum = computed(() => rows.value.length <= Number(props.schema.min || 0))
|
||||||
|
const reachedMaximum = computed(() => Number(props.schema.max || 0) > 0 && rows.value.length >= Number(props.schema.max))
|
||||||
|
|
||||||
|
const keys = new WeakMap()
|
||||||
|
let keySeed = 0
|
||||||
|
function rowKey(row) {
|
||||||
|
if (!row || typeof row !== 'object') return String(row)
|
||||||
|
if (row._id) return row._id
|
||||||
|
if (!keys.has(row)) keys.set(row, `rpt_list_${++keySeed}`)
|
||||||
|
return keys.get(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
function preview(row) {
|
||||||
|
return repeaterRowPreview(row, props.schema.listPreview || {})
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove(index) {
|
||||||
|
if (reachedMinimum.value) return
|
||||||
|
const next = [...rows.value]
|
||||||
|
next.splice(index, 1)
|
||||||
|
emit('update:modelValue', next)
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalShow = ref(false)
|
||||||
|
const editingIndex = ref(-1)
|
||||||
|
const draft = ref({})
|
||||||
|
const draftFields = computed(() => (props.schema.fields || []).filter(field => matchDependency(field.dependsOn, draft.value)))
|
||||||
|
|
||||||
|
function openModal(index) {
|
||||||
|
if (props.schema.disabled) return
|
||||||
|
if (index < 0 && reachedMaximum.value) return
|
||||||
|
const base = createRepeaterRow(props.schema.fields)
|
||||||
|
draft.value = index >= 0 ? { ...base, ...(rows.value[index] || {}) } : base
|
||||||
|
editingIndex.value = index
|
||||||
|
modalShow.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
modalShow.value = false
|
||||||
|
editingIndex.value = -1
|
||||||
|
draft.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmModal() {
|
||||||
|
const next = [...rows.value]
|
||||||
|
const row = { ...draft.value }
|
||||||
|
if (!row._id) row._id = createRepeaterRowId()
|
||||||
|
if (editingIndex.value >= 0 && editingIndex.value < next.length) {
|
||||||
|
next[editingIndex.value] = row
|
||||||
|
} else {
|
||||||
|
next.push(row)
|
||||||
|
}
|
||||||
|
emit('update:modelValue', next)
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.jio-repeater-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-repeater-list__icon-html {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-repeater-list__icon-html :deep(svg),
|
||||||
|
.jio-repeater-list__icon-html :deep(img) {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-repeater-list__modal-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
max-height: min(70vh, 560px);
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="dirty" class="jio-form-renderer__save-bar">
|
||||||
|
<div class="jio-form-renderer__save-bar__inner">
|
||||||
|
<div class="jio-form-renderer__save-bar__info">
|
||||||
|
<FaIcon icon="fa fas fa-exclamation-circle" :size="14" />
|
||||||
|
<span>您有未保存的更改</span>
|
||||||
|
</div>
|
||||||
|
<div class="jio-form-renderer__save-bar__actions">
|
||||||
|
<n-button size="small" :disabled="saving" @click="emit('reload')">放弃更改</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
:loading="saving"
|
||||||
|
@click="emit('save')"
|
||||||
|
>
|
||||||
|
<FaIcon v-if="!saving" icon="fa fas fa-save" :size="12" />
|
||||||
|
保存设置
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* JioSaveBar — the admin-wide sticky save bar (shown only when dirty).
|
||||||
|
* Shared by JioFormRenderer (schema pages) and custom settings pages.
|
||||||
|
*/
|
||||||
|
import FaIcon from './FaIcon.vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
dirty: { type: Boolean, default: false },
|
||||||
|
saving: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['save', 'reload'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.jio-form-renderer__save-bar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: var(--jio-color-surface);
|
||||||
|
border-top: 1px solid var(--jio-color-border);
|
||||||
|
box-shadow: var(--jio-shadow-floating);
|
||||||
|
animation: slideUp 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-form-renderer__save-bar__inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 12px 28px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-form-renderer__save-bar__info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #d48806;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-form-renderer__save-bar__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.jio-form-renderer__save-bar__inner {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px 14px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-form-renderer__save-bar__actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-form-renderer__save-bar__actions .n-button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="visible" class="jio-secret-clear">
|
||||||
|
<n-button size="small" secondary :type="marked ? 'warning' : 'error'" @click="$emit('toggle')">
|
||||||
|
{{ marked ? '取消清除' : '清除已配置密钥' }}
|
||||||
|
</n-button>
|
||||||
|
<span v-if="marked" class="jio-secret-clear__hint">保存后将删除此密钥</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/**
|
||||||
|
* 已配置密钥的「清除 / 取消清除」操作,input 与 textarea 两种 secret 字段共用。
|
||||||
|
*/
|
||||||
|
defineProps({
|
||||||
|
/** 当前存在已配置密钥('***')或已标记清除时显示 */
|
||||||
|
visible: { type: Boolean, default: false },
|
||||||
|
/** 是否已标记为保存时清除 */
|
||||||
|
marked: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits(['toggle'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.jio-secret-clear {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-secret-clear__hint {
|
||||||
|
color: var(--jio-color-warning, #d97706);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<draggable
|
||||||
|
v-model="values"
|
||||||
|
v-bind="SORTABLE_LIST_ATTRS"
|
||||||
|
:item-key="valueKey"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="jio-sortable jio-sortable--compact jio-selected-order"
|
||||||
|
>
|
||||||
|
<template #item="{ element, index }">
|
||||||
|
<div class="jio-sortable__item">
|
||||||
|
<div class="jio-sortable__head">
|
||||||
|
<JioSortHandle />
|
||||||
|
<span class="jio-sortable__index">{{ index + 1 }}</span>
|
||||||
|
<span class="jio-sortable__title">{{ labelOf(element) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/** 多选 + sortable 的已选值排序列表(select / async_select 下方)。 */
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
import JioSortHandle from './JioSortHandle.vue'
|
||||||
|
import { SORTABLE_LIST_ATTRS } from './sortable-options.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Array, required: true },
|
||||||
|
options: { type: Array, default: () => [] },
|
||||||
|
disabled: { type: Boolean, default: false },
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const values = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (next) => emit('update:modelValue', next),
|
||||||
|
})
|
||||||
|
|
||||||
|
function valueKey(value) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function labelOf(value) {
|
||||||
|
return props.options.find(option => option.value === value)?.label || String(value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.jio-selected-order {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<button type="button" class="jio-sortable__handle" :title="label" :aria-label="label">
|
||||||
|
<i class="fa fas fa-grip-vertical" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/** 拖拽排序手柄;SORTABLE_LIST_ATTRS.handle 指向它的类名。 */
|
||||||
|
defineProps({
|
||||||
|
label: { type: String, default: '拖拽排序' },
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<template>
|
||||||
|
<div class="jio-sorter">
|
||||||
|
<div
|
||||||
|
v-for="column in COLUMNS"
|
||||||
|
:key="column.key"
|
||||||
|
class="jio-sorter__column"
|
||||||
|
:class="`jio-sorter__column--${column.key}`"
|
||||||
|
>
|
||||||
|
<div class="jio-sorter__title">
|
||||||
|
<span>{{ column.title }}</span>
|
||||||
|
<n-tag size="small" :type="column.tagType" :bordered="false">{{ columns[column.key].length }}</n-tag>
|
||||||
|
</div>
|
||||||
|
<draggable
|
||||||
|
v-model="columns[column.key]"
|
||||||
|
v-bind="SORTABLE_LIST_ATTRS"
|
||||||
|
item-key="value"
|
||||||
|
group="jio-sorter"
|
||||||
|
class="jio-sortable jio-sortable--compact jio-sorter__list"
|
||||||
|
@change="commit"
|
||||||
|
>
|
||||||
|
<template #item="{ element }">
|
||||||
|
<div class="jio-sortable__item">
|
||||||
|
<div class="jio-sortable__head">
|
||||||
|
<JioSortHandle />
|
||||||
|
<i v-if="element.icon" :class="element.icon" class="jio-sorter__icon" aria-hidden="true"></i>
|
||||||
|
<span class="jio-sortable__title">{{ element.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
<div v-if="columns[column.key].length === 0" class="jio-sortable__empty jio-sorter__empty">
|
||||||
|
{{ column.empty }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
/** 启用 / 未启用两列拖拽排序器;样式见 styles/sortable-list.css。 */
|
||||||
|
import { reactive, watch } from 'vue'
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
import JioSortHandle from './JioSortHandle.vue'
|
||||||
|
import { SORTABLE_LIST_ATTRS } from './sortable-options.js'
|
||||||
|
|
||||||
|
const COLUMNS = [
|
||||||
|
{ key: 'enabled', title: '已启用', tagType: 'success', empty: '拖拽到此处启用' },
|
||||||
|
{ key: 'disabled', title: '未启用', tagType: 'default', empty: '拖拽到此处停用' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: Object, default: () => ({}) },
|
||||||
|
options: { type: Array, default: () => [] },
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const columns = reactive({ enabled: [], disabled: [] })
|
||||||
|
let syncing = false
|
||||||
|
|
||||||
|
function keysOf(value) {
|
||||||
|
if (Array.isArray(value)) return value.map(String)
|
||||||
|
return value && typeof value === 'object' ? Object.keys(value) : []
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncColumns() {
|
||||||
|
syncing = true
|
||||||
|
const byKey = new Map(props.options.map(option => [String(option.value), option]))
|
||||||
|
const used = new Set()
|
||||||
|
const normalize = keys => keys.filter(key => byKey.has(key) && !used.has(key)).map(key => {
|
||||||
|
used.add(key)
|
||||||
|
return byKey.get(key)
|
||||||
|
})
|
||||||
|
columns.enabled = normalize(keysOf(props.modelValue?.enabled))
|
||||||
|
columns.disabled = normalize(keysOf(props.modelValue?.disabled))
|
||||||
|
for (const option of props.options) {
|
||||||
|
if (!used.has(String(option.value))) columns.disabled.push(option)
|
||||||
|
}
|
||||||
|
syncing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function commit() {
|
||||||
|
if (syncing) return
|
||||||
|
const toMap = items => Object.fromEntries(items.map(item => [String(item.value), String(item.label)]))
|
||||||
|
emit('update:modelValue', {
|
||||||
|
enabled: toMap(columns.enabled),
|
||||||
|
disabled: toMap(columns.disabled),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => [props.modelValue, props.options], syncColumns, { immediate: true, deep: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.jio-sorter {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-sorter__column {
|
||||||
|
position: relative;
|
||||||
|
min-height: 210px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--jio-color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--jio-color-canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-sorter__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--jio-color-text);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空列也要留出可放置区域,拖拽目标不能塌成 0 高度。 */
|
||||||
|
.jio-sorter__list {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-sorter__empty {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
bottom: 12px;
|
||||||
|
left: 12px;
|
||||||
|
top: 46px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jio-sorter__icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 16px;
|
||||||
|
color: var(--jio-color-primary);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.jio-sorter {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* schema 字段的 dependsOn 规则求值。sourceValue 是规则引用的数据对象
|
||||||
|
* (fieldset 值 / repeater 行 / 弹窗草稿)。
|
||||||
|
*/
|
||||||
|
export function matchDependency(rule, sourceValue) {
|
||||||
|
if (!rule) return true
|
||||||
|
const source = sourceValue && typeof sourceValue === 'object' ? sourceValue : {}
|
||||||
|
if (rule.all) return rule.all.every(item => matchDependency(item, source))
|
||||||
|
if (rule.any) return rule.any.some(item => matchDependency(item, source))
|
||||||
|
const value = source[rule.key]
|
||||||
|
if (rule.operator === 'truthy') return !!value
|
||||||
|
if (rule.operator === 'falsy') return !value
|
||||||
|
if (rule.operator === 'not_empty') return Array.isArray(value) ? value.length > 0 : value !== '' && value != null
|
||||||
|
if (rule.operator === 'empty') return Array.isArray(value) ? value.length === 0 : value === '' || value == null
|
||||||
|
if (rule.operator === 'eq') return value === rule.value
|
||||||
|
if (rule.operator === 'neq') return value !== rule.value
|
||||||
|
return rule.value === undefined || value === rule.value
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* 把 accordion / fieldset 的 `schema.sections` 声明展开成「块标题 + 字段定义」。
|
||||||
|
*
|
||||||
|
* 参考主题把「升级价格」这类复合配置拆成若干可折叠块,每块用 keys 声明自己
|
||||||
|
* 包含哪些字段。这里是纯函数,方便在 Node 契约测试里直接用真实 schema 校验,
|
||||||
|
* 避免渲染层的一个小疏忽让整块手风琴静默退化成平铺。
|
||||||
|
*/
|
||||||
|
export function buildFieldsetSections(schema) {
|
||||||
|
const declared = schema && Array.isArray(schema.sections) ? schema.sections : []
|
||||||
|
if (!declared.length) return []
|
||||||
|
|
||||||
|
const byKey = {}
|
||||||
|
const nested = Array.isArray(schema.fields) ? schema.fields : []
|
||||||
|
for (const field of nested) {
|
||||||
|
if (field && field.key) byKey[field.key] = field
|
||||||
|
}
|
||||||
|
|
||||||
|
return declared.map((section) => ({
|
||||||
|
title: String((section && section.title) || ''),
|
||||||
|
fields: (Array.isArray(section && section.keys) ? section.keys : [])
|
||||||
|
.map((key) => byKey[key])
|
||||||
|
.filter(Boolean),
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* repeater / group 行的纯函数:新行默认值、行 ID、listPreview 摘要与预览。
|
||||||
|
* inline 与 list-modal 两种渲染共用。
|
||||||
|
*/
|
||||||
|
let rowIdCounter = 0
|
||||||
|
|
||||||
|
export function createRepeaterRowId() {
|
||||||
|
return `rpt_${Date.now()}_${++rowIdCounter}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRepeaterRow(fields) {
|
||||||
|
const row = { _id: createRepeaterRowId() }
|
||||||
|
for (const field of (Array.isArray(fields) ? fields : [])) {
|
||||||
|
// UI-only chrome rows (notice / alert) are presentation only.
|
||||||
|
if (field.ui_only) continue
|
||||||
|
if (field.default !== undefined) {
|
||||||
|
row[field.key] = field.default
|
||||||
|
} else if (field.type === 'switch') {
|
||||||
|
row[field.key] = false
|
||||||
|
} else if (field.type === 'checkbox' || field.type === 'repeater') {
|
||||||
|
row[field.key] = []
|
||||||
|
} else {
|
||||||
|
row[field.key] = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
/** time/unit 形态的时长摘要:0 视为永久,其余按 day/month 补单位。 */
|
||||||
|
function rowDuration(row, cfg) {
|
||||||
|
if (!cfg.descKey) return ''
|
||||||
|
const raw = row[cfg.descKey]
|
||||||
|
if (raw === '' || raw == null) return ''
|
||||||
|
const amount = Number(raw)
|
||||||
|
if (!Number.isFinite(amount)) return String(raw)
|
||||||
|
if (amount <= 0) return '永久'
|
||||||
|
if (!cfg.descUnitKey) return String(raw)
|
||||||
|
const unit = String(row[cfg.descUnitKey] || '')
|
||||||
|
if (unit === 'month') return `${amount} 个月`
|
||||||
|
if (unit === 'day') return `${amount} 天`
|
||||||
|
return String(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 未配置 titleKey 时,取首个值为非空字符串/数字的字段作为标题。 */
|
||||||
|
function fallbackTitleKey(row, fields) {
|
||||||
|
if (row.title != null && String(row.title).trim() !== '') return 'title'
|
||||||
|
for (const field of (Array.isArray(fields) ? fields : [])) {
|
||||||
|
if (field.ui_only) continue
|
||||||
|
const value = row[field.key]
|
||||||
|
if ((typeof value === 'string' && value.trim() !== '') || typeof value === 'number') return field.key
|
||||||
|
}
|
||||||
|
return 'title'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 收起态标题行上的一行摘要,取自 listPreview 配置。 */
|
||||||
|
export function repeaterRowSummary(row, cfg = {}, fields = []) {
|
||||||
|
if (!row) return ''
|
||||||
|
const title = String(row[cfg.titleKey || fallbackTitleKey(row, fields)] ?? '').trim()
|
||||||
|
const prefix = String(cfg.prefix || '')
|
||||||
|
if (title === '') return prefix
|
||||||
|
const duration = rowDuration(row, cfg)
|
||||||
|
return duration ? `${prefix}${title} · ${duration}` : `${prefix}${title}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** list-modal 卡片预览:图标 / 自定义图标 HTML / 图标类 / 标题 / 描述。 */
|
||||||
|
export function repeaterRowPreview(row, cfg = {}) {
|
||||||
|
const read = (key) => String(row?.[key] || '').trim()
|
||||||
|
const desc = read(cfg.descKey || 'desc') || read(cfg.urlKey || 'url')
|
||||||
|
return {
|
||||||
|
icon: read(cfg.iconKey || 'icon'),
|
||||||
|
customIcon: read(cfg.customIconKey || 'custom_icon'),
|
||||||
|
iconClass: read(cfg.iconClassKey || 'icon_class'),
|
||||||
|
title: read(cfg.titleKey || 'title') || '未命名卡片',
|
||||||
|
desc: desc || '暂无描述',
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 所有后台 vuedraggable 列表共用的 Sortable 选项(v-bind 到 <draggable>)。
|
||||||
|
* 配套样式见 src/styles/sortable-list.css,拖拽手柄统一用 JioSortHandle。
|
||||||
|
*/
|
||||||
|
const prefersReducedMotion = typeof window !== 'undefined'
|
||||||
|
&& typeof window.matchMedia === 'function'
|
||||||
|
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||||
|
|
||||||
|
export const SORTABLE_LIST_ATTRS = Object.freeze({
|
||||||
|
handle: '.jio-sortable__handle',
|
||||||
|
ghostClass: 'jio-sortable__ghost',
|
||||||
|
chosenClass: 'jio-sortable__chosen',
|
||||||
|
dragClass: 'jio-sortable__drag',
|
||||||
|
animation: prefersReducedMotion ? 0 : 180,
|
||||||
|
})
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* 可排序卡片列表的折叠状态:所有带展开区的卡片默认收起,点击行头展开/收起。
|
||||||
|
*
|
||||||
|
* 状态按行的稳定 key 记录(而不是下标),拖拽换位后每张卡片保持自己的开合状态。
|
||||||
|
* 行头属性统一由 collapseHeadAttrs 生成:可点击、可键盘操作(Enter / Space)、
|
||||||
|
* 带 aria-expanded,样式见 styles/sortable-list.css。
|
||||||
|
*/
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export function collapseHeadAttrs(open, toggle) {
|
||||||
|
return {
|
||||||
|
class: 'is-clickable',
|
||||||
|
role: 'button',
|
||||||
|
tabindex: 0,
|
||||||
|
'aria-expanded': open ? 'true' : 'false',
|
||||||
|
onClick: toggle,
|
||||||
|
onKeydown: (event) => {
|
||||||
|
// 只响应行头本身的按键,行内按钮 / 输入框的 Enter、空格不触发折叠。
|
||||||
|
if (event.target !== event.currentTarget) return
|
||||||
|
if (event.key !== 'Enter' && event.key !== ' ') return
|
||||||
|
event.preventDefault()
|
||||||
|
toggle()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCollapsibleItems() {
|
||||||
|
const openKeys = ref(new Set())
|
||||||
|
|
||||||
|
function isOpen(key) {
|
||||||
|
return openKeys.value.has(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(key) {
|
||||||
|
const next = new Set(openKeys.value)
|
||||||
|
if (next.has(key)) next.delete(key)
|
||||||
|
else next.add(key)
|
||||||
|
openKeys.value = next
|
||||||
|
}
|
||||||
|
|
||||||
|
function headAttrs(key) {
|
||||||
|
return collapseHeadAttrs(isOpen(key), () => toggle(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isOpen, toggle, headAttrs }
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import api from '@/api'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JioField 的远程选项加载:async_select / taxonomy_select / user_select / page_select。
|
||||||
|
* 仅按 schema.type 在挂载时拉取对应数据源。
|
||||||
|
*/
|
||||||
|
export function useFieldRemoteOptions(props) {
|
||||||
|
const asyncOptions = ref([])
|
||||||
|
const asyncLoading = ref(false)
|
||||||
|
const taxonomyOptions = ref([])
|
||||||
|
const taxonomyLoading = ref(false)
|
||||||
|
const userOptions = ref([])
|
||||||
|
const userLoading = ref(false)
|
||||||
|
const pageOptions = ref([])
|
||||||
|
const pageLoading = ref(false)
|
||||||
|
let asyncTimer = null
|
||||||
|
let userTimer = null
|
||||||
|
let pageTimer = null
|
||||||
|
|
||||||
|
async function loadAsyncOptions(query = '') {
|
||||||
|
asyncLoading.value = true
|
||||||
|
try {
|
||||||
|
const selected = Array.isArray(props.modelValue) ? props.modelValue : (props.modelValue ? [props.modelValue] : [])
|
||||||
|
const response = await api.searchPageDisplayOptions(props.schema.source, query, query ? [] : selected)
|
||||||
|
asyncOptions.value = response?.data || []
|
||||||
|
} catch (error) {
|
||||||
|
asyncOptions.value = []
|
||||||
|
} finally {
|
||||||
|
asyncLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAsyncSearch(query) {
|
||||||
|
clearTimeout(asyncTimer)
|
||||||
|
asyncTimer = setTimeout(() => loadAsyncOptions(query), 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTaxonomyTerms() {
|
||||||
|
if (!props.schema.taxonomy) return
|
||||||
|
taxonomyLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.getCategories(props.schema.taxonomy || 'category')
|
||||||
|
const items = res?.data || res || []
|
||||||
|
taxonomyOptions.value = (Array.isArray(items) ? items : []).map(t => ({
|
||||||
|
label: t.name + (t.count ? ' (' + t.count + ')' : ''),
|
||||||
|
value: t.term_id,
|
||||||
|
}))
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[JioField] taxonomy load failed:', e)
|
||||||
|
taxonomyOptions.value = []
|
||||||
|
} finally {
|
||||||
|
taxonomyLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUsers(query = '') {
|
||||||
|
userLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.searchUsers(query, 20)
|
||||||
|
const items = res?.data || res || []
|
||||||
|
userOptions.value = (Array.isArray(items) ? items : []).map(u => ({
|
||||||
|
label: u.display_name + ' (' + u.user_login + ')',
|
||||||
|
value: u.id,
|
||||||
|
}))
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[JioField] user load failed:', e)
|
||||||
|
userOptions.value = []
|
||||||
|
} finally {
|
||||||
|
userLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleUserSearch(query) {
|
||||||
|
clearTimeout(userTimer)
|
||||||
|
userTimer = setTimeout(() => loadUsers(query || ''), 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPages(query = '') {
|
||||||
|
pageLoading.value = true
|
||||||
|
try {
|
||||||
|
const selected = props.modelValue ? [props.modelValue] : []
|
||||||
|
const res = await api.searchPageDisplayOptions('pages', query, query ? [] : selected)
|
||||||
|
const items = res?.data || res || []
|
||||||
|
pageOptions.value = Array.isArray(items) ? items : []
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[JioField] page load failed:', e)
|
||||||
|
pageOptions.value = []
|
||||||
|
} finally {
|
||||||
|
pageLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageSearch(query) {
|
||||||
|
clearTimeout(pageTimer)
|
||||||
|
pageTimer = setTimeout(() => loadPages(query), 250)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const type = props.schema.type
|
||||||
|
if (type === 'taxonomy_select') loadTaxonomyTerms()
|
||||||
|
if (type === 'user_select') loadUsers()
|
||||||
|
if (type === 'async_select') loadAsyncOptions()
|
||||||
|
if (type === 'page_select') loadPages()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
asyncOptions,
|
||||||
|
asyncLoading,
|
||||||
|
handleAsyncSearch,
|
||||||
|
taxonomyOptions,
|
||||||
|
taxonomyLoading,
|
||||||
|
userOptions,
|
||||||
|
userLoading,
|
||||||
|
handleUserSearch,
|
||||||
|
pageOptions,
|
||||||
|
pageLoading,
|
||||||
|
handlePageSearch,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { renderFaIcon, schemaPage, customPage } from './helpers'
|
import { renderFaIcon, schemaPage } from './helpers'
|
||||||
|
|
||||||
export const extensionsSection = {
|
export const extensionsSection = {
|
||||||
key: 'extensions',
|
key: 'extensions',
|
||||||
@@ -6,6 +6,16 @@ export const extensionsSection = {
|
|||||||
icon: renderFaIcon('fa fas fa-puzzle-piece'),
|
icon: renderFaIcon('fa fas fa-puzzle-piece'),
|
||||||
route: '/extensions',
|
route: '/extensions',
|
||||||
children: [
|
children: [
|
||||||
|
schemaPage({
|
||||||
|
key: 'extensions-system',
|
||||||
|
name: 'ExtensionsSystem',
|
||||||
|
label: '系统工具',
|
||||||
|
route: '/extensions/system',
|
||||||
|
title: '系统工具',
|
||||||
|
desc: 'WordPress 优化、上传优化与编辑器显示的系统级开关。',
|
||||||
|
canonicalDomain: 'extensions',
|
||||||
|
section: 'system',
|
||||||
|
}),
|
||||||
schemaPage({
|
schemaPage({
|
||||||
key: 'extensions-ip',
|
key: 'extensions-ip',
|
||||||
name: 'ExtensionsIp',
|
name: 'ExtensionsIp',
|
||||||
@@ -32,19 +42,19 @@ export const extensionsSection = {
|
|||||||
label: '百度推送',
|
label: '百度推送',
|
||||||
route: '/extensions/push',
|
route: '/extensions/push',
|
||||||
title: '百度推送',
|
title: '百度推送',
|
||||||
desc: '配置搜索引擎主动推送、批量提交和状态追踪。',
|
desc: '配置百度自动推送、收录通道与触发规则;推送结果与「重新提交」在文章/分类编辑页查看。',
|
||||||
canonicalDomain: 'extensions',
|
canonicalDomain: 'extensions',
|
||||||
section: 'push',
|
section: 'push',
|
||||||
}),
|
}),
|
||||||
customPage({
|
schemaPage({
|
||||||
key: 'extensions-ai',
|
key: 'extensions-security',
|
||||||
name: 'ExtensionAI',
|
name: 'ExtensionsSecurity',
|
||||||
label: 'AI 连接配置',
|
label: '网站安全',
|
||||||
route: '/extensions/ai',
|
route: '/extensions/security',
|
||||||
component: 'ExtensionAI',
|
title: '网站安全',
|
||||||
status: 'available',
|
desc: '防刷限制、验证码发送限制与站点安全加固。',
|
||||||
title: 'AI 连接配置',
|
canonicalDomain: 'extensions',
|
||||||
desc: '配置 AI 生成连接;SEO 在文章独立 SEO 框,摘要/标签在前台投稿页。',
|
section: 'security',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ export const globalSection = {
|
|||||||
label: '常用功能',
|
label: '常用功能',
|
||||||
route: '/global/common',
|
route: '/global/common',
|
||||||
title: '常用功能',
|
title: '常用功能',
|
||||||
desc: '评论、性能、头像源与备案等全局基础配置。',
|
desc: '评论、性能、头像源等全局基础配置。',
|
||||||
canonicalDomain: 'global',
|
canonicalDomain: 'global',
|
||||||
section: 'common',
|
section: 'common',
|
||||||
}),
|
}),
|
||||||
@@ -84,6 +84,16 @@ export const globalSection = {
|
|||||||
canonicalDomain: 'global',
|
canonicalDomain: 'global',
|
||||||
section: 'i18n',
|
section: 'i18n',
|
||||||
}),
|
}),
|
||||||
|
schemaPage({
|
||||||
|
key: 'global-share',
|
||||||
|
name: 'GlobalShare',
|
||||||
|
label: '社交分享',
|
||||||
|
route: '/global/share',
|
||||||
|
title: '社交分享',
|
||||||
|
desc: '配置分享平台、微信 JSAPI 与海报分享。',
|
||||||
|
canonicalDomain: 'global',
|
||||||
|
section: 'share',
|
||||||
|
}),
|
||||||
schemaPage({
|
schemaPage({
|
||||||
key: 'global-email',
|
key: 'global-email',
|
||||||
name: 'GlobalEmail',
|
name: 'GlobalEmail',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const SECTION_DEFAULT_CHILDREN = {
|
|||||||
permissions: 'permissions-comment',
|
permissions: 'permissions-comment',
|
||||||
users: 'users-auth',
|
users: 'users-auth',
|
||||||
payment: 'payment-knowledge',
|
payment: 'payment-knowledge',
|
||||||
extensions: 'extensions-ip',
|
extensions: 'extensions-system',
|
||||||
tools: 'tools-import-export',
|
tools: 'tools-import-export',
|
||||||
'theme-info': 'theme-info-dashboard',
|
'theme-info': 'theme-info-dashboard',
|
||||||
docs: 'docs-dashboard',
|
docs: 'docs-dashboard',
|
||||||
|
|||||||
@@ -37,26 +37,15 @@ export const permissionsSection = {
|
|||||||
canonicalDomain: 'permissions',
|
canonicalDomain: 'permissions',
|
||||||
section: 'moderation',
|
section: 'moderation',
|
||||||
}),
|
}),
|
||||||
customPage({
|
schemaPage({
|
||||||
key: 'permissions-attachment',
|
key: 'permissions-attachment',
|
||||||
name: 'PermissionsAttachment',
|
name: 'PermissionsAttachment',
|
||||||
label: '附件配置',
|
label: '附件配置',
|
||||||
route: '/permissions/attachment',
|
route: '/permissions/attachment',
|
||||||
component: 'AttachmentPermissions',
|
|
||||||
title: '附件配置',
|
title: '附件配置',
|
||||||
desc: '管理文件大小、类型、安全限制和审核策略。',
|
desc: '头像图片、上传大小、上传格式与上传行为配置。',
|
||||||
canonicalDomain: 'permissions',
|
canonicalDomain: 'permissions',
|
||||||
section: 'attachment',
|
section: 'attachment',
|
||||||
}),
|
}),
|
||||||
schemaPage({
|
|
||||||
key: 'permissions-download',
|
|
||||||
name: 'PermissionsDownload',
|
|
||||||
label: '下载配置',
|
|
||||||
route: '/permissions/download',
|
|
||||||
title: '下载配置',
|
|
||||||
desc: '下载链接别名、默认使用次数与有效期限规则。',
|
|
||||||
canonicalDomain: 'permissions',
|
|
||||||
section: 'download',
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -57,16 +57,6 @@ export const postListSection = {
|
|||||||
canonicalDomain: 'post_list',
|
canonicalDomain: 'post_list',
|
||||||
section: 'feature',
|
section: 'feature',
|
||||||
}),
|
}),
|
||||||
schemaPage({
|
|
||||||
key: 'post-list-share',
|
|
||||||
name: 'PostListShare',
|
|
||||||
label: '分享设置',
|
|
||||||
route: '/post-list/share',
|
|
||||||
title: '分享设置',
|
|
||||||
desc: '配置微信分享凭据、海报分享和社交分享选项。',
|
|
||||||
canonicalDomain: 'post_list',
|
|
||||||
section: 'share',
|
|
||||||
}),
|
|
||||||
schemaPage({
|
schemaPage({
|
||||||
key: 'post-list-comment',
|
key: 'post-list-comment',
|
||||||
name: 'PostListComment',
|
name: 'PostListComment',
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const usersSection = {
|
|||||||
route: '/users/verify',
|
route: '/users/verify',
|
||||||
component: 'UsersVerify',
|
component: 'UsersVerify',
|
||||||
title: '身份认证',
|
title: '身份认证',
|
||||||
desc: '配置认证展示、申请类型与申请门槛;审核、驳回和取消认证统一在 WordPress 用户菜单处理。',
|
desc: '配置通用认证申请规则;申请审核和认证资料管理位于 WordPress 用户菜单。',
|
||||||
canonicalDomain: 'users',
|
canonicalDomain: 'users',
|
||||||
section: 'verify',
|
section: 'verify',
|
||||||
}),
|
}),
|
||||||
@@ -108,16 +108,6 @@ export const usersSection = {
|
|||||||
canonicalDomain: 'users',
|
canonicalDomain: 'users',
|
||||||
section: 'sms',
|
section: 'sms',
|
||||||
}),
|
}),
|
||||||
schemaPage({
|
|
||||||
key: 'users-wechat',
|
|
||||||
name: 'UsersWechat',
|
|
||||||
label: '微信通知',
|
|
||||||
route: '/users/wechat',
|
|
||||||
title: '微信通知',
|
|
||||||
desc: '配置微信模板消息、订阅/解绑和站外通知策略。',
|
|
||||||
canonicalDomain: 'users',
|
|
||||||
section: 'wechat',
|
|
||||||
}),
|
|
||||||
customPage({
|
customPage({
|
||||||
key: 'users-wechat-template',
|
key: 'users-wechat-template',
|
||||||
name: 'UsersWechatTemplate',
|
name: 'UsersWechatTemplate',
|
||||||
@@ -140,5 +130,16 @@ export const usersSection = {
|
|||||||
canonicalDomain: 'users',
|
canonicalDomain: 'users',
|
||||||
section: 'report',
|
section: 'report',
|
||||||
}),
|
}),
|
||||||
|
customPage({
|
||||||
|
key: 'users-ticket',
|
||||||
|
name: 'UsersTicket',
|
||||||
|
label: '工单中心',
|
||||||
|
route: '/users/ticket',
|
||||||
|
title: '工单中心',
|
||||||
|
desc: '工单功能开关、工单号前缀、提交公告与提交表单(图片上传、关联订单、催单冷却)配置,以及部门、状态、预设回复维护;工单处理在 WordPress 用户菜单的「工单管理」中进行。',
|
||||||
|
component: 'UsersTicket',
|
||||||
|
canonicalDomain: 'users',
|
||||||
|
section: 'ticket',
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
+12
-11
@@ -4,6 +4,7 @@ import NaiveUI from 'naive-ui'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
|
import './styles/sortable-list.css'
|
||||||
import { styleMountTarget } from './style-target'
|
import { styleMountTarget } from './style-target'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
@@ -13,35 +14,35 @@ app.use(NaiveUI)
|
|||||||
|
|
||||||
// 全局错误处理
|
// 全局错误处理
|
||||||
app.config.errorHandler = (err, vm, info) => {
|
app.config.errorHandler = (err, vm, info) => {
|
||||||
console.error('[AKSRT] 全局错误:', err, info)
|
console.error('[Jio] 全局错误:', err, info)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兜底:立即显示加载状态(防止白屏)
|
// 兜底:立即显示加载状态(防止白屏)
|
||||||
function showFallback(el) {
|
function showFallback(el) {
|
||||||
el.innerHTML = '<div style="padding:40px;text-align:center;color:#666;font-family:sans-serif;">'
|
el.innerHTML = '<div style="padding:40px;text-align:center;color:#666;font-family:sans-serif;">'
|
||||||
+ '<p style="font-size:18px;margin-bottom:8px;">AKSRT 正在加载...</p>'
|
+ '<p style="font-size:18px;margin-bottom:8px;">Jio 正在加载...</p>'
|
||||||
+ '<p style="font-size:13px;color:#999;">如果长时间停留在此页面,请按 F12 查看控制台错误</p>'
|
+ '<p style="font-size:13px;color:#999;">如果长时间停留在此页面,请按 F12 查看控制台错误</p>'
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
}
|
}
|
||||||
showFallback(document.getElementById('aksrt-admin-app') || document.body.appendChild(
|
showFallback(document.getElementById('jio-admin-app') || document.body.appendChild(
|
||||||
Object.assign(document.createElement('div'), { id: 'aksrt-admin-app' })
|
Object.assign(document.createElement('div'), { id: 'jio-admin-app' })
|
||||||
))
|
))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取或创建一个干净的挂载容器
|
* 获取或创建一个干净的挂载容器
|
||||||
* 核心问题:WordPress 后台的 jQuery/脚本会在页面加载后持续操作 DOM,
|
* 核心问题:WordPress 后台的 jQuery/脚本会在页面加载后持续操作 DOM,
|
||||||
* 导致 PHP 输出的 <div id="aksrt-admin-app"> 可能被外部修改或其兄弟节点被移除,
|
* 导致 PHP 输出的 <div id="jio-admin-app"> 可能被外部修改或其兄弟节点被移除,
|
||||||
* Vue patch 时遍历到 null 节点 → nextSibling 报错。
|
* Vue patch 时遍历到 null 节点 → nextSibling 报错。
|
||||||
*
|
*
|
||||||
* 解决方案:在挂载前一刻,动态创建一个全新的容器元素替换旧的,
|
* 解决方案:在挂载前一刻,动态创建一个全新的容器元素替换旧的,
|
||||||
* 确保该节点及其父级在 Vue 接管前不被任何外部脚本污染。
|
* 确保该节点及其父级在 Vue 接管前不被任何外部脚本污染。
|
||||||
*/
|
*/
|
||||||
function getCleanContainer() {
|
function getCleanContainer() {
|
||||||
const existing = document.getElementById('aksrt-admin-app')
|
const existing = document.getElementById('jio-admin-app')
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
// 极端情况:PHP 没有输出容器,手动创建
|
// 极端情况:PHP 没有输出容器,手动创建
|
||||||
const fresh = document.createElement('div')
|
const fresh = document.createElement('div')
|
||||||
fresh.id = 'aksrt-admin-app'
|
fresh.id = 'jio-admin-app'
|
||||||
document.body.appendChild(fresh)
|
document.body.appendChild(fresh)
|
||||||
return fresh
|
return fresh
|
||||||
}
|
}
|
||||||
@@ -52,13 +53,13 @@ function getCleanContainer() {
|
|||||||
if (!parent) {
|
if (!parent) {
|
||||||
// 旧容器已脱离文档树,重新创建
|
// 旧容器已脱离文档树,重新创建
|
||||||
const fresh = document.createElement('div')
|
const fresh = document.createElement('div')
|
||||||
fresh.id = 'aksrt-admin-app'
|
fresh.id = 'jio-admin-app'
|
||||||
document.body.appendChild(fresh)
|
document.body.appendChild(fresh)
|
||||||
return fresh
|
return fresh
|
||||||
}
|
}
|
||||||
|
|
||||||
const fresh = document.createElement('div')
|
const fresh = document.createElement('div')
|
||||||
fresh.id = 'aksrt-admin-app'
|
fresh.id = 'jio-admin-app'
|
||||||
|
|
||||||
// 将旧容器的位置信息转移给新容器(保留 fallback 内容)
|
// 将旧容器的位置信息转移给新容器(保留 fallback 内容)
|
||||||
fresh.innerHTML = existing.innerHTML || ''
|
fresh.innerHTML = existing.innerHTML || ''
|
||||||
@@ -90,12 +91,12 @@ router.isReady().then(() => {
|
|||||||
styleMountTarget.value = container
|
styleMountTarget.value = container
|
||||||
app.mount(container)
|
app.mount(container)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[AKSRT] Vue 挂载失败:', err)
|
console.error('[Jio] Vue 挂载失败:', err)
|
||||||
showError(getCleanContainer(), err)
|
showError(getCleanContainer(), err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.error('[AKSRT] Router 初始化失败:', err)
|
console.error('[Jio] Router 初始化失败:', err)
|
||||||
})
|
})
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-page-container">
|
<div class="jio-page-container">
|
||||||
<h2 class="aksrt-page-title">积分配置</h2>
|
<h2 class="jio-page-title">积分配置</h2>
|
||||||
<p class="aksrt-page-desc">
|
<p class="jio-page-desc">
|
||||||
配置积分规则与发放策略。可自定义货币名称(如”金币”、”魔石”等)。用户余额与手工调整在 WordPress 原生”用户”菜单中管理。
|
配置积分规则与发放策略。可自定义货币名称(如”金币”、”魔石”等)。用户余额与手工调整在 WordPress 原生”用户”菜单中管理。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<n-card class="aksrt-section-card">
|
<n-card class="jio-section-card">
|
||||||
<div class="aksrt-section-header"><h3>主开关 + 基础设置</h3></div>
|
<div class="jio-section-header"><h3>主开关 + 基础设置</h3></div>
|
||||||
<n-form :model="cfg" label-placement="left" label-width="200" class="aksrt-form">
|
<n-form :model="cfg" label-placement="left" label-width="200" class="jio-form">
|
||||||
<n-form-item label="启用积分系统">
|
<n-form-item label="启用积分系统">
|
||||||
<n-switch v-model:value="cfg.enabled" />
|
<n-switch v-model:value="cfg.enabled" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="自定义货币名称">
|
<n-form-item label="自定义货币名称">
|
||||||
<n-input v-model:value="cfg.currency_name" placeholder="积分" maxlength="20" />
|
<n-input v-model:value="cfg.currency_name" placeholder="积分" maxlength="20" />
|
||||||
<template #feedback>
|
<template #feedback>
|
||||||
<span class="aksrt-hint-text">自定义货币显示名称,如"金币"、"魔石"等。留空则使用默认名称"积分"。</span>
|
<span class="jio-hint-text">自定义货币显示名称,如"金币"、"魔石"等。留空则使用默认名称"积分"。</span>
|
||||||
</template>
|
</template>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="注册赠送积分">
|
<n-form-item label="注册赠送积分">
|
||||||
@@ -23,38 +23,9 @@
|
|||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<n-card title="统一资产账本" class="aksrt-section-card">
|
<n-card class="jio-section-card">
|
||||||
<n-space vertical :size="12">
|
<div class="jio-section-header"><h3>每日签到</h3></div>
|
||||||
<n-space wrap>
|
<n-form :model="cfg.daily_signin" label-placement="left" label-width="200" class="jio-form">
|
||||||
<n-select v-model:value="ledgerFilters.asset" :options="assetOptions" clearable placeholder="资产" style="width: 130px" />
|
|
||||||
<n-input v-model:value="ledgerFilters.transaction_id" placeholder="交易号" clearable style="width: 130px" />
|
|
||||||
<n-input v-model:value="ledgerFilters.operation_key" placeholder="幂等键" clearable style="width: 240px" />
|
|
||||||
<n-input v-model:value="ledgerFilters.ref_type" placeholder="引用类型" clearable style="width: 130px" />
|
|
||||||
<n-input v-model:value="ledgerFilters.ref_key" placeholder="引用键" clearable style="width: 180px" />
|
|
||||||
<n-input v-model:value="ledgerFilters.actor_user_id" placeholder="操作者 ID" clearable style="width: 130px" />
|
|
||||||
<n-button type="primary" :loading="ledgerLoading" @click="searchLedger">查询</n-button>
|
|
||||||
<n-button @click="resetLedger">重置</n-button>
|
|
||||||
</n-space>
|
|
||||||
<n-data-table
|
|
||||||
:columns="ledgerColumns"
|
|
||||||
:data="ledgerRows"
|
|
||||||
:loading="ledgerLoading"
|
|
||||||
:row-key="row => row.id"
|
|
||||||
:scroll-x="1600"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<n-pagination
|
|
||||||
v-model:page="ledgerPage"
|
|
||||||
:page-count="ledgerPageCount"
|
|
||||||
:page-size="20"
|
|
||||||
@update:page="loadLedger"
|
|
||||||
/>
|
|
||||||
</n-space>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<n-card class="aksrt-section-card">
|
|
||||||
<div class="aksrt-section-header"><h3>每日签到</h3></div>
|
|
||||||
<n-form :model="cfg.daily_signin" label-placement="left" label-width="200" class="aksrt-form">
|
|
||||||
<n-form-item label="启用">
|
<n-form-item label="启用">
|
||||||
<n-switch v-model:value="cfg.daily_signin.enabled" />
|
<n-switch v-model:value="cfg.daily_signin.enabled" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@@ -69,15 +40,15 @@
|
|||||||
:autosize="{ minRows: 2 }"
|
:autosize="{ minRows: 2 }"
|
||||||
/>
|
/>
|
||||||
<template #feedback>
|
<template #feedback>
|
||||||
<span class="aksrt-hint-text">连续 N 天时额外发放的积分。</span>
|
<span class="jio-hint-text">连续 N 天时额外发放的积分。</span>
|
||||||
</template>
|
</template>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<n-card class="aksrt-section-card">
|
<n-card class="jio-section-card">
|
||||||
<div class="aksrt-section-header"><h3>评论奖励</h3></div>
|
<div class="jio-section-header"><h3>评论奖励</h3></div>
|
||||||
<n-form :model="cfg.comment_reward" label-placement="left" label-width="200" class="aksrt-form">
|
<n-form :model="cfg.comment_reward" label-placement="left" label-width="200" class="jio-form">
|
||||||
<n-form-item label="启用">
|
<n-form-item label="启用">
|
||||||
<n-switch v-model:value="cfg.comment_reward.enabled" />
|
<n-switch v-model:value="cfg.comment_reward.enabled" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@@ -96,9 +67,9 @@
|
|||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<n-card class="aksrt-section-card">
|
<n-card class="jio-section-card">
|
||||||
<div class="aksrt-section-header"><h3>Ref 推广奖励</h3></div>
|
<div class="jio-section-header"><h3>Ref 推广奖励</h3></div>
|
||||||
<n-form :model="cfg.referral_reward" label-placement="left" label-width="200" class="aksrt-form">
|
<n-form :model="cfg.referral_reward" label-placement="left" label-width="200" class="jio-form">
|
||||||
<n-form-item label="启用">
|
<n-form-item label="启用">
|
||||||
<n-switch v-model:value="cfg.referral_reward.enabled" />
|
<n-switch v-model:value="cfg.referral_reward.enabled" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@@ -120,19 +91,19 @@
|
|||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<n-card class="aksrt-section-card">
|
<n-card class="jio-section-card">
|
||||||
<div class="aksrt-section-header"><h3>下载消费(参考)</h3></div>
|
<div class="jio-section-header"><h3>下载消费(参考)</h3></div>
|
||||||
<n-form :model="cfg" label-placement="left" label-width="200" class="aksrt-form">
|
<n-form :model="cfg" label-placement="left" label-width="200" class="jio-form">
|
||||||
<n-form-item :label="`默认积分价参考(${cfg.currency_name || '积分'})`">
|
<n-form-item :label="`默认积分价参考(${cfg.currency_name || '积分'})`">
|
||||||
<n-input-number v-model:value="cfg.download_default_cost" :min="0" :max="100000" />
|
<n-input-number v-model:value="cfg.download_default_cost" :min="0" :max="100000" />
|
||||||
<template #feedback>
|
<template #feedback>
|
||||||
<span class="aksrt-hint-text">实际价格以文章「付费内容设置」的积分价或现金价为准;下载解锁走收银台订单,不按次扣费。</span>
|
<span class="jio-hint-text">实际价格以文章「付费内容设置」的积分价或现金价为准;下载解锁走收银台订单,不按次扣费。</span>
|
||||||
</template>
|
</template>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<div class="aksrt-form-footer">
|
<div class="jio-form-footer">
|
||||||
<n-button type="primary" :loading="saving" @click="saveCfg">保存配置</n-button>
|
<n-button type="primary" :loading="saving" @click="saveCfg">保存配置</n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -164,83 +135,6 @@ const cfg = reactive({
|
|||||||
})
|
})
|
||||||
const streakJson = ref('{}')
|
const streakJson = ref('{}')
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const ledgerLoading = ref(false)
|
|
||||||
const ledgerRows = ref([])
|
|
||||||
const ledgerPage = ref(1)
|
|
||||||
const ledgerPageCount = ref(1)
|
|
||||||
const ledgerFilters = reactive({
|
|
||||||
asset: null,
|
|
||||||
transaction_id: '',
|
|
||||||
operation_key: '',
|
|
||||||
ref_type: '',
|
|
||||||
ref_key: '',
|
|
||||||
actor_user_id: '',
|
|
||||||
})
|
|
||||||
const assetOptions = [
|
|
||||||
{ label: '积分', value: 'points' },
|
|
||||||
{ label: '余额', value: 'balance' },
|
|
||||||
]
|
|
||||||
const ledgerColumns = [
|
|
||||||
{ title: '交易号', key: 'transaction_label', width: 100, fixed: 'left' },
|
|
||||||
{ title: '幂等键', key: 'operation_key', width: 260, ellipsis: { tooltip: true } },
|
|
||||||
{ title: '资产', key: 'assets_label', width: 100 },
|
|
||||||
{ title: '原因', key: 'reason', width: 150, ellipsis: { tooltip: true } },
|
|
||||||
{ title: '业务引用', key: 'reference_label', width: 200, ellipsis: { tooltip: true } },
|
|
||||||
{ title: '操作者', key: 'actor_label', width: 100 },
|
|
||||||
{ title: '增减 / 手续费', key: 'delta_label', width: 180, ellipsis: { tooltip: true } },
|
|
||||||
{ title: '变动后余额', key: 'balance_label', width: 150, ellipsis: { tooltip: true } },
|
|
||||||
{ title: '备注', key: 'remark', width: 180, ellipsis: { tooltip: true } },
|
|
||||||
{ title: 'UTC 时间', key: 'created_at', width: 190 },
|
|
||||||
]
|
|
||||||
|
|
||||||
function formatAssetValue(asset, value) {
|
|
||||||
if (asset === 'balance') return typeof value === 'string' && /^[+-]?\d+\.\d{2}$/.test(value) ? value : '0.00'
|
|
||||||
const raw = String(value ?? '0')
|
|
||||||
return /^-?\d+$/.test(raw) ? raw : '0'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadLedger() {
|
|
||||||
ledgerLoading.value = true
|
|
||||||
try {
|
|
||||||
const params = { page: ledgerPage.value, per_page: 20 }
|
|
||||||
Object.entries(ledgerFilters).forEach(([key, value]) => {
|
|
||||||
if (value !== null && String(value).trim() !== '') params[key] = value
|
|
||||||
})
|
|
||||||
const response = await api.getCreditLog(params)
|
|
||||||
const data = response?.data || response || {}
|
|
||||||
ledgerRows.value = (data.items || []).map((row) => {
|
|
||||||
const entries = Array.isArray(row.entries) ? row.entries : []
|
|
||||||
return {
|
|
||||||
...row,
|
|
||||||
transaction_label: `#${row.id}`,
|
|
||||||
assets_label: (row.assets || []).join(' / '),
|
|
||||||
reference_label: [row.ref_type, row.ref_key].filter(Boolean).join(':') || '—',
|
|
||||||
actor_label: row.actor_user_id ? `#${row.actor_user_id}` : '系统',
|
|
||||||
delta_label: entries.map((entry) => {
|
|
||||||
const value = formatAssetValue(entry.asset, entry.delta)
|
|
||||||
const signed = entry.asset === 'points' && !value.startsWith('-') && value !== '0' ? `+${value}` : value
|
|
||||||
return `${entry.asset}:${signed}`
|
|
||||||
}).join(';'),
|
|
||||||
balance_label: entries.map((entry) => `${entry.asset}:${formatAssetValue(entry.asset, entry.balance_after)}`).join(';'),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
ledgerPageCount.value = Math.max(1, Math.ceil(Number(data.total || 0) / Number(data.per_page || 20)))
|
|
||||||
} catch (e) {
|
|
||||||
message.error(e.message || '加载账本失败')
|
|
||||||
} finally {
|
|
||||||
ledgerLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function searchLedger() {
|
|
||||||
ledgerPage.value = 1
|
|
||||||
loadLedger()
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetLedger() {
|
|
||||||
Object.assign(ledgerFilters, { asset: null, transaction_id: '', operation_key: '', ref_type: '', ref_key: '', actor_user_id: '' })
|
|
||||||
searchLedger()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadCfg() {
|
async function loadCfg() {
|
||||||
try {
|
try {
|
||||||
@@ -270,6 +164,5 @@ async function saveCfg() {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadCfg()
|
loadCfg()
|
||||||
loadLedger()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-schema-page">
|
<div class="jio-schema-page">
|
||||||
<AksFormRenderer
|
<JioFormRenderer
|
||||||
:title="pageMeta.title"
|
:title="pageMeta.title"
|
||||||
:description="pageMeta.description"
|
:description="pageMeta.description"
|
||||||
:icon="pageMeta.icon"
|
:icon="pageMeta.icon"
|
||||||
:schema="pageMeta.groups"
|
:schema="pageMeta.groups"
|
||||||
:form-data="formData"
|
:form-data="formData"
|
||||||
:dirty="dirty"
|
:dirty="isSmsPage ? isSmsSettingsDirty : dirty"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:saving="saving"
|
:saving="saving"
|
||||||
:load-error="loadError"
|
:load-error="loadError"
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
v-model:value="captchaProviderTab"
|
v-model:value="captchaProviderTab"
|
||||||
type="segment"
|
type="segment"
|
||||||
animated
|
animated
|
||||||
class="aksrt-tabs aksrt-captcha-provider-tabs"
|
class="jio-tabs jio-captcha-provider-tabs"
|
||||||
>
|
>
|
||||||
<n-tab-pane
|
<n-tab-pane
|
||||||
v-for="provider in captchaProviders"
|
v-for="provider in captchaProviders"
|
||||||
@@ -48,18 +48,18 @@
|
|||||||
#field-geoip_block_countries="{ value, setValue }"
|
#field-geoip_block_countries="{ value, setValue }"
|
||||||
>
|
>
|
||||||
<n-dynamic-tags :value="Array.isArray(value) ? value : []" @update:value="setValue" />
|
<n-dynamic-tags :value="Array.isArray(value) ? value : []" @update:value="setValue" />
|
||||||
<p class="aksrt-schema-page__field-hint">使用 ISO 国家代码,如 CN / US / RU。</p>
|
<p class="jio-schema-page__field-hint">使用 ISO 国家代码,如 CN / US / RU。</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Slot for custom group content (override per page if needed) -->
|
<!-- Slot for custom group content (override per page if needed) -->
|
||||||
<template v-for="(_, name) in $slots" #[name]="slotData">
|
<template v-for="(_, name) in $slots" #[name]="slotData">
|
||||||
<slot :name="name" v-bind="slotData" />
|
<slot :name="name" v-bind="slotData" />
|
||||||
</template>
|
</template>
|
||||||
</AksFormRenderer>
|
</JioFormRenderer>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="isPermissionsLogin && !loading"
|
v-if="isPermissionsLogin && !loading"
|
||||||
class="aksrt-schema-page__tool-actions"
|
class="jio-schema-page__tool-actions"
|
||||||
>
|
>
|
||||||
<n-button
|
<n-button
|
||||||
:disabled="!formData?.enabled || !formData?.active_provider"
|
:disabled="!formData?.enabled || !formData?.active_provider"
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<n-spin />
|
<n-spin />
|
||||||
<p style="margin-top:10px;color:#999;">加载中...</p>
|
<p style="margin-top:10px;color:#999;">加载中...</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="testResult && !testLoading" :style="{color: testResult.success ? 'var(--aksrt-color-primary)' : '#d03050', textAlign:'center', padding:'20px'}">
|
<div v-if="testResult && !testLoading" :style="{color: testResult.success ? 'var(--jio-color-primary)' : '#d03050', textAlign:'center', padding:'20px'}">
|
||||||
<p style="font-size:16px;margin:0;">{{ testResult.message }}</p>
|
<p style="font-size:16px;margin:0;">{{ testResult.message }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!testLoading && !testResult" id="captcha-test-box" style="min-height:150px;"></div>
|
<div v-if="!testLoading && !testResult" id="captcha-test-box" style="min-height:150px;"></div>
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
* SchemaPage — generic Schema-driven settings page.
|
* SchemaPage — generic Schema-driven settings page.
|
||||||
*
|
*
|
||||||
* Reads the schema from the Vue-side schema registry, fetches/saves settings
|
* Reads the schema from the Vue-side schema registry, fetches/saves settings
|
||||||
* via the API client, and renders using AksFormRenderer.
|
* via the API client, and renders using JioFormRenderer.
|
||||||
*
|
*
|
||||||
* Usage (in router or parent):
|
* Usage (in router or parent):
|
||||||
* <SchemaPage domain="global" section="logo" />
|
* <SchemaPage domain="global" section="logo" />
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
import { computed, watch, ref, nextTick, onBeforeUnmount } from 'vue'
|
import { computed, watch, ref, nextTick, onBeforeUnmount } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useMessage } from 'naive-ui'
|
import { useMessage } from 'naive-ui'
|
||||||
import AksFormRenderer from '@/components/form/AksFormRenderer.vue'
|
import JioFormRenderer from '@/components/form/JioFormRenderer.vue'
|
||||||
import { useSectionForm } from '@/composables/useSectionForm'
|
import { useSectionForm } from '@/composables/useSectionForm'
|
||||||
import { getSchema, normalizeSchemaDomain } from '@/schema/index.js'
|
import { getSchema, normalizeSchemaDomain } from '@/schema/index.js'
|
||||||
import api from '@/api/index.js'
|
import api from '@/api/index.js'
|
||||||
@@ -131,6 +131,7 @@ const showTestModal = ref(false)
|
|||||||
const testLoading = ref(false)
|
const testLoading = ref(false)
|
||||||
const testResult = ref(null)
|
const testResult = ref(null)
|
||||||
const serverSchema = ref(null)
|
const serverSchema = ref(null)
|
||||||
|
const smsSavedSettings = ref('')
|
||||||
const captchaSecretsToClear = ref(new Set())
|
const captchaSecretsToClear = ref(new Set())
|
||||||
const captchaProviders = [
|
const captchaProviders = [
|
||||||
{ key: 'tencent', label: '腾讯云 Captcha', component: TencentConfig },
|
{ key: 'tencent', label: '腾讯云 Captcha', component: TencentConfig },
|
||||||
@@ -144,8 +145,17 @@ const captchaProviderTab = ref('tencent')
|
|||||||
const resolvedDomain = computed(() => normalizeSchemaDomain(props.domain || route.meta?.canonicalDomain || route.meta?.schemaDomain || resolveDomainFromPath(route.path)))
|
const resolvedDomain = computed(() => normalizeSchemaDomain(props.domain || route.meta?.canonicalDomain || route.meta?.schemaDomain || resolveDomainFromPath(route.path)))
|
||||||
const resolvedSection = computed(() => props.section || route.meta?.section || route.meta?.schemaSection || resolveSectionFromPath(route.path))
|
const resolvedSection = computed(() => props.section || route.meta?.section || route.meta?.schemaSection || resolveSectionFromPath(route.path))
|
||||||
const isEmailPage = computed(() => resolvedDomain.value === 'global' && resolvedSection.value === 'email')
|
const isEmailPage = computed(() => resolvedDomain.value === 'global' && resolvedSection.value === 'email')
|
||||||
|
const isSmsPage = computed(() => resolvedDomain.value === 'users' && resolvedSection.value === 'sms')
|
||||||
const isPermissionsLogin = computed(() => resolvedDomain.value === 'permissions' && resolvedSection.value === 'login')
|
const isPermissionsLogin = computed(() => resolvedDomain.value === 'permissions' && resolvedSection.value === 'login')
|
||||||
const isPermissionsModeration = computed(() => resolvedDomain.value === 'permissions' && resolvedSection.value === 'moderation')
|
const isPermissionsModeration = computed(() => resolvedDomain.value === 'permissions' && resolvedSection.value === 'moderation')
|
||||||
|
const isSmsSettingsDirty = computed(() => smsSavedSettings.value !== '' && smsSavedSettings.value !== smsSettingsSnapshot(formData.value))
|
||||||
|
|
||||||
|
function smsSettingsSnapshot(data) {
|
||||||
|
const settings = { ...(data || {}) }
|
||||||
|
delete settings.sms_test_phone
|
||||||
|
delete settings.sms_test_send
|
||||||
|
return JSON.stringify(settings)
|
||||||
|
}
|
||||||
|
|
||||||
function resolveDomainFromPath(path) {
|
function resolveDomainFromPath(path) {
|
||||||
const parts = path.split('/').filter(Boolean)
|
const parts = path.split('/').filter(Boolean)
|
||||||
@@ -211,11 +221,15 @@ const {
|
|||||||
const payload = { ...data }
|
const payload = { ...data }
|
||||||
const clearSecrets = []
|
const clearSecrets = []
|
||||||
Object.keys(payload).forEach((key) => {
|
Object.keys(payload).forEach((key) => {
|
||||||
if (payload[key] === '__AKSRT_CLEAR_SECRET__') {
|
if (payload[key] === '__JIO_CLEAR_SECRET__') {
|
||||||
clearSecrets.push(key)
|
clearSecrets.push(key)
|
||||||
delete payload[key]
|
delete payload[key]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
if (isSmsPage.value) {
|
||||||
|
delete payload.sms_test_phone
|
||||||
|
delete payload.sms_test_send
|
||||||
|
}
|
||||||
if (clearSecrets.length) payload.__clear_secrets = clearSecrets
|
if (clearSecrets.length) payload.__clear_secrets = clearSecrets
|
||||||
if (isPermissionsLogin.value && captchaSecretsToClear.value.size) {
|
if (isPermissionsLogin.value && captchaSecretsToClear.value.size) {
|
||||||
payload.__clear_secrets = [...(payload.__clear_secrets || []), ...captchaSecretsToClear.value]
|
payload.__clear_secrets = [...(payload.__clear_secrets || []), ...captchaSecretsToClear.value]
|
||||||
@@ -226,12 +240,34 @@ const {
|
|||||||
return response
|
return response
|
||||||
},
|
},
|
||||||
defaults: defaults.value,
|
defaults: defaults.value,
|
||||||
|
onLoaded: (data) => {
|
||||||
|
if (isSmsPage.value) smsSavedSettings.value = smsSettingsSnapshot(data)
|
||||||
|
},
|
||||||
onSaved: () => {
|
onSaved: () => {
|
||||||
|
if (isSmsPage.value) smsSavedSettings.value = smsSettingsSnapshot(formData.value)
|
||||||
message.success('设置已保存')
|
message.success('设置已保存')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleFieldAction(key) {
|
async function handleFieldAction(key) {
|
||||||
|
if (key === 'sms_test_send' && isSmsPage.value) {
|
||||||
|
const phone = String(formData.value?.sms_test_phone || '').trim()
|
||||||
|
if (!phone) {
|
||||||
|
message.warning('请先填写测试手机号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isSmsSettingsDirty.value) {
|
||||||
|
message.warning('请先保存当前配置,再发送测试短信')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await api.testSms(phone)
|
||||||
|
message.success(response?.message || '测试短信已发送')
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error?.message || '测试短信发送失败')
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (key !== 'email_test_send' || !isEmailPage.value) return
|
if (key !== 'email_test_send' || !isEmailPage.value) return
|
||||||
const to = String(formData.value?.email_test_to || '').trim()
|
const to = String(formData.value?.email_test_to || '').trim()
|
||||||
if (!to) {
|
if (!to) {
|
||||||
@@ -381,7 +417,7 @@ function loadProviderSDK(provider, cfg) {
|
|||||||
sitekey: cfg.site_key,
|
sitekey: cfg.site_key,
|
||||||
theme: cfg.theme || 'auto',
|
theme: cfg.theme || 'auto',
|
||||||
size: cfg.size || 'normal',
|
size: cfg.size || 'normal',
|
||||||
action: 'aksrt-admin-test',
|
action: 'jio-admin-test',
|
||||||
cdata: 'admin-captcha-test',
|
cdata: 'admin-captcha-test',
|
||||||
callback: function(token) {
|
callback: function(token) {
|
||||||
submitCaptchaTest('cloudflare', { cf_turnstile_response: token })
|
submitCaptchaTest('cloudflare', { cf_turnstile_response: token })
|
||||||
@@ -503,21 +539,21 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-schema-page {
|
.jio-schema-page {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-schema-page__tool-actions {
|
.jio-schema-page__tool-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-schema-page__field-hint {
|
.jio-schema-page__field-hint {
|
||||||
margin: 8px 0 0;
|
margin: 8px 0 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.captcha-test-overlay {
|
.captcha-test-overlay {
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-page-container content-page">
|
<div class="jio-page-container content-page">
|
||||||
<PageHeader title="底部Tab配置" desc="自定义移动端底部导航栏的按钮,支持排序、图标配置和返回顶部功能。">
|
<PageHeader title="底部Tab配置" desc="自定义移动端底部导航栏的按钮,支持排序、图标配置和返回顶部功能。">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<n-button type="primary" :loading="saving" @click="handleSave">保存</n-button>
|
<n-button type="primary" :loading="saving" @click="handleSave">保存</n-button>
|
||||||
@@ -8,24 +8,24 @@
|
|||||||
|
|
||||||
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
|
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
|
||||||
<!-- 基础设置 -->
|
<!-- 基础设置 -->
|
||||||
<n-card class="aksrt-section-card">
|
<n-card class="jio-section-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="aksrt-card-header-row">
|
<div class="jio-card-header-row">
|
||||||
<div class="aksrt-card-header-row__left">
|
<div class="jio-card-header-row__left">
|
||||||
<span class="header-icon"><i class="fa fas fa-cog"></i></span>
|
<span class="header-icon"><i class="fa fas fa-cog"></i></span>
|
||||||
<strong class="aksrt-card-header-row__title">基础设置</strong>
|
<strong class="jio-card-header-row__title">基础设置</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<n-form :model="form" label-placement="left" label-width="140" class="aksrt-form">
|
<n-form :model="form" label-placement="left" label-width="140" class="jio-form">
|
||||||
<n-form-item label="启用底部Tab">
|
<n-form-item label="启用底部Tab">
|
||||||
<n-switch v-model:value="form.enabled" />
|
<n-switch v-model:value="form.enabled" />
|
||||||
<span class="aksrt-hint-text" style="margin-left: 12px;">移动端底部导航栏总开关</span>
|
<span class="jio-hint-text" style="margin-left: 12px;">移动端底部导航栏总开关</span>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="向下滚动隐藏">
|
<n-form-item label="向下滚动隐藏">
|
||||||
<n-switch v-model:value="form.scroll_down_hide" />
|
<n-switch v-model:value="form.scroll_down_hide" />
|
||||||
<span class="aksrt-hint-text" style="margin-left: 12px;">页面向下滚动时隐藏,向上滚动时显示</span>
|
<span class="jio-hint-text" style="margin-left: 12px;">页面向下滚动时隐藏,向上滚动时显示</span>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="文章页专属Tab">
|
<n-form-item label="文章页专属Tab">
|
||||||
<n-select
|
<n-select
|
||||||
@@ -36,18 +36,18 @@
|
|||||||
]"
|
]"
|
||||||
style="width: 240px;"
|
style="width: 240px;"
|
||||||
/>
|
/>
|
||||||
<span class="aksrt-hint-text" style="margin-left: 12px;">文章详情页底部显示的Tab按钮类型</span>
|
<span class="jio-hint-text" style="margin-left: 12px;">文章详情页底部显示的Tab按钮类型</span>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- Tab按钮配置 -->
|
<!-- Tab按钮配置 -->
|
||||||
<n-card class="aksrt-section-card">
|
<n-card class="jio-section-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="aksrt-card-header-row">
|
<div class="jio-card-header-row">
|
||||||
<div class="aksrt-card-header-row__left">
|
<div class="jio-card-header-row__left">
|
||||||
<span class="header-icon"><i class="fa fas fa-list-ul"></i></span>
|
<span class="header-icon"><i class="fa fas fa-list-ul"></i></span>
|
||||||
<strong class="aksrt-card-header-row__title">Tab按钮配置 ({{ form.items.length }})</strong>
|
<strong class="jio-card-header-row__title">Tab按钮配置 ({{ form.items.length }})</strong>
|
||||||
</div>
|
</div>
|
||||||
<n-button type="primary" size="small" @click="addItem">
|
<n-button type="primary" size="small" @click="addItem">
|
||||||
<template #icon><i class="fa fas fa-plus"></i></template>
|
<template #icon><i class="fa fas fa-plus"></i></template>
|
||||||
@@ -61,34 +61,29 @@
|
|||||||
<draggable
|
<draggable
|
||||||
v-if="form.items.length"
|
v-if="form.items.length"
|
||||||
v-model="form.items"
|
v-model="form.items"
|
||||||
|
v-bind="SORTABLE_LIST_ATTRS"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
handle=".tabbar-sort__handle"
|
class="jio-sortable"
|
||||||
ghost-class="tabbar-sort__ghost"
|
|
||||||
animation="180"
|
|
||||||
class="tabbar-sort"
|
|
||||||
@end="resetSortOrder"
|
@end="resetSortOrder"
|
||||||
>
|
>
|
||||||
<template #item="{ element, index }">
|
<template #item="{ element, index }">
|
||||||
<div class="tabbar-sort__item">
|
<div class="jio-sortable__item">
|
||||||
<div class="tabbar-sort__main">
|
<div class="jio-sortable__head">
|
||||||
<button type="button" class="tabbar-sort__handle" aria-label="拖拽排序">
|
<JioSortHandle />
|
||||||
<i class="fa fas fa-bars"></i>
|
<span class="jio-sortable__icon">
|
||||||
</button>
|
|
||||||
<span class="tabbar-sort__icon">
|
|
||||||
<IconValue v-if="element.icon" :value="element.icon" />
|
<IconValue v-if="element.icon" :value="element.icon" />
|
||||||
</span>
|
</span>
|
||||||
<div class="tabbar-sort__meta">
|
<div class="jio-sortable__main">
|
||||||
<div class="tabbar-sort__title-row">
|
<div class="jio-sortable__title-row">
|
||||||
<strong>{{ element.text || '按钮' }}</strong>
|
<span class="jio-sortable__title">{{ element.text || '按钮' }}</span>
|
||||||
<n-tag size="small" :bordered="false">{{ typeLabelMap[element.type] || element.type }}</n-tag>
|
<n-tag size="small" :bordered="false">{{ typeLabelMap[element.type] || element.type }}</n-tag>
|
||||||
<n-tag v-if="element.badge" size="small" type="error" :bordered="false">徽标 {{ element.badge }}</n-tag>
|
<n-tag v-if="element.badge" size="small" type="error" :bordered="false">徽标 {{ element.badge }}</n-tag>
|
||||||
<n-tag v-if="element.type === 'home' && element.ontop" size="small" type="success" :bordered="false">返回顶部</n-tag>
|
<n-tag v-if="element.type === 'home' && element.ontop" size="small" type="success" :bordered="false">返回顶部</n-tag>
|
||||||
<n-tag size="small" :type="element.enabled ? 'success' : 'default'" :bordered="false">{{ element.enabled ? '启用' : '禁用' }}</n-tag>
|
<n-tag size="small" :type="element.enabled ? 'success' : 'default'" :bordered="false">{{ element.enabled ? '启用' : '禁用' }}</n-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="tabbar-sort__desc">{{ getItemSummary(element) }}</div>
|
<div class="jio-sortable__desc">{{ getItemSummary(element) }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="jio-sortable__actions">
|
||||||
<div class="tabbar-sort__actions">
|
|
||||||
<n-button size="tiny" quaternary type="primary" @click="editItem(index)">
|
<n-button size="tiny" quaternary type="primary" @click="editItem(index)">
|
||||||
<template #icon><i class="fa fas fa-edit" style="font-size:12px;" /></template>
|
<template #icon><i class="fa fas fa-edit" style="font-size:12px;" /></template>
|
||||||
编辑
|
编辑
|
||||||
@@ -99,12 +94,13 @@
|
|||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
<n-empty v-else description="暂无Tab按钮,请点击上方按钮添加" />
|
<n-empty v-else description="暂无Tab按钮,请点击上方按钮添加" />
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<div class="aksrt-form-actions">
|
<div class="jio-form-actions">
|
||||||
<n-button type="primary" :loading="saving" @click="handleSave">保存底部Tab配置</n-button>
|
<n-button type="primary" :loading="saving" @click="handleSave">保存底部Tab配置</n-button>
|
||||||
</div>
|
</div>
|
||||||
</n-spin>
|
</n-spin>
|
||||||
@@ -115,10 +111,10 @@
|
|||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
preset="card"
|
preset="card"
|
||||||
:title="editingIndex >= 0 ? '编辑Tab按钮' : '添加Tab按钮'"
|
:title="editingIndex >= 0 ? '编辑Tab按钮' : '添加Tab按钮'"
|
||||||
class="aksrt-modal-card"
|
class="jio-modal-card"
|
||||||
style="max-width: 520px;"
|
style="max-width: 520px;"
|
||||||
>
|
>
|
||||||
<n-form :model="editingItem" label-placement="top" class="aksrt-form">
|
<n-form :model="editingItem" label-placement="top" class="jio-form">
|
||||||
<n-form-item label="按钮类型">
|
<n-form-item label="按钮类型">
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="editingItem.type"
|
v-model:value="editingItem.type"
|
||||||
@@ -216,23 +212,22 @@
|
|||||||
<div v-if="editingItem.add_items.length" class="add-items-editor__list">
|
<div v-if="editingItem.add_items.length" class="add-items-editor__list">
|
||||||
<draggable
|
<draggable
|
||||||
v-model="editingItem.add_items"
|
v-model="editingItem.add_items"
|
||||||
|
v-bind="SORTABLE_LIST_ATTRS"
|
||||||
item-key="id"
|
item-key="id"
|
||||||
handle=".add-items-editor__drag"
|
class="jio-sortable jio-sortable--compact"
|
||||||
ghost-class="add-items-editor__ghost"
|
|
||||||
animation="180"
|
|
||||||
>
|
>
|
||||||
<template #item="{ element, index: subIndex }">
|
<template #item="{ element, index: subIndex }">
|
||||||
<div class="add-items-editor__row">
|
<div class="jio-sortable__item">
|
||||||
<button type="button" class="add-items-editor__drag" aria-label="拖拽排序">
|
<div class="jio-sortable__head add-items-editor__row">
|
||||||
<i class="fa fas fa-bars"></i>
|
<JioSortHandle />
|
||||||
</button>
|
|
||||||
<n-input v-model:value="element.label" placeholder="名称,例如:写文章" />
|
<n-input v-model:value="element.label" placeholder="名称,例如:写文章" />
|
||||||
<FaIconPicker v-model="element.icon" placeholder="选择图标" />
|
<FaIconPicker v-model="element.icon" placeholder="选择图标" />
|
||||||
<n-input v-model:value="element.url" placeholder="链接,例如:/wp-admin/post-new.php" />
|
<n-input v-model:value="element.url" placeholder="链接,例如:/wp-admin/post-new.php" />
|
||||||
<n-button quaternary type="error" @click="removeAddItem(subIndex)">
|
<n-button quaternary type="error" aria-label="删除子项" @click="removeAddItem(subIndex)">
|
||||||
<template #icon><i class="fa fas fa-trash-alt" /></template>
|
<template #icon><i class="fa fas fa-trash-alt" /></template>
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,7 +236,7 @@
|
|||||||
|
|
||||||
<n-form-item v-if="editingItem.type === 'home'" label="返回顶部功能">
|
<n-form-item v-if="editingItem.type === 'home'" label="返回顶部功能">
|
||||||
<n-switch v-model:value="editingItem.ontop" />
|
<n-switch v-model:value="editingItem.ontop" />
|
||||||
<span class="aksrt-hint-text" style="margin-left: 12px;">页面滚动后切换为返回顶部按钮</span>
|
<span class="jio-hint-text" style="margin-left: 12px;">页面滚动后切换为返回顶部按钮</span>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
|
||||||
<n-form-item label="启用状态">
|
<n-form-item label="启用状态">
|
||||||
@@ -250,7 +245,7 @@
|
|||||||
</n-form>
|
</n-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="aksrt-modal-footer">
|
<div class="jio-modal-footer">
|
||||||
<n-button @click="showModal = false">取消</n-button>
|
<n-button @click="showModal = false">取消</n-button>
|
||||||
<n-button type="primary" @click="saveItem">保存</n-button>
|
<n-button type="primary" @click="saveItem">保存</n-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -267,6 +262,8 @@ import api from '@/api'
|
|||||||
import PageHeader from '@/components/PageHeader.vue'
|
import PageHeader from '@/components/PageHeader.vue'
|
||||||
import FaIconPicker from '@/components/form/FaIconPicker.vue'
|
import FaIconPicker from '@/components/form/FaIconPicker.vue'
|
||||||
import IconValue from '@/components/form/IconValue.vue'
|
import IconValue from '@/components/form/IconValue.vue'
|
||||||
|
import JioSortHandle from '@/components/form/JioSortHandle.vue'
|
||||||
|
import { SORTABLE_LIST_ATTRS } from '@/components/form/sortable-options.js'
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
@@ -609,7 +606,7 @@ onMounted(loadSettings)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.icon-preview-row a {
|
.icon-preview-row a {
|
||||||
color: var(--n-primary-color);
|
color: var(--jio-color-primary, #18A058);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,18 +615,12 @@ onMounted(loadSettings)
|
|||||||
}
|
}
|
||||||
|
|
||||||
.muted-text {
|
.muted-text {
|
||||||
color: #999;
|
color: var(--jio-color-muted, #6F7A72);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-modal-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-desc {
|
.section-desc {
|
||||||
color: var(--n-text-color-3);
|
color: var(--jio-color-muted, #6F7A72);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin: 0 0 16px;
|
margin: 0 0 16px;
|
||||||
}
|
}
|
||||||
@@ -659,127 +650,19 @@ onMounted(loadSettings)
|
|||||||
|
|
||||||
.add-items-editor__presets-label {
|
.add-items-editor__presets-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--n-text-color-3, #999);
|
color: var(--jio-color-muted, #6F7A72);
|
||||||
}
|
|
||||||
|
|
||||||
.add-items-editor__list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-items-editor__row {
|
.add-items-editor__row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 32px 1fr 1fr 1.2fr 40px;
|
grid-template-columns: 28px 1fr 1fr 1.2fr 34px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-items-editor__drag {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #f3f5f7;
|
|
||||||
color: #6b7280;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-items-editor__ghost {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbar-sort {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbar-sort__item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 14px 16px;
|
|
||||||
border: 1px solid var(--n-border-color);
|
|
||||||
border-radius: 14px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbar-sort__main {
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbar-sort__handle {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: #f3f5f7;
|
|
||||||
color: #6b7280;
|
|
||||||
cursor: grab;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbar-sort__icon {
|
|
||||||
width: 38px;
|
|
||||||
height: 38px;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: rgba(24, 160, 88, 0.12);
|
|
||||||
color: var(--aksrt-color-primary);
|
|
||||||
font-size: 18px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbar-sort__meta {
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbar-sort__title-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbar-sort__desc {
|
|
||||||
color: var(--n-text-color-3);
|
|
||||||
font-size: 13px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbar-sort__actions {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbar-sort__ghost {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1023px) {
|
@media (max-width: 1023px) {
|
||||||
.tabbar-sort__item {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabbar-sort__actions {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-items-editor__row {
|
.add-items-editor__row {
|
||||||
grid-template-columns: 32px 1fr;
|
grid-template-columns: 28px 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-page-container content-page">
|
<div class="jio-page-container content-page">
|
||||||
<PageHeader title="通知管理" desc="管理通知内容开关、user 徽标统计口径和自动清理策略。">
|
<PageHeader title="通知管理" desc="管理通知内容开关、user 徽标统计口径和自动清理策略。">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<n-space>
|
<n-space>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
|
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
|
||||||
<n-card title="通知内容开关" class="aksrt-section-card">
|
<n-card title="通知内容开关" class="jio-section-card">
|
||||||
<n-form :model="settings" label-placement="left" label-width="180">
|
<n-form :model="settings" label-placement="left" label-width="180">
|
||||||
<n-form-item label="评论回复通知">
|
<n-form-item label="评论回复通知">
|
||||||
<n-switch v-model:value="settings.enabled_types.comment_reply" />
|
<n-switch v-model:value="settings.enabled_types.comment_reply" />
|
||||||
@@ -35,21 +35,21 @@
|
|||||||
|
|
||||||
<n-grid :cols="2" :x-gap="16" responsive="screen" item-responsive style="margin-top: 16px;">
|
<n-grid :cols="2" :x-gap="16" responsive="screen" item-responsive style="margin-top: 16px;">
|
||||||
<n-grid-item span="24 l:10">
|
<n-grid-item span="24 l:10">
|
||||||
<n-card title="user 徽标规则" class="aksrt-section-card">
|
<n-card title="user 徽标规则" class="jio-section-card">
|
||||||
<n-form :model="settings" label-placement="left" label-width="180">
|
<n-form :model="settings" label-placement="left" label-width="180">
|
||||||
<n-form-item label="启用 user 动态徽标">
|
<n-form-item label="启用 user 动态徽标">
|
||||||
<n-switch v-model:value="settings.user_badge_enabled" />
|
<n-switch v-model:value="settings.user_badge_enabled" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="徽标包含状态提醒">
|
<n-form-item label="徽标包含状态提醒">
|
||||||
<n-switch v-model:value="settings.badge_include_stateful" />
|
<n-switch v-model:value="settings.badge_include_stateful" />
|
||||||
<span class="aksrt-hint-text" style="margin-left: 12px;">未读持久化通知之外,叠加邮箱待验证、待审核评论等即时提醒</span>
|
<span class="jio-hint-text" style="margin-left: 12px;">未读持久化通知之外,叠加邮箱待验证、待审核评论等即时提醒</span>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
|
|
||||||
<n-grid-item span="24 l:14">
|
<n-grid-item span="24 l:14">
|
||||||
<n-card title="通知清理策略" class="aksrt-section-card">
|
<n-card title="通知清理策略" class="jio-section-card">
|
||||||
<n-form :model="settings" label-placement="left" label-width="180">
|
<n-form :model="settings" label-placement="left" label-width="180">
|
||||||
<n-form-item label="已读通知保留天数">
|
<n-form-item label="已读通知保留天数">
|
||||||
<n-input-number v-model:value="settings.read_retention_days" :min="1" :max="365" style="width: 180px;" />
|
<n-input-number v-model:value="settings.read_retention_days" :min="1" :max="365" style="width: 180px;" />
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-page-container">
|
<div class="jio-page-container">
|
||||||
<PageHeader title="文档中心" desc="主题使用文档、开发指南、迁移说明和更新日志。" />
|
<PageHeader title="文档中心" desc="主题使用文档、开发指南、迁移说明和更新日志。" />
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<n-card title="常用操作" size="small" class="aksrt-docs__section">
|
<n-card title="常用操作" size="small" class="jio-docs__section">
|
||||||
<n-grid :cols="2" :x-gap="12" :y-gap="12">
|
<n-grid :cols="2" :x-gap="12" :y-gap="12">
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<div class="aksrt-docs__action">
|
<div class="jio-docs__action">
|
||||||
<FaIcon icon="fa fas fa-paint-brush" :size="16" />
|
<FaIcon icon="fa fas fa-paint-brush" :size="16" />
|
||||||
<div>
|
<div>
|
||||||
<strong>全局设置</strong>
|
<strong>全局设置</strong>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<div class="aksrt-docs__action">
|
<div class="jio-docs__action">
|
||||||
<FaIcon icon="fa fas fa-home" :size="16" />
|
<FaIcon icon="fa fas fa-home" :size="16" />
|
||||||
<div>
|
<div>
|
||||||
<strong>首页配置</strong>
|
<strong>首页配置</strong>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<div class="aksrt-docs__action">
|
<div class="jio-docs__action">
|
||||||
<FaIcon icon="fa fas fa-users" :size="16" />
|
<FaIcon icon="fa fas fa-users" :size="16" />
|
||||||
<div>
|
<div>
|
||||||
<strong>用户管理</strong>
|
<strong>用户管理</strong>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<div class="aksrt-docs__action">
|
<div class="jio-docs__action">
|
||||||
<FaIcon icon="fa fas fa-wrench" :size="16" />
|
<FaIcon icon="fa fas fa-wrench" :size="16" />
|
||||||
<div>
|
<div>
|
||||||
<strong>工具</strong>
|
<strong>工具</strong>
|
||||||
@@ -49,30 +49,30 @@
|
|||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- Reference Docs -->
|
<!-- Reference Docs -->
|
||||||
<div class="aksrt-docs__grid">
|
<div class="jio-docs__grid">
|
||||||
<n-card
|
<n-card
|
||||||
v-for="doc in docs"
|
v-for="doc in docs"
|
||||||
:key="doc.key"
|
:key="doc.key"
|
||||||
class="aksrt-docs__card"
|
class="jio-docs__card"
|
||||||
size="small"
|
size="small"
|
||||||
hoverable
|
hoverable
|
||||||
@click="openDoc(doc)"
|
@click="openDoc(doc)"
|
||||||
>
|
>
|
||||||
<div class="aksrt-docs__card-inner">
|
<div class="jio-docs__card-inner">
|
||||||
<div class="aksrt-docs__card-icon" :style="{ background: doc.color }">
|
<div class="jio-docs__card-icon" :style="{ background: doc.color }">
|
||||||
<FaIcon :icon="doc.icon" :size="20" color="#fff" />
|
<FaIcon :icon="doc.icon" :size="20" color="#fff" />
|
||||||
</div>
|
</div>
|
||||||
<div class="aksrt-docs__card-body">
|
<div class="jio-docs__card-body">
|
||||||
<h4 class="aksrt-docs__card-title">{{ doc.title }}</h4>
|
<h4 class="jio-docs__card-title">{{ doc.title }}</h4>
|
||||||
<p class="aksrt-docs__card-desc">{{ doc.description }}</p>
|
<p class="jio-docs__card-desc">{{ doc.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
<FaIcon icon="fa fas fa-external-link-alt" :size="12" class="aksrt-docs__card-arrow" />
|
<FaIcon icon="fa fas fa-external-link-alt" :size="12" class="jio-docs__card-arrow" />
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Version Info -->
|
<!-- Version Info -->
|
||||||
<n-card title="当前版本" size="small" class="aksrt-docs__section">
|
<n-card title="当前版本" size="small" class="jio-docs__section">
|
||||||
<n-descriptions :column="2" bordered label-placement="left" size="small">
|
<n-descriptions :column="2" bordered label-placement="left" size="small">
|
||||||
<n-descriptions-item label="主题版本">
|
<n-descriptions-item label="主题版本">
|
||||||
<n-tag type="success" size="small">{{ themeVersion }}</n-tag>
|
<n-tag type="success" size="small">{{ themeVersion }}</n-tag>
|
||||||
@@ -91,12 +91,12 @@ import PageHeader from '@/components/PageHeader.vue'
|
|||||||
import FaIcon from '@/components/form/FaIcon.vue'
|
import FaIcon from '@/components/form/FaIcon.vue'
|
||||||
import api from '@/api/index.js'
|
import api from '@/api/index.js'
|
||||||
|
|
||||||
const themeVersion = ref(window.aksrtAdminData?.themeVersion || '3.4.0-dev')
|
const themeVersion = ref(window.jioAdminData?.themeVersion || '3.4.0-dev')
|
||||||
const dbSchemaVersion = ref('—')
|
const dbSchemaVersion = ref('—')
|
||||||
|
|
||||||
const docs = ref([
|
const docs = ref([
|
||||||
{ key: 'admin-ia-map', title: '后台 IA 蓝图', description: '完整的后台信息架构映射,12 个顶级分区及子页面清单。', icon: 'fa fas fa-sitemap', color: 'var(--aksrt-color-primary)', file: 'admin-ia-map.md' },
|
{ key: 'admin-ia-map', title: '后台 IA 蓝图', description: '完整的后台信息架构映射,12 个顶级分区及子页面清单。', icon: 'fa fas fa-sitemap', color: 'var(--jio-color-primary)', file: 'admin-ia-map.md' },
|
||||||
{ key: 'legacy-gap-matrix', title: '对标差异矩阵', description: 'AKSRT 当前 → 原主题参考 → AKSRT 目标三层对照表。', icon: 'fa fas fa-columns', color: '#722ed1', file: 'legacy-gap-matrix.md' },
|
{ key: 'legacy-gap-matrix', title: '对标差异矩阵', description: 'Jio 当前 → 原主题参考 → Jio 目标三层对照表。', icon: 'fa fas fa-columns', color: '#722ed1', file: 'legacy-gap-matrix.md' },
|
||||||
{ key: 'frontend-page-matrix', title: '前端页面矩阵', description: '前端模板、CSS、JS 资产映射与覆盖范围。', icon: 'fa fas fa-desktop', color: '#13c2c2', file: 'frontend-page-matrix.md' },
|
{ key: 'frontend-page-matrix', title: '前端页面矩阵', description: '前端模板、CSS、JS 资产映射与覆盖范围。', icon: 'fa fas fa-desktop', color: '#13c2c2', file: 'frontend-page-matrix.md' },
|
||||||
{ key: 'manual-acceptance-baseline', title: '验收基线', description: '每个 Phase 的人工验收标准和检查清单。', icon: 'fa fas fa-clipboard', color: '#52c41a', file: 'manual-acceptance-baseline.md' },
|
{ key: 'manual-acceptance-baseline', title: '验收基线', description: '每个 Phase 的人工验收标准和检查清单。', icon: 'fa fas fa-clipboard', color: '#52c41a', file: 'manual-acceptance-baseline.md' },
|
||||||
{ key: 'feature-domain-map', title: '功能域地图', description: '业务域划分、依赖关系和模块边界说明。', icon: 'fa fas fa-map', color: '#fa8c16', file: 'feature-domain-map.md' },
|
{ key: 'feature-domain-map', title: '功能域地图', description: '业务域划分、依赖关系和模块边界说明。', icon: 'fa fas fa-map', color: '#fa8c16', file: 'feature-domain-map.md' },
|
||||||
@@ -117,35 +117,35 @@ onMounted(async () => {
|
|||||||
|
|
||||||
function openDoc(doc) {
|
function openDoc(doc) {
|
||||||
// Link to local docs directory (admin can open in new tab)
|
// Link to local docs directory (admin can open in new tab)
|
||||||
const base = window.aksrtAdminData?.siteUrl || window.location.origin
|
const base = window.jioAdminData?.siteUrl || window.location.origin
|
||||||
window.open(`${base}/wp-content/themes/AKSRT-theme/docs/${doc.file}`, '_blank')
|
window.open(`${base}/wp-content/themes/Jio/docs/${doc.file}`, '_blank')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-docs__grid {
|
.jio-docs__grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__card {
|
.jio-docs__card {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: box-shadow 0.2s;
|
transition: box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__card:hover {
|
.jio-docs__card:hover {
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__card-inner {
|
.jio-docs__card-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__card-icon {
|
.jio-docs__card-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@@ -155,38 +155,38 @@ function openDoc(doc) {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__card-body {
|
.jio-docs__card-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__card-title {
|
.jio-docs__card-title {
|
||||||
margin: 0 0 2px;
|
margin: 0 0 2px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--aksrt-color-text);
|
color: var(--jio-color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__card-desc {
|
.jio-docs__card-desc {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__card-arrow {
|
.jio-docs__card-arrow {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: #c0c4cc;
|
color: #c0c4cc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__section {
|
.jio-docs__section {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__action {
|
.jio-docs__action {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -195,31 +195,31 @@ function openDoc(doc) {
|
|||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__action i {
|
.jio-docs__action i {
|
||||||
color: var(--aksrt-color-primary);
|
color: var(--jio-color-primary);
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__action strong {
|
.jio-docs__action strong {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--aksrt-color-text);
|
color: var(--jio-color-text);
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__action p {
|
.jio-docs__action p {
|
||||||
margin: 0 0 4px;
|
margin: 0 0 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__action a {
|
.jio-docs__action a {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--aksrt-color-primary);
|
color: var(--jio-color-primary);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-docs__action a:hover {
|
.jio-docs__action a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,316 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="aksrt-page-container">
|
|
||||||
<PageHeader title="AI 连接配置" desc="配置 AI 生成能力的连接方式。优先使用 WordPress AI Connectors;也可填写 OpenAI 兼容接口。SEO 生成入口在文章「独立 SEO」框,摘要/标签入口在前台投稿页。" />
|
|
||||||
|
|
||||||
<div v-if="loadError" class="aksrt-ai__form-card aksrt-ai__load-error" role="alert">
|
|
||||||
<strong>加载失败:</strong>{{ loadError }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__status">
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon" :class="status.available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--off'">
|
|
||||||
<i class="fa fab fa-android"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">可用状态</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ status.available ? '可用' : '不可用' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon" :class="status.wp_ai_available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
|
||||||
<i class="fa fas fa-plug"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">WP AI Client</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ status.wp_ai_available ? '已就绪' : '不可用' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon" :class="status.theme_available ? 'aksrt-ai__status-icon--ok' : 'aksrt-ai__status-icon--warn'">
|
|
||||||
<i class="fa fas fa-key"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">主题 Provider</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ status.theme_available ? '已配置' : '未配置' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-card">
|
|
||||||
<div class="aksrt-ai__status-icon aksrt-ai__status-icon--info">
|
|
||||||
<i class="fa fas fa-road"></i>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-ai__status-info">
|
|
||||||
<div class="aksrt-ai__status-label">当前后端</div>
|
|
||||||
<div class="aksrt-ai__status-value">{{ backendLabel }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__form-card">
|
|
||||||
<h3>主题 Provider 配置(回退链路)</h3>
|
|
||||||
<p class="aksrt-form-hint">已配置主题 Provider 时优先走下方接口;未配置时才尝试 WordPress AI Connectors。NewAPI/兼容网关请填到 <code>/v1</code>(也可只填域名,保存时会自动补全)。</p>
|
|
||||||
<div class="aksrt-form-group">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" v-model="form.enabled"> 启用主题 AI Provider
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group">
|
|
||||||
<label>AI Provider</label>
|
|
||||||
<select v-model="form.provider" class="aksrt-select">
|
|
||||||
<option value="none">不使用</option>
|
|
||||||
<option value="openai">OpenAI 兼容接口</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>API Key</label>
|
|
||||||
<input v-model="form.api_key" type="password" class="aksrt-input" placeholder="sk-...">
|
|
||||||
<p class="aksrt-form-hint">保存后不会回显完整密钥</p>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider === 'openai'">
|
|
||||||
<label>API Base URL</label>
|
|
||||||
<input v-model="form.api_base" type="text" class="aksrt-input" placeholder="https://llm.example.com/v1">
|
|
||||||
<p class="aksrt-form-hint">示例:https://api.openai.com/v1 或 https://你的网关/v1(不要填到 /chat/completions)</p>
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>模型</label>
|
|
||||||
<input v-model="form.model" type="text" class="aksrt-input" placeholder="gpt-3.5-turbo">
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>最大 Token 数</label>
|
|
||||||
<input v-model.number="form.max_tokens" type="number" min="100" max="4000" class="aksrt-input">
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-group" v-if="form.provider !== 'none'">
|
|
||||||
<label>每分钟请求限制</label>
|
|
||||||
<input v-model.number="form.rate_limit" type="number" min="1" max="100" class="aksrt-input">
|
|
||||||
</div>
|
|
||||||
<div class="aksrt-form-actions">
|
|
||||||
<button class="aksrt-button aksrt-button--primary" :disabled="saving" @click="saveSettings">
|
|
||||||
{{ saving ? '保存中...' : '保存设置' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__form-card">
|
|
||||||
<h3>已接通能力</h3>
|
|
||||||
<table class="aksrt-table aksrt-table--sm">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>能力</th>
|
|
||||||
<th>入口</th>
|
|
||||||
<th>状态</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="feat in features" :key="feat.key">
|
|
||||||
<td>{{ feat.name }}</td>
|
|
||||||
<td>{{ feat.scene }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="aksrt-badge" :class="feat.ready ? 'aksrt-badge--success' : 'aksrt-badge--warning'">
|
|
||||||
{{ feat.ready ? '可用' : '未就绪' }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p class="aksrt-form-hint">SEO 开关与提示词请到「全局 → SEO」配置(seo_ai_enabled / ai_seo_*_prompt)。</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="aksrt-ai__form-card aksrt-ai__notice">
|
|
||||||
<h3><i class="fa fas fa-exclamation-triangle"></i> 重要说明</h3>
|
|
||||||
<ul>
|
|
||||||
<li>AI 输出仅作为<strong>建议</strong>,不会自动写库;需用户点「更新/提交」才保存</li>
|
|
||||||
<li>主题 API Key 已配置时优先使用;否则回退 WordPress AI Connectors</li>
|
|
||||||
<li>生成接口要求登录与编辑/投稿权限;配置接口仍需管理员</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { ref, reactive, computed, onMounted } from 'vue';
|
|
||||||
import PageHeader from '@/components/PageHeader.vue';
|
|
||||||
import api from '@/api';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'ExtensionAI',
|
|
||||||
components: { PageHeader },
|
|
||||||
setup() {
|
|
||||||
const status = ref({
|
|
||||||
enabled: false,
|
|
||||||
provider: 'none',
|
|
||||||
configured: false,
|
|
||||||
model: '',
|
|
||||||
api_key: '',
|
|
||||||
rate_limit: 10,
|
|
||||||
available: false,
|
|
||||||
backend: 'none',
|
|
||||||
wp_ai_available: false,
|
|
||||||
theme_available: false,
|
|
||||||
seo_ai_enabled: false,
|
|
||||||
write_assist: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = reactive({
|
|
||||||
enabled: false,
|
|
||||||
provider: 'none',
|
|
||||||
api_key: '',
|
|
||||||
api_base: 'https://api.openai.com/v1',
|
|
||||||
model: 'gpt-3.5-turbo',
|
|
||||||
max_tokens: 1000,
|
|
||||||
rate_limit: 10,
|
|
||||||
});
|
|
||||||
|
|
||||||
const saving = ref(false);
|
|
||||||
const loadError = ref('');
|
|
||||||
|
|
||||||
const backendLabel = computed(() => {
|
|
||||||
const map = { wp_ai: 'WP AI Client', theme: '主题 Provider', none: '无' };
|
|
||||||
return map[status.value.backend] || status.value.backend || '无';
|
|
||||||
});
|
|
||||||
|
|
||||||
const features = computed(() => {
|
|
||||||
const avail = !!status.value.available;
|
|
||||||
const seoReady = avail && !!status.value.seo_ai_enabled;
|
|
||||||
return [
|
|
||||||
{ key: 'seo', name: 'AI 生成 SEO', scene: '文章/分类「独立 SEO」框', ready: seoReady },
|
|
||||||
{ key: 'summary', name: 'AI 生成摘要', scene: '前台投稿页', ready: avail },
|
|
||||||
{ key: 'tags', name: 'AI 建议标签', scene: '前台投稿页', ready: avail },
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
function hydrateFromStatus(data) {
|
|
||||||
if (!data || typeof data !== 'object') return;
|
|
||||||
status.value = {
|
|
||||||
enabled: !!data.enabled,
|
|
||||||
provider: data.provider || 'none',
|
|
||||||
configured: !!data.configured,
|
|
||||||
model: data.model || '',
|
|
||||||
api_key: '',
|
|
||||||
rate_limit: data.rate_limit || 10,
|
|
||||||
available: !!data.available,
|
|
||||||
backend: data.backend || 'none',
|
|
||||||
wp_ai_available: !!data.wp_ai_available,
|
|
||||||
theme_available: !!data.theme_available,
|
|
||||||
seo_ai_enabled: !!data.seo_ai_enabled,
|
|
||||||
write_assist: !!data.write_assist,
|
|
||||||
};
|
|
||||||
form.enabled = !!data.enabled;
|
|
||||||
form.provider = data.provider || 'none';
|
|
||||||
form.model = data.model || 'gpt-3.5-turbo';
|
|
||||||
form.rate_limit = data.rate_limit || 10;
|
|
||||||
if (data.api_base) form.api_base = data.api_base;
|
|
||||||
if (typeof data.max_tokens === 'number' && data.max_tokens > 0) {
|
|
||||||
form.max_tokens = data.max_tokens;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadStatus() {
|
|
||||||
loadError.value = '';
|
|
||||||
try {
|
|
||||||
const json = await api.getAiStatus();
|
|
||||||
hydrateFromStatus(json.data || {});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Load AI status failed:', e);
|
|
||||||
loadError.value = e?.message || '加载 AI 状态失败';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveSettings() {
|
|
||||||
saving.value = true;
|
|
||||||
try {
|
|
||||||
const payload = {
|
|
||||||
enabled: form.enabled,
|
|
||||||
provider: form.provider,
|
|
||||||
api_base: form.api_base,
|
|
||||||
model: form.model,
|
|
||||||
max_tokens: form.max_tokens,
|
|
||||||
rate_limit: form.rate_limit,
|
|
||||||
};
|
|
||||||
// Only send api_key when the user entered a new one.
|
|
||||||
if (form.api_key && form.api_key !== '***') {
|
|
||||||
payload.api_key = form.api_key;
|
|
||||||
}
|
|
||||||
const json = await api.updateAiSettings(payload);
|
|
||||||
alert(json.message || '设置已保存');
|
|
||||||
await loadStatus();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Save failed:', e);
|
|
||||||
alert(e?.message || '保存失败');
|
|
||||||
} finally {
|
|
||||||
saving.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => loadStatus());
|
|
||||||
|
|
||||||
return { status, form, saving, loadError, features, backendLabel, saveSettings };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.aksrt-ai__status {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__status-card {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
}
|
|
||||||
.aksrt-ai__status-icon {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 8px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__status-icon--ok { background: #d4edda; color: #155724; }
|
|
||||||
.aksrt-ai__status-icon--warn { background: #fff3cd; color: #856404; }
|
|
||||||
.aksrt-ai__status-icon--off { background: #f8d7da; color: #721c24; }
|
|
||||||
.aksrt-ai__status-icon--info { background: #d1ecf1; color: #0c5460; }
|
|
||||||
.aksrt-ai__status-value { font-size: 18px; font-weight: 600; color: var(--aksrt-color-text); }
|
|
||||||
.aksrt-ai__status-label { font-size: 12px; color: #888; }
|
|
||||||
.aksrt-ai__form-card {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #e8e8e8;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 24px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__form-card h3 { margin: 0 0 16px; font-size: 16px; font-weight: 600; }
|
|
||||||
.aksrt-form-hint { font-size: 12px; color: #888; margin-top: 4px; }
|
|
||||||
.aksrt-ai__load-error {
|
|
||||||
background: #fdecea;
|
|
||||||
border-color: #f5c2c0;
|
|
||||||
color: #842029;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.aksrt-ai__notice { background: #fff8e1; border-color: #ffe082; }
|
|
||||||
.aksrt-ai__notice h3 { color: #e65100; }
|
|
||||||
.aksrt-ai__notice ul { margin: 0; padding-left: 20px; }
|
|
||||||
.aksrt-ai__notice li { margin-bottom: 8px; font-size: 14px; }
|
|
||||||
.aksrt-ai__notice code {
|
|
||||||
background: rgba(0,0,0,0.06);
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 3px;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
.aksrt-badge { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 12px; }
|
|
||||||
.aksrt-badge--success { background: #d4edda; color: #155724; }
|
|
||||||
.aksrt-badge--warning { background: #fff3cd; color: #856404; }
|
|
||||||
.aksrt-table { width: 100%; border-collapse: collapse; }
|
|
||||||
.aksrt-table th, .aksrt-table td { border-bottom: 1px solid #eee; padding: 10px 8px; text-align: left; }
|
|
||||||
.aksrt-form-group { margin-bottom: 14px; }
|
|
||||||
.aksrt-input, .aksrt-select { width: 100%; max-width: 480px; padding: 8px 10px; }
|
|
||||||
.aksrt-button { padding: 8px 16px; border: 0; border-radius: 6px; cursor: pointer; }
|
|
||||||
.aksrt-button--primary { background: #1677ff; color: #fff; }
|
|
||||||
.aksrt-button:disabled { opacity: .6; cursor: not-allowed; }
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-page-container">
|
<div class="jio-page-container">
|
||||||
<PageHeader :title="title" :desc="desc">
|
<PageHeader :title="title" :desc="desc">
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<n-space v-if="links.length">
|
<n-space v-if="links.length">
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<n-card v-if="note" class="aksrt-section-card">
|
<n-card v-if="note" class="jio-section-card">
|
||||||
<n-alert type="info" :show-icon="false">
|
<n-alert type="info" :show-icon="false">
|
||||||
{{ note }}
|
{{ note }}
|
||||||
</n-alert>
|
</n-alert>
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="aksrt-page-container">
|
|
||||||
<PageHeader
|
|
||||||
title="附件治理"
|
|
||||||
desc="管理各类上传文件的大小、安全限制和审核策略;准入权限请在“基本权限”中配置。"
|
|
||||||
>
|
|
||||||
<template #actions>
|
|
||||||
<n-space>
|
|
||||||
<n-button quaternary @click="goModeration">审核策略</n-button>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<n-spin :show="loading" style="width:100%; min-height:200px;">
|
|
||||||
<!-- Summary -->
|
|
||||||
<n-card class="aksrt-section-card">
|
|
||||||
<div class="aksrt-section-header">
|
|
||||||
<h3>上传策略总览</h3>
|
|
||||||
</div>
|
|
||||||
<n-grid :cols="3" :x-gap="12" :y-gap="12">
|
|
||||||
<n-grid-item v-for="item in summaryItems" :key="item.key">
|
|
||||||
<div class="aksrt-attach__summary">
|
|
||||||
<FaIcon :icon="item.icon" :size="16" :color="item.enabled ? '#52c41a' : 'var(--aksrt-color-muted)'" />
|
|
||||||
<div>
|
|
||||||
<strong>{{ item.label }}</strong>
|
|
||||||
<p>{{ item.enabled ? '已启用' : '已关闭' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</n-grid-item>
|
|
||||||
</n-grid>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<!-- Avatar & Cover -->
|
|
||||||
<n-card class="aksrt-section-card">
|
|
||||||
<div class="aksrt-section-header">
|
|
||||||
<h3>用户资料卡背景图</h3>
|
|
||||||
</div>
|
|
||||||
<n-form :model="form" label-placement="left" label-width="200" class="aksrt-form">
|
|
||||||
<n-form-item label="头像/用户资料卡背景图大小上限 (MB)">
|
|
||||||
<n-input-number v-model:value="form.avatar_max_size" :min="1" :max="10" style="max-width:160px;" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<!-- Content uploads -->
|
|
||||||
<n-card class="aksrt-section-card">
|
|
||||||
<div class="aksrt-section-header">
|
|
||||||
<h3>内容上传</h3>
|
|
||||||
</div>
|
|
||||||
<n-form :model="form" label-placement="left" label-width="200" class="aksrt-form">
|
|
||||||
<n-form-item label="投稿图片大小上限 (MB)">
|
|
||||||
<n-input-number v-model:value="form.submit_image_max_size" :min="1" :max="20" style="max-width:160px;" />
|
|
||||||
</n-form-item>
|
|
||||||
|
|
||||||
<n-form-item label="评论图片大小上限 (MB)">
|
|
||||||
<n-input-number v-model:value="form.comment_image_max_size" :min="1" :max="10" style="max-width:160px;" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<!-- Resource attachments -->
|
|
||||||
<n-card class="aksrt-section-card">
|
|
||||||
<div class="aksrt-section-header">
|
|
||||||
<h3>资源附件</h3>
|
|
||||||
</div>
|
|
||||||
<n-form :model="form" label-placement="left" label-width="200" class="aksrt-form">
|
|
||||||
<n-form-item label="资源附件大小上限 (MB)">
|
|
||||||
<n-input-number v-model:value="form.resource_max_size" :min="1" :max="200" style="max-width:160px;" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
|
|
||||||
<div class="aksrt-form-actions">
|
|
||||||
<n-button type="primary" :loading="saving" @click="handleSave">保存配置</n-button>
|
|
||||||
</div>
|
|
||||||
</n-spin>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onMounted, reactive, ref, computed } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
import api from '@/api'
|
|
||||||
import PageHeader from '@/components/PageHeader.vue'
|
|
||||||
import FaIcon from '@/components/form/FaIcon.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const loading = ref(true)
|
|
||||||
const saving = ref(false)
|
|
||||||
|
|
||||||
const form = reactive({
|
|
||||||
avatar_max_size: 2,
|
|
||||||
submit_image_max_size: 5,
|
|
||||||
comment_image_max_size: 2,
|
|
||||||
resource_max_size: 50,
|
|
||||||
})
|
|
||||||
|
|
||||||
const summaryItems = computed(() => [
|
|
||||||
{ key: 'profile', icon: 'fa fas fa-id-card', label: '资料卡图片', enabled: true },
|
|
||||||
{ key: 'submit', icon: 'fa fas fa-edit', label: '投稿图片', enabled: true },
|
|
||||||
{ key: 'comment', icon: 'fa fas fa-comment-dots', label: '评论图片', enabled: true },
|
|
||||||
{ key: 'resource', icon: 'fa fas fa-file-archive', label: '资源附件', enabled: true },
|
|
||||||
])
|
|
||||||
|
|
||||||
function go(path) {
|
|
||||||
router.push(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
function goModeration() { router.push('/permissions/moderation') }
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const permissionRes = await api.getPermissionsAttachmentSettings()
|
|
||||||
|
|
||||||
const permission = permissionRes?.data ?? permissionRes ?? {}
|
|
||||||
|
|
||||||
// Map permission fields
|
|
||||||
form.avatar_max_size = Number(permission.avatar_max_size || 2)
|
|
||||||
form.submit_image_max_size = Number(permission.submit_image_max_size || 5)
|
|
||||||
form.comment_image_max_size = Number(permission.comment_image_max_size || 2)
|
|
||||||
form.resource_max_size = Number(permission.resource_max_size || 50)
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || '加载附件权限失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSave() {
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
await api.savePermissionsAttachmentSettings({
|
|
||||||
avatar_max_size: form.avatar_max_size,
|
|
||||||
submit_image_max_size: form.submit_image_max_size,
|
|
||||||
comment_image_max_size: form.comment_image_max_size,
|
|
||||||
resource_max_size: form.resource_max_size,
|
|
||||||
})
|
|
||||||
message.success('附件治理配置已保存')
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || '保存失败')
|
|
||||||
} finally {
|
|
||||||
saving.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(load)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.aksrt-attach__summary {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-attach__summary strong {
|
|
||||||
display: block;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--aksrt-color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.aksrt-attach__summary p {
|
|
||||||
margin: 2px 0 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--aksrt-color-muted);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-page-container">
|
<div class="jio-page-container">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="基本权限 / 能力矩阵"
|
title="基本权限 / 能力矩阵"
|
||||||
desc="统一管理用户治理、投稿和评论的角色能力矩阵。"
|
desc="统一管理用户治理、投稿和评论的角色能力矩阵。"
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
|
|
||||||
|
|
||||||
<n-spin :show="loading" style="width: 100%; min-height: 240px;">
|
<n-spin :show="loading" style="width: 100%; min-height: 240px;">
|
||||||
<n-card class="aksrt-section-card" v-if="summary.cap_count">
|
<n-card class="jio-section-card" v-if="summary.cap_count">
|
||||||
<div class="aksrt-section-header">
|
<div class="jio-section-header">
|
||||||
<h3>矩阵概览</h3>
|
<h3>矩阵概览</h3>
|
||||||
</div>
|
</div>
|
||||||
<n-space>
|
<n-space>
|
||||||
@@ -28,12 +28,12 @@
|
|||||||
</n-space>
|
</n-space>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<n-card v-for="group in groups" :key="group.key" class="aksrt-section-card">
|
<n-card v-for="group in groups" :key="group.key" class="jio-section-card">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="aksrt-cap-matrix__header">
|
<div class="jio-cap-matrix__header">
|
||||||
<div>
|
<div>
|
||||||
<h3 class="aksrt-cap-matrix__title">{{ group.title }}</h3>
|
<h3 class="jio-cap-matrix__title">{{ group.title }}</h3>
|
||||||
<p class="aksrt-cap-matrix__desc">共 {{ group.items.length }} 项能力</p>
|
<p class="jio-cap-matrix__desc">共 {{ group.items.length }} 项能力</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
/>
|
/>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<div class="aksrt-form-actions">
|
<div class="jio-form-actions">
|
||||||
<n-button type="primary" :loading="saving" @click="handleSave">保存能力矩阵</n-button>
|
<n-button type="primary" :loading="saving" @click="handleSave">保存能力矩阵</n-button>
|
||||||
</div>
|
</div>
|
||||||
</n-spin>
|
</n-spin>
|
||||||
@@ -117,7 +117,7 @@ function buildColumns(group) {
|
|||||||
key: 'label',
|
key: 'label',
|
||||||
fixed: 'left',
|
fixed: 'left',
|
||||||
width: 280,
|
width: 280,
|
||||||
render: (row) => h('div', { class: 'aksrt-cap-matrix__cap' }, [
|
render: (row) => h('div', { class: 'jio-cap-matrix__cap' }, [
|
||||||
h('strong', row.label),
|
h('strong', row.label),
|
||||||
h('code', row.cap_id),
|
h('code', row.cap_id),
|
||||||
]),
|
]),
|
||||||
@@ -148,7 +148,7 @@ function buildColumns(group) {
|
|||||||
function renderCapCell(row, col) {
|
function renderCapCell(row, col) {
|
||||||
const hidden = !(row.visible_columns || []).includes(col)
|
const hidden = !(row.visible_columns || []).includes(col)
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
return h('span', { class: 'aksrt-cap-matrix__empty' }, '—')
|
return h('span', { class: 'jio-cap-matrix__empty' }, '—')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userCaps[row.cap_id]) {
|
if (!userCaps[row.cap_id]) {
|
||||||
@@ -209,40 +209,40 @@ async function handleSave() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-cap-matrix__header {
|
.jio-cap-matrix__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-cap-matrix__title {
|
.jio-cap-matrix__title {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-cap-matrix__desc {
|
.jio-cap-matrix__desc {
|
||||||
margin: 4px 0 0;
|
margin: 4px 0 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--aksrt-color-muted);
|
color: var(--jio-color-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-cap-matrix__cap {
|
.jio-cap-matrix__cap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-cap-matrix__cap code {
|
.jio-cap-matrix__cap code {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #667085;
|
color: #667085;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-cap-matrix__empty {
|
.jio-cap-matrix__empty {
|
||||||
color: #c0c4cc;
|
color: #c0c4cc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-form-actions {
|
.jio-form-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aks-native-ops-redirect">
|
<div class="jio-native-ops-redirect">
|
||||||
<h2>{{ title }}</h2>
|
<h2>{{ title }}</h2>
|
||||||
<p class="desc">
|
<p class="desc">
|
||||||
此运营功能已迁移到 WordPress 原生后台侧栏(对齐 legacy)。正在跳转…
|
此运营功能已迁移到 WordPress 原生后台侧栏(对齐 legacy)。正在跳转…
|
||||||
@@ -21,7 +21,7 @@ const title = computed(() => route.meta?.title || '运营功能已迁移')
|
|||||||
const nativeUrl = computed(() => {
|
const nativeUrl = computed(() => {
|
||||||
const path = route.meta?.nativeAdminPath || ''
|
const path = route.meta?.nativeAdminPath || ''
|
||||||
if (!path) return ''
|
if (!path) return ''
|
||||||
const base = (window.aksrtAdminData?.adminUrl || '/wp-admin/').replace(/\/?$/, '/')
|
const base = (window.jioAdminData?.adminUrl || '/wp-admin/').replace(/\/?$/, '/')
|
||||||
return base + path.replace(/^\//, '')
|
return base + path.replace(/^\//, '')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -33,13 +33,13 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aks-native-ops-redirect {
|
.jio-native-ops-redirect {
|
||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
margin: 48px auto;
|
margin: 48px auto;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: var(--aks-surface, #fff);
|
background: var(--jio-surface, #fff);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid var(--aks-border, #e5e7eb);
|
border: 1px solid var(--jio-border, #e5e7eb);
|
||||||
}
|
}
|
||||||
.desc {
|
.desc {
|
||||||
color: #646970;
|
color: #646970;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-page-container">
|
<div class="jio-page-container">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="主题授权"
|
title="主题授权"
|
||||||
desc="当前版本按产品决策不实现在线授权/撤销授权,仅保留主题信息与更新能力。"
|
desc="当前版本按产品决策不实现在线授权/撤销授权,仅保留主题信息与更新能力。"
|
||||||
@@ -12,14 +12,14 @@
|
|||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<n-card class="aksrt-section-card">
|
<n-card class="jio-section-card">
|
||||||
<n-alert type="info" title="按设计省略" :show-icon="true">
|
<n-alert type="info" title="按设计省略" :show-icon="true">
|
||||||
本主题当前不实现 原主题风格的授权码校验、域名绑定、撤销授权、本地免授权等机制。
|
本主题当前不实现 原主题风格的授权码校验、域名绑定、撤销授权、本地免授权等机制。
|
||||||
`AKSRT_Update_Checker` 仅负责匿名版本检测与更新提示,不与授权系统耦合。
|
`JIO_Update_Checker` 仅负责匿名版本检测与更新提示,不与授权系统耦合。
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<n-card class="aksrt-section-card" title="当前策略" size="small">
|
<n-card class="jio-section-card" title="当前策略" size="small">
|
||||||
<n-list bordered>
|
<n-list bordered>
|
||||||
<n-list-item>不提供授权码输入框或“立即授权”按钮</n-list-item>
|
<n-list-item>不提供授权码输入框或“立即授权”按钮</n-list-item>
|
||||||
<n-list-item>不提供“已授权 / 未授权 / 本地免授权”三态逻辑</n-list-item>
|
<n-list-item>不提供“已授权 / 未授权 / 本地免授权”三态逻辑</n-list-item>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="aksrt-page-container">
|
<div class="jio-page-container">
|
||||||
<PageHeader title="主题信息" desc="主题版本、运行环境、已启用模块和关键系统信息。" />
|
<PageHeader title="主题信息" desc="主题版本、运行环境、已启用模块和关键系统信息。" />
|
||||||
|
|
||||||
<div v-if="loading" class="aksrt-theme-info__loading">
|
<div v-if="loading" class="jio-theme-info__loading">
|
||||||
<n-spin size="medium" />
|
<n-spin size="medium" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<!-- Version Card -->
|
<!-- Version Card -->
|
||||||
<n-card class="aksrt-theme-info__card" title="版本信息" size="small">
|
<n-card class="jio-theme-info__card" title="版本信息" size="small">
|
||||||
<n-descriptions :column="2" bordered label-placement="left" size="small">
|
<n-descriptions :column="2" bordered label-placement="left" size="small">
|
||||||
<n-descriptions-item label="主题版本">
|
<n-descriptions-item label="主题版本">
|
||||||
<n-tag type="success" size="small">{{ info.themeVersion }}</n-tag>
|
<n-tag type="success" size="small">{{ info.themeVersion }}</n-tag>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- Environment Card -->
|
<!-- Environment Card -->
|
||||||
<n-card class="aksrt-theme-info__card" title="运行环境" size="small">
|
<n-card class="jio-theme-info__card" title="运行环境" size="small">
|
||||||
<n-descriptions :column="2" bordered label-placement="left" size="small">
|
<n-descriptions :column="2" bordered label-placement="left" size="small">
|
||||||
<n-descriptions-item label="站点地址">
|
<n-descriptions-item label="站点地址">
|
||||||
<n-text code>{{ info.siteUrl }}</n-text>
|
<n-text code>{{ info.siteUrl }}</n-text>
|
||||||
@@ -61,14 +61,14 @@
|
|||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- Modules Card -->
|
<!-- Modules Card -->
|
||||||
<n-card class="aksrt-theme-info__card" title="已启用模块" size="small">
|
<n-card class="jio-theme-info__card" title="已启用模块" size="small">
|
||||||
<div class="aksrt-theme-info__modules">
|
<div class="jio-theme-info__modules">
|
||||||
<n-tag
|
<n-tag
|
||||||
v-for="mod in info.modules"
|
v-for="mod in info.modules"
|
||||||
:key="mod.key"
|
:key="mod.key"
|
||||||
:type="mod.enabled ? 'success' : 'default'"
|
:type="mod.enabled ? 'success' : 'default'"
|
||||||
size="small"
|
size="small"
|
||||||
class="aksrt-theme-info__module-tag"
|
class="jio-theme-info__module-tag"
|
||||||
>
|
>
|
||||||
<FaIcon :icon="mod.enabled ? 'fa fas fa-check-circle' : 'fa fas fa-circle'" :size="10" />
|
<FaIcon :icon="mod.enabled ? 'fa fas fa-check-circle' : 'fa fas fa-circle'" :size="10" />
|
||||||
{{ mod.label }}
|
{{ mod.label }}
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- Data Tables Card -->
|
<!-- Data Tables Card -->
|
||||||
<n-card class="aksrt-theme-info__card" title="数据表状态" size="small">
|
<n-card class="jio-theme-info__card" title="数据表状态" size="small">
|
||||||
<n-data-table
|
<n-data-table
|
||||||
:columns="tableColumns"
|
:columns="tableColumns"
|
||||||
:data="info.tables"
|
:data="info.tables"
|
||||||
@@ -91,25 +91,25 @@
|
|||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- Docs Links Card -->
|
<!-- Docs Links Card -->
|
||||||
<n-card class="aksrt-theme-info__card" title="快速入口" size="small">
|
<n-card class="jio-theme-info__card" title="快速入口" size="small">
|
||||||
<div class="aksrt-theme-info__links">
|
<div class="jio-theme-info__links">
|
||||||
<router-link to="/docs" class="aksrt-theme-info__link">
|
<router-link to="/docs" class="jio-theme-info__link">
|
||||||
<FaIcon icon="fa fas fa-book" :size="16" />
|
<FaIcon icon="fa fas fa-book" :size="16" />
|
||||||
<span>文档中心</span>
|
<span>文档中心</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/tools/update" class="aksrt-theme-info__link">
|
<router-link to="/tools/update" class="jio-theme-info__link">
|
||||||
<FaIcon icon="fa fas fa-cloud-download-alt" :size="16" />
|
<FaIcon icon="fa fas fa-cloud-download-alt" :size="16" />
|
||||||
<span>更新中心</span>
|
<span>更新中心</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/tools/backup" class="aksrt-theme-info__link">
|
<router-link to="/tools/backup" class="jio-theme-info__link">
|
||||||
<FaIcon icon="fa fas fa-archive" :size="16" />
|
<FaIcon icon="fa fas fa-archive" :size="16" />
|
||||||
<span>数据备份</span>
|
<span>数据备份</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/tools/health" class="aksrt-theme-info__link">
|
<router-link to="/tools/health" class="jio-theme-info__link">
|
||||||
<FaIcon icon="fa fas fa-heartbeat" :size="16" />
|
<FaIcon icon="fa fas fa-heartbeat" :size="16" />
|
||||||
<span>健康检查</span>
|
<span>健康检查</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link to="/tools/import-export" class="aksrt-theme-info__link">
|
<router-link to="/tools/import-export" class="jio-theme-info__link">
|
||||||
<FaIcon icon="fa fas fa-exchange-alt" :size="16" />
|
<FaIcon icon="fa fas fa-exchange-alt" :size="16" />
|
||||||
<span>导入导出</span>
|
<span>导入导出</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -173,13 +173,13 @@ onMounted(async () => {
|
|||||||
const data = res?.data || res || {}
|
const data = res?.data || res || {}
|
||||||
|
|
||||||
info.value = {
|
info.value = {
|
||||||
themeVersion: window.aksrtAdminData?.themeVersion || data.theme_version || '3.4.0-dev',
|
themeVersion: window.jioAdminData?.themeVersion || data.theme_version || '3.4.0-dev',
|
||||||
wpVersion: data.wp_version || '',
|
wpVersion: data.wp_version || '',
|
||||||
phpVersion: data.php_version || '',
|
phpVersion: data.php_version || '',
|
||||||
dbSchemaVersion: data.db_schema_version || '',
|
dbSchemaVersion: data.db_schema_version || '',
|
||||||
emailBackfillDone: !!data.email_backfill_done,
|
emailBackfillDone: !!data.email_backfill_done,
|
||||||
siteUrl: data.site_url || window.aksrtAdminData?.siteUrl || '',
|
siteUrl: data.site_url || window.jioAdminData?.siteUrl || '',
|
||||||
apiUrl: data.api_url || window.aksrtAdminData?.apiUrl || '',
|
apiUrl: data.api_url || window.jioAdminData?.apiUrl || '',
|
||||||
themeDir: data.theme_dir || '',
|
themeDir: data.theme_dir || '',
|
||||||
uploadDir: data.upload_dir || '',
|
uploadDir: data.upload_dir || '',
|
||||||
isMultisite: !!data.is_multisite,
|
isMultisite: !!data.is_multisite,
|
||||||
@@ -191,8 +191,8 @@ onMounted(async () => {
|
|||||||
console.error('[ThemeInfo] Failed to load health data:', err)
|
console.error('[ThemeInfo] Failed to load health data:', err)
|
||||||
// Fallback with minimal info
|
// Fallback with minimal info
|
||||||
info.value.themeVersion = '3.4.0-dev'
|
info.value.themeVersion = '3.4.0-dev'
|
||||||
info.value.siteUrl = window.aksrtAdminData?.siteUrl || ''
|
info.value.siteUrl = window.jioAdminData?.siteUrl || ''
|
||||||
info.value.apiUrl = window.aksrtAdminData?.apiUrl || ''
|
info.value.apiUrl = window.jioAdminData?.apiUrl || ''
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -218,35 +218,35 @@ function normalizeTables(raw) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.aksrt-theme-info__loading {
|
.jio-theme-info__loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 60px 0;
|
padding: 60px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-theme-info__card {
|
.jio-theme-info__card {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-theme-info__modules {
|
.jio-theme-info__modules {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-theme-info__module-tag {
|
.jio-theme-info__module-tag {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-theme-info__links {
|
.jio-theme-info__links {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-theme-info__link {
|
.jio-theme-info__link {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -259,7 +259,7 @@ function normalizeTables(raw) {
|
|||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.aksrt-theme-info__link:hover {
|
.jio-theme-info__link:hover {
|
||||||
background: #e8eaed;
|
background: #e8eaed;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Loaded 100 of 1363 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user