Compare commits

...
68 Commits
Author SHA1 Message Date
kate522andClaude Opus 5 5f31c58df2 fix(front-submit): let drafts save with partial content
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-07 00:58:13 +08:00
kate522andClaude Opus 5 4a119358ca feat(creator-studio): bottom status bar, editor-rendered math/footnote, code language picker, block delete
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-07 00:38:39 +08:00
kate522andClaude Opus 5 6928127bfa fix(creator-studio): render math/footnote shortcodes, inline code blocks, block deletion
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 23:52:39 +08:00
kate522andClaude Opus 5 7885f42cff feat(creator-studio): dialog-driven table/math/footnote, theme quote, code language picker
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 23:23:59 +08:00
kate522andClaude Opus 5 7c33482be7 feat(creator-studio): icon toolbar with block tools, drop author strip
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 22:30:08 +08:00
kate522andClaude Opus 5 7bdf7cf018 feat(creator-studio): centered paper layout and scheduled publishing
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 21:33:40 +08:00
kate522andClaude Opus 5 384d2b7aa0 feat(creator-studio): switch writing editor to TinyMCE
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 20:51:18 +08:00
kate522andClaude Opus 5 4868de2cda fix(creator-studio): 加载正文编辑格式工具栏
选中文字时的 RichText 加粗、斜体、链接等工具由 WordPress format-library 注册。
BlockTools 虽然提供 inline toolbar slot,但此前前台没有加载 wp-format-library,
因此正文选区没有格式工具。为 Creator Studio 前台显式加载该 script/style handle,
并将行内格式工具栏统一为深色圆角悬浮样式。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 17:59:17 +08:00
kate522andClaude Opus 5 9e8913ea0a fix(creator-studio): 固定公众号式创作工具栏
- 顶部改为 fixed 创作工具栏:左侧返回/品牌/模式,中间文章标题与保存状态,
  右侧撤销/重做、保存草稿、预览、发布
- 新增工作台级撤销/重做快照栈,连续编辑合并记录;支持 Ctrl/Cmd+Z / Shift+Z,
  不覆盖 RichText 输入框自身的编辑历史
- 移动端收敛工具栏布局,保留撤销/重做、预览、发布
- 先保存草稿再请求审核/直接发布;请求使用服务端更新后的文章与版本 ID
- 固定工具栏以独立 chrome 层布局,正文避开顶部工具栏高度
- 补充固定工具栏、历史操作与键盘快捷键契约

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 17:36:15 +08:00
kate522andClaude Opus 5 f5e9420266 feat(creator-studio): 图片宽度四档与作者行
- 通过 editor.BlockEdit 过滤器给 core/image 增加「半屏 / 中等 / 通栏 / 全宽」
  四档宽度,选中图片时出现在区块工具条;宽度写入 className 并序列化到
  post_content,前台样式即可生效,无需自定义区块或额外元数据
- 前台样式:半屏靠左 50%,中等 72%,通栏跟随正文行宽,全宽突破行宽但受容器
  内边距约束;移动端半屏与全宽回退为整行,避免横向滚动
- 新增作者行(头像 + 昵称 + 身份说明),数据由 CreatorRest::author_payload()
  从服务端给出,前端不自行拼装;无昵称时整行不渲染,头像缺失时回退首字占位
- 作者行放在摘要之后、正文之前

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 16:05:19 +08:00
kate522andClaude Opus 5 79909f449e feat(creator-studio): 公众号式插入面板与封面条
- 新增常驻「添加内容」按钮与插入面板:分组网格列出正文/标题/列表/引用、
  图片/视频/嵌入、代码/分隔线/表格/付费内容;条目不满足能力时不出现在面板里
- 面板支持方向键在网格内移动焦点、Esc 关闭;桌面居中浮层,移动端贴底抽屉并
  留出安全区
- 空文档插入区块时替换初始空白段落,不留空行
- 新增封面条:无封面时给低调入口,有封面时显示 2.35:1 缩略图,悬停出现
  「更换封面」,右上角可移除
- 补充契约测试:头部/页脚/挂件区开关、插入面板真实区块名与能力过滤、
  中文字数口径

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 15:57:44 +08:00
kate522andClaude Opus 5 d07cffe99e fix(creator-studio): 沉浸式布局与公众号式写作体验
首轮实机体验暴露的问题:

- 投稿页仍在渲染主题头部的 all_top_fluid 挂件区、页脚与浮动入口,
  与工作台顶栏重复。header.php / footer.php 新增 jio_render_header、
  jio_render_footer、jio_render_fluid_widgets 过滤器,工作台模式下全部关闭
- 区块 Inspector 被写在 BlockList 之后,作为正文流的一部分渲染出
  一个巨大的「未选择区块」白框;移除内联 Inspector(区块设置改由
  行内工具栏的区块菜单提供)
- 中文正文被 wordcount 的 'words' 口径算成「字数:1」;改用
  characters_excluding_spaces,与前台 TinyMCE 字数一致
- 行宽 820px、标题 34px 偏松散;收窄到 720px / 27px,正文 16px/1.85,
  段落间距与选区描边收敛,行内工具栏改为深色圆角悬浮条

新增本地视觉验收脚本(不参与运行时):
- scripts/preview.cjs:用模拟 bootstrap 生成离线预览页
- scripts/shoot.cjs:桌面/移动端截图与控制台错误采集

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 15:49:57 +08:00
kate522andClaude Opus 5 2c7262770b fix(creator-studio): 修复白屏与前台 REST 鉴权
白屏根因:CreatorRest / SubmissionVersion 位于 Jio\Core 命名空间,
但调用 Jio\Content\PrivateAssets 时缺少 use 导入,PHP 就近解析为不存在的
Jio\Core\PrivateAssets,在 wp_head 中抛致命错误导致页面输出截断。

- 为 CreatorRest、SubmissionVersion 补上 use Jio\Content\PrivateAssets
- 新增前台 api-fetch nonce 中间件:WordPress 只在后台注入 wpApiSettings,
  前台工作台里 Gutenberg core-data 请求(/wp/v2/users/me)因此全部 403
- 提交前先落一次草稿,避免自动保存未触发时发布到旧内容
- 新增 tools/check-class-refs.php:静态检测命名空间内未导入的裸类引用
- 新增 assets/creator-studio/scripts/smoke.cjs:浏览器冒烟脚本,
  抓控制台错误、失败请求与未捕获的 Promise 拒绝

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 15:39:10 +08:00
kate522andClaude Opus 5 fac4a714e0 feat(creator-studio): 前台 Gutenberg 创作工作台与受保护付费附件
前台投稿页改为沉浸式创作工作台:Gutenberg 负责区块解析与序列化,
React + TypeScript 负责工作台外壳与发布抽屉,Jio 服务端继续作为权限、
审核与付费的唯一权威。

- 新增 assets/creator-studio 独立构建(wp-scripts,锁定 WP 6.4 时代包),
  仅在投稿模板加载,构建缺失或游客访问时回退原有 TinyMCE 表单
- 自动保存(4s 防抖 + rev 冲突 409)、本地快照恢复、发布配置抽屉、
  移动端全屏单列与安全区适配
- 图片插入走现有 front-submit 上传管线,保留能力校验与待认领生命周期
- 已发布文章的改稿写入私有版本,审核通过前线上内容/链接/付费条款不变;
  原生后台「改稿审核」一次性应用或退回,并校验源文章并发指纹
- 私有附件新表 jio_private_asset + jio_dl_token.asset_id,schema 升到 3.4.0;
  文件存放 ABSPATH 之外,未配置私有存储时 fail closed
- 下载令牌绑定文章/附件/用户/IP/UA/次数,每次交付复核 Visibility 的
  订单、VIP 与配额判定;中断传输归还次数
- 付费字段改为写入前校验,非法价格不再留下半保存文章
- 新增 creator-studio 契约测试;更新受影响的 schema/下载/投稿契约

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 14:53:23 +08:00
kate522andClaude Opus 5 7a8279f8e7 fix(layout): pass term object to archive settings
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-06 00:00:17 +08:00
kate522andClaude Opus 5 fbb53c6890 feat(categories): align archive and directory pages
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-05 23:51:33 +08:00
kate522andClaude Opus 5 7faa2d53d8 fix(admin): improve native link manager filters
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-05 22:19:50 +08:00
kate522andClaude Opus 5 b124795d84 fix(links): use plus icon for logo upload placeholder
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-05 21:41:09 +08:00
kate522andClaude Opus 5 f53cdd4dc8 feat(links): align the friends page layout and submit UI
- assemble the friends template with page header, search cover, content position, widget zones and comments
- render the three page title styles and the tabbed search cover
- restore card, category nav and submit modal markup for the friends page
- add page content style support to the page editor
- port the linked card, category nav and submit modal styles to the mobile and desktop breakpoints

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-05 21:09:40 +08:00
kate522andClaude Opus 5 4c562428b4 feat(links): move friend links to native WordPress links with review flow
- enable the native Links manager and drop the custom friend-link option/REST path
- add per-page link settings for the friends template
- render link categories from get_bookmarks() with search and page-content options
- add a validated public link submission endpoint with logo upload, captcha, rate limiting and duplicate checks
- create pending links for review and notify administrators by email and in-app message
- add native Links admin pending count, columns and edit notes
- cover the new submission and cleanup contracts

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-05 19:54:33 +08:00
kate522andClaude Opus 5 ef7b5939ba chore(assets): refresh the site logo
Same 2172x724 artwork, re-exported at 29,609 bytes (down from 41,469).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-05 18:30:11 +08:00
kate522andClaude Opus 5 6bcfbfb252 feat(branding): default both site logos to assets/img/logo.png
- Point 站点 Logo / 暗色模式 Logo schema defaults (PHP + VUE) and the
  Settings::logo_url() fallback at assets/img/logo.png.
- Point the footer night image default at the same file.
- Remove logo_dark.png and the now-unreferenced assets/icons/default-logo.svg
  along with the private default_asset_url() helper that was its only reader.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-05 18:02:51 +08:00
kate522andClaude Opus 5 1dfcc38a83 chore(assets): ship the new site logo
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-05 17:59:19 +08:00
kate522andClaude Opus 5 8e74b60570 fix(navigation): let CSS size the nav logo and drop the inline width cap
The navbar logo carried an inline `height:36px;width:auto;max-width:{logo_width}px`
while `.navbar-logo > img` already owns `height:36px;padding:0 30px`. Under
`box-sizing:border-box` that max-width capped the border box, so the 60px of
horizontal padding stayed inside it and wide logos were squeezed against a
fixed height, breaking the intrinsic aspect ratio.

- Render the nav logo with no inline sizing; CSS alone sizes it.
- Add a `$lazy` argument to jio_get_adaptive_theme_img() so the navbar can emit
  the real src up front instead of a square lazyload placeholder; defaults to
  'auto' so every other caller keeps the lazy_other behaviour.
- Remove the superseded logo_width plumbing: the setting (PHP + VUE schemas),
  Settings::logo_width(), jio_get_site_logo_width(), the --jio-logo-width token
  and the unused has_logo context key.
- Cover the contract in the header reference test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-05 17:50:56 +08:00
kate522andClaude Opus 5 3ce5bfc20c feat(branding): default the site icons to assets/img/favicon.png
- Point the 浏览器图标 / 移动端图标 schema defaults (PHP + VUE) at
  assets/img/favicon.png instead of an empty media field.
- Fall back to the same PNG in Settings::favicon_url() when no custom
  favicon and no native site_icon are configured.
- Add Settings::mobile_icon_url() and emit apple-touch-icon from SEOHead;
  the 移动端图标 field previously had no consumer and never reached the page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-05 17:00:50 +08:00
kate522andClaude Opus 5 b56b9bbe37 fix(navigation): restore the reference 8px radius and unclip the mega menu
Traced against the running site (docker theme at AKSRT-theme) with real
computed styles:

- theme_main_radius defaulted to 12, so --main-radius was 12px and every
  dropdown panel, card and button was rounder than the reference, which
  sets 8px. Default is now 8 in the runtime reader and both schemas.
- The public bundle loads after main.min.css. Its bare img rule replaced
  the inline image baseline the menu relies on, and html{overflow-x} made
  the root a clipping context that crops the fixed senior mega menu.
  Reset is now scoped to content; the horizontal-overflow guard is on body.
- The senior mega menu renders a .container inside the dropdown where the
  reference keeps Bootstrap's 15px gutter; the padding-free site-shell
  .container rule stripped it, running the card/link grid to the edges.

Rebuilt public.css / public.min.css and the admin SPA.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-05 12:33:10 +08:00
kate522andClaude Opus 5 ed38dec314 fix(navigation): stop the public reset from overriding the reference menu styles
The public bundle loads after main.min.css, and its bare-element rules
won on equal specificity:

- img { display: block; height: auto } replaced the inline image baseline
  the menu relies on (Bootstrap img{vertical-align:middle} + main.css
  img{max-height:100%}). Menu glyphs and graphic-card thumbnails were
  pulled out of the inline flow, breaking row height and baseline.
  Scoped the reset to content and restored the inline baseline inside
  .header / .mobile-header.
- html { overflow-x: clip/hidden } made the root a clipping context,
  which crops the position:fixed senior mega menu and its drop panels.
  The horizontal-overflow guard now lives on body only.

Rebuilt public.css / public.min.css.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-05 12:04:31 +08:00
kate522andClaude Opus 5 3595935475 refactor(moderation): remove the Akismet backend and its admin fields
The comment moderation pipeline no longer calls Akismet. Removed:
- check_akismet() and the Akismet step in Moderation::process().
- akismet_* config keys from Moderation defaults and ExtensionRuntime.
- akismet decision state, the jio_moderation_akismet / akismet_result
  comment meta writes, and the legacy-meta fallbacks in the comments column.
- The Akismet fieldset in both the PHP and Vue permissions schemas.

Rate limit, GeoIP, blocked keywords and AI moderation are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-05 11:27:24 +08:00
kate522andClaude Opus 5 e6d9ff49a2 fix(navigation): align mega-menu rendering and menu-item backend with the reference
Renderer:
- Graphic-card images carry fit-cover so they crop into the fixed card box
  instead of stretching it and pushing the title out of alignment.
- Mega-menu link icons use fa-fw (fixed glyph column) instead of the
  undefined menu-item-icon class, so link titles line up.

Backend:
- Restore show_policy (login / vip / vip_2 / auth) on menu items; the field
  was missing entirely, so items could not be gated by identity.
- Default submenu_type to graphic_card, badge_class to jb-red, arrangement
  to swiper, size to md.
- Clamp img_scale to 20-300 to match the reference's spinner bounds.
- Drop the now-meaningless "none" submenu_type option: the senior-submenu
  switch is itself the on/off control.
- Admin JS gains the align control (left/center/space-around).

Cleanup:
- Remove the unused header_actions nav menu location.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-05 10:51:36 +08:00
kate522 55423403a6 fix qcode default 2026-11-04 21:27:05 +08:00
kate522 8c4aabb9a4 fix(withdrawal): wire the channel hint to the actual withdrawal modal
The previous commit put the channel-switching code in the wrong init block.
credits.js initializes two modals side by side — the points recharge dialog and
the withdrawal dialog — and the snippet landed in the recharge one, which has
no [data-jio-withdraw-method] element at all. Both symptoms followed from that:
methodField resolved to null so no change listener was ever bound, and the
initial sync never ran, leaving the note paragraph permanently hidden.

The block now lives inside initWithdraw(), and the stray copy is gone. The
behaviour itself was already correct — switching channels swaps the account
placeholder and shows or hides the channel note — it just never executed.

Verified in a headless browser against the real markup and stylesheet: the
placeholder starts as the first channel's hint and the note is hidden; switching
to the second channel swaps the placeholder and reveals the note as
display:block; switching back hides it again as display:none.

The contract test previously only checked that the code existed, which is why
it missed a placement bug that made the whole block dead. It now asserts
syncChannelHint appears exactly once, sits after initWithdraw(), and does not
run past the next function definition. 873 tests pass.
2026-11-04 19:37:56 +08:00
kate522 dcd306b428 feat(withdrawal): make payout channels a configurable sortable list
The payout channel was a two-item checkbox (alipay / wechat). Sites that pay
out through a bank transfer, USDT or any other route had no way to express it,
and every surface behind it — the withdrawal modal, the server-side check, the
admin review list — hard-coded those two names.

affiliate_payout_channels is now a repeater on both the PHP and Vue schemas,
using the same draggable component as the post filters and gateway config. Each
row carries a stable key, a display label, an account placeholder, an optional
note and an enabled flag. Disabled channels disappear from the front end but
stay resolvable, so historical withdrawal records still render their original
name.

PaymentRuntime::payout_channels() normalises the stored structure: it drops rows
with an empty key or label, de-duplicates keys, and falls back to the built-in
two channels when nothing is configured. enabled_payout_channels() feeds the
front end and payout_channel() resolves a single key for record display.
CommissionLedger's validation now compares against the configured, enabled keys
instead of a literal allowlist.

The withdrawal modal renders the configured list and swaps the account
placeholder and note when the channel changes; credits.js drives that. The
admin review page reads the same settings for its method labels and filter, and
the review dialog gained a "渠道说明" row so the operator sees the payout
instruction, hidden when empty.

Default channels live in SchemaPayment as plain data — the runtime imports them
rather than the reverse, because the schema layer must not depend on a runtime
settings class (doing it the other way made the schema harness fatal).

Verification: drove payout_channels() through default / custom / dirty / duplicate
inputs and confirmed enabled filtering, note lookup and disabled-channel
resolution. Added payment-withdrawal-channels-contract.test.cjs covering the
schema shape, runtime helpers, front-end wiring and admin read path. 873 tests
pass, PSR-4 audit clean, admin SPA builds.
2026-11-04 19:16:39 +08:00
kate522 bcba0f598a fix(admin): stop the browser autofilling credentials into the settings search box
A settings screen renders twenty-odd AppSecret and private-key fields as
type="password". Chrome reads any page carrying a password input as a login
form and fills the saved username and password into the first text box it
finds — the settings search field at the top of the panel.

Suppressing autocomplete on that one input could not fix it, because the
trigger is the password inputs elsewhere on the page. Secret fields now render
as text with CSS masking (-webkit-text-security: disc), so they still look
like ciphertext but no longer register as password inputs; the mask lifts on
focus so a freshly typed value can be checked. That covers the 21 schema-driven
fields through JioField plus the four captcha provider forms and the WeChat
template page.

The search box keeps a second layer of defence: a descriptive name and
autocomplete/autocorrect/spellcheck disabled, with the LastPass and 1Password
ignore markers set.

Verification: vite build succeeds and the emitted bundles no longer contain a
single type="password"; 980 admin/widget tests pass.

Note: assets/admin/build/ is gitignored, so the production build must be run on
deploy. -webkit-text-security is WebKit/Blink only — Firefox falls back to
plain text.
2026-11-04 18:53:44 +08:00
kate522 b1c3d86e44 refactor(oauth): drop the hand-written providers and keep only what the SDK ships
The previous commit added five direct clients (Douyin, Microsoft, Facebook,
Twitter, Apple) written against each platform's own endpoints. Maintaining
five bespoke OAuth implementations — including Apple's ES256 client secret and
Twitter's PKCE — is maintenance the bundled yurunsoft SDK does not ask for,
and none of them could be verified without production credentials.

Only providers yurunsoft/yurun-oauth-login actually implements stay on the
direct surface: GitHub, Gitee, QQ, Google, Weibo, Weixin, Baidu and Alipay.
Baidu and Alipay move into OAuthProviderBroker's existing client() switch and
normalize() map rather than living in a separate class, so there is no new
layer to reason about.

OAuthProviderClient, its harness and its contract test are deleted outright
(-809 lines). The Apple form_post handling added to handle_callback() is
reverted too — without Apple there is no POST callback to read.

The aggregate-login channel keeps its 16 platforms, matching the reference;
that list describes what the aggregation provider offers, not a local
implementation. Brand icons for those names stay for the same reason.

Verification: 869 widget/navigation tests pass, the PSR-4 audit reports 0
errors, and the catalog constant, PHP schema and Vue schema now list the same
16 aggregate platforms.
2026-11-04 18:33:37 +08:00
kate522 6db34646ed feat(oauth): add the seven missing providers and open the full aggregate list
The theme shipped six direct providers while the reference carries fourteen,
and its aggregate-login channel offered three of sixteen platforms.

OAuthProviderClient now owns the providers the bundled SDK does not cover.
Douyin, Microsoft, Facebook, Twitter and Apple talk to their documented OAuth2
endpoints directly; Baidu and Alipay reuse yurunsoft, which already ships both
(Alipay needs its application private key, so provider_config learned an
extra_keys map to surface it). The broker dispatches through handles(), so the
existing start/callback routes stay provider-agnostic.

Three provider quirks needed handling rather than a plain copy:

Apple returns the callback as response_mode=form_post, and handle_callback
only read $_GET, so it now reads $_POST when the request is a POST — otherwise
every Apple login would fail with code_missing. Its client secret is built as
an ES256 JWT from Team ID / Key ID / .p8 rather than a static string.

Twitter requires PKCE. The verifier is derived from the state token, so it
travels with the existing transaction row and needs no extra storage.

Facebook hands back protocol-less avatar URLs, which are upgraded to https the
same way the OAuth avatar resolver does.

Settings gained matching toggles and credential groups on both sides — the PHP
schema and the Vue schema — and the aggregate platform list went from three to
sixteen, with CLoginClient's hard-coded allowlist replaced by the catalog
constant. Brand icons were extended so each new provider resolves to its
sprite; facebook and twitter moved to their colour sprites, and the
`twitter -> x` alias was dropped since it bypassed the new mapping.

The oauth-security contract previously asserted that Baidu and Alipay must not
appear on the OAuth surface. That was the old intent, so the assertion is
replaced with a positive one, keeping the separate check that the Alipay
payment gateway still exists.

Verification: a new harness stubs the HTTP layer and drives all five custom
clients — authorization URLs carry redirect/state (Twitter also a PKCE
challenge), identities normalize to the shared shape, Facebook's avatar is
upgraded to https, Apple's private-relay email survives, and an error payload
throws instead of yielding an empty identity. 871 widget/navigation tests pass
and the PSR-4 audit reports 0 errors.

Not covered: no live handshake happened — every provider needs real AppID and
secret pairs, and Apple/Douyin require developer-account review before the
full round trip can be exercised.
2026-11-04 18:00:29 +08:00
kate522 6f29e3d865 fix(oauth): actually use the third-party avatar and seed it only once
The OAuth flow wrote the provider avatar to `jio_oauth_avatar`, but nothing
ever read it — every user who signed up through GitHub/QQ saw a Gravatar hash
or the default avatar instead of their own picture. The reference seeds its
`custom_avatar` from the OAuth profile and its resolver reads it back, so both
halves were missing here.

jio_get_custom_avatar_url() now falls back to the stored OAuth avatar when no
uploaded one exists, which puts the chain at uploaded → third-party → external
service → default. The theme already swaps WordPress's get_avatar_data, so one
change covers every avatar surface.

Writes go through a single seed_avatar() guard: it no-ops when an uploaded
avatar or an existing OAuth avatar is present, matching the reference's
`if ($args['avatar'] && !$custom_avatar)`. Previously both the bind path and
the login refresh overwrote the meta unconditionally, which would have wiped a
user's own picture on their next social login.

Because the OAuth avatar now shows through once the upload is gone, both
"restore default" flows (the REST endpoint and the admin profile box) delete
it too; the admin checkbox also appears when only an OAuth avatar is set,
since $avatar_url is now non-empty in that case.

Baidu serves avatars from tb.himg.baidu.com, which 403s — rewritten to
himg.bdimg.com the way the reference does. On top of that the URL is upgraded
to https when the site runs over SSL, because the reference's scheme-relative
form still yields a plain http:// when copied verbatim, and browsers block it
as mixed content.

WeChatShare was reading `custom_avatar` — a reference-theme key this codebase
never writes — so its author branch always fell through to get_avatar_url();
it now uses the shared resolver like every other surface.

Verification: drove the resolver through five states with a stub (none /
OAuth only / both / upload only / Baidu legacy host) and confirmed the
expected URL each time. Added oauth-avatar-contract.test.cjs to pin the
resolution order, the seed guard, and the reset cleanup. 868 widget and
navigation tests pass; the PSR-4 path audit reports 0 errors.
2026-11-04 17:41:48 +08:00
kate522 7d78a33a9d fix(user-center): match the reference meta pills on the profile header card
The username and e-mail entries under the profile header rendered as plain
grey text on a solid icon. The reference makes each one a bordered pill that
copies its own value on click, drawn with the outline glyphs.

Both entries now carry `.but` plus `data-clipboard-tag` / `data-clipboard-text`
so the existing frontend clipboard runtime (assets/js/runtime/libs/clipboard.js)
picks them up and toasts "用户名已复制". Icons move from `fas` to `far`, which
is the FA5 equivalent of the reference's `fa-user-o` / `fa-envelope-o`.

Geometry follows `.author-header .user-identity > .but/.badg` exactly:
padding 2px 6px, margin 5px 6px 0 0, 12px type, 1.4 line height, and the meta
row gets the reference's 520px max width. medal.css already carried its own
approximation of this pill (2px 8px 2px 0, .3em .6em, 1.44) for the medal
entry; it now uses the same numbers so the two entries line up. The base rule
lives in layout.css rather than medal.css because the medal module is only
enqueued when badges are enabled, and the header must keep its shape either
way.

Verification: rendered the real header through a PHP stub and measured the
computed styles in the browser — padding 2px 6px, margin 5px 6px 0 0, 12px /
16.8px, 23px tall, both entries carrying the right clipboard payload. Added
user-center-header-meta-contract.test.cjs to pin the markup and geometry;
864 widget/navigation tests pass.
2026-11-04 17:27:51 +08:00
kate522 6d01e7489a fix(user-center): use the reference trend sprite for experience task rows
Each experience task row closed with a solid `fa-bolt` glyph. The reference
marks the same slot with the colour `trend-color` sprite followed by "+ N",
and uses that sprite for the 成长体系 heading as well, so the level page also
carried a stray `fa-chart-line` there.

The sprite already ships in the theme (`jiosvg-trend-color`); its path data is
byte-identical to the reference sheet. Both slots now render it, sized to
match the reference (`icon mr6 em09` for the rows, `icon mr6 em12` for the
heading). The "动态" rows keep their wording without a "+", which is how they
read before.

The check-in experience badges in src/Credits/Signin.php stay on
`fa-line-chart` — the reference uses the same monochrome glyph there.

Verification: rendered the real level page through a PHP stub — all ten task
rows emit `#icon-trend-color`, the only Font Awesome glyph left is the
"每日详情" arrow, matching the reference. Screenshotted against the real
stylesheet. A contract test pins both sprite usages and rejects a regression
to `fa-bolt` / `fa-chart-line`; 862 widget/navigation tests pass.
2026-11-04 17:17:49 +08:00
kate522 09b1e535e4 fix(user-center): use the reference colour sprites on the balance and points cards
Both asset cards opened with the same solid `fa-money-bill` glyph, so the
wallet and the points card showed an identical icon and neither matched the
reference, which pairs a blue wallet mark with the balance card and an orange
coin mark with the points card.

The theme already ships both sprites — the header wallet popover and the user
center sidebar resolve to `jiosvg-money-color-2` and `jiosvg-points-color`
through the same helper. Only these two cards were still on Font Awesome.
Swapping them keeps one icon family across the user center; the existing
`em12 mr6` sizing carries over unchanged because `svg` is 1em tall.

The wallet/points icon contract test now pins both keys and fails if either
card falls back to `fa-money-bill` or `fa-coins`.

Verification: rendered both cards through a PHP stub and screenshotted them
against the real theme stylesheet — the blue wallet and orange coin render at
the same optical size as the labels. 861 widget/navigation tests pass.
2026-11-04 17:08:09 +08:00
kate522 00ae62f62b fix(auth): close the modal regressions and keep the header component off /sign/
Three defects slipped into the sign-card rebuild:

The hidden-glyph rule was dropped. `.fa` sets display:inline-block, which
outranks the UA [hidden] rule, so every hidden glyph stayed visible — most
visibly a spinner rendered beside the login button at rest. `.sign [hidden]`
restores the override.

Closing account login produced an empty shell. The whole sign-in pane was
gated on `!$close_signin`, and the social block carried the same gate, so a
site with account login off but registration on still opened on an empty
panel with no social entry. The pane is now rendered in three shapes: open
registration falls through to the signup tab, a QR entry falls through to
the QR tab, social-only renders a notice plus the social bar, and when
nothing is available it states that login is closed.

The modal close button sat in normal flow and pushed the card content down;
`#u_sign .sign > .close` pins it back to the top-right corner.

The /sign/ route is `index.php?jio_sign=1`, so WordPress reports it as home.
The header multifunction component resolves page type by query flags and the
home branch won, so the component appeared on the auth page whenever "首页"
was in its scope — even though the sign page template itself was already
excluded. `jio_header_slide_current_page_type()` now bails out on `jio_sign`
before any of the is_* checks.

The breakpoint overrides that were fighting the shared sheet between 641px
and 991px are narrowed to the one offset that actually needed adjusting.

Verification: the modal was rendered through a PHP stub, served with the real
theme stylesheets, and driven by the real auth.js in a headless browser —
tab switching, signin-mode switching, password reveal, close button and
geometry at 1440/1100/900/760/500/390px. The header harness gained a
`jio_sign` case asserting no markup, and 860 widget/navigation tests pass.
2026-11-04 16:51:41 +08:00
kate522 41366e7fc4 feat(auth): rebuild the sign-in surfaces on the shared sign-card contract
The login/register page and the auth modal had drifted into a parallel
system: floating-input fields, a sign-page-card shell and a bespoke
jio-auth-modal wrapper that none of the theme's own sign CSS targeted.
The reference card rules (.sign, .sign-img, .sign-content, .line-form-input,
.scale-placeholder, .social_loginbar, .title-h-left, #u_sign .modal-dialog,
.qrcode-signin-container) have been in main.css all along, so the fix is to
render the reference DOM instead of re-styling a second one.

auth-content.php now emits one .sign.jio-panel.blur-bg.relative card with a
.tab-content of #tab-sign-in / #tab-sign-up / #tab-resetpassword /
#tab-qrcode-signin / #tab-bind panes. Each pane opens with a
.title-h-left.fa-2x heading plus a .muted-color.px12 link, fields are
.relative.line-form.mb10 > .line-form-input + .scale-placeholder +
.line-form-line, the verification trigger is the .yztx.abs-right captchsubmit
button, and submits are .but.radius.jb-blue|jb-green.padding-lg.btn-block.
Social entries come from the shared render_social_buttons() separator +
.social_loginbar, and the OAuth bind pane uses the oauth-data-box avatar card.

The modal shell is #u_sign.modal > .modal-dialog > .sign-content >
.sign-img.absolute.hide-sm + .sign, and the page shell is the reference
.container.sign-page > .row.sign-row.gutters-5 > .col-md-6(.col-md-offset-6/
.col-md-offset-3).sign-col with a .fixed.lazyload background and a blurred
fixed footer. auth.js drives .in instead of .is-open, toggles the new
password/signin-mode/invite-gate controls and no longer touches the retired
markup. The page also drops two footer-hook removals that targeted functions
which no longer exist, and opts out of the fixed-nav offset.

The sign page's custom theme setting now actually applies: Router answers
jio_forced_theme_mode, jioData.themeForced wins over the stored cookie in
theme.js, theme-toggle positions are suppressed, and the page marks html/body
dark or white accordingly.

sign.css shrinks from ~114 lines of bespoke component styling to a thin
positioning layer, and the retired classes are gone rather than kept.

Verification: php -l on every changed PHP file, node --check on both JS files,
node --test across widget-system, navigation and front-submit (873 passing),
and tools/audit-psr4-paths.php / audit-admin-contracts.php both at 0 errors.
assets/js/runtime/main.min.js was rebuilt because theme.js gained the forced
theme branch.
2026-11-04 16:04:24 +08:00
kate522andClaude Fable 5 d40906b4a3 feat(single): align author bar follow and reward actions with the site flows
- guests get the sign-in trigger instead of a redirect to the login page,
  for both follow and wallet reward controls
- follow requests swap the label for an inline loader and block a second
  submit; the followed state uses the .actived hook
- follower counts sync through data-follow-count on the user homepage
  (the previous selector matched nothing) and only for the viewed profile
- reward button uses the secondary action colour; the reward template is
  only rendered for signed-in visitors
- drop the unused author-hero aria-disabled rule and stale wording in the
  touched comments

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-11-04 13:34:34 +08:00
kate522 8ad5ef9aff fix(single): align article code blocks with the reference theme
The article body ran two highlighting pipelines at once: the runtime
EnlighterJS pass (same one the comment area uses) plus a bespoke
precode.js enhancer, and a hand-rolled .code-block skin. The two
disagreed, so rendered blocks carried a dark #282c34 card with 15px
padding, 14px text, a border and a second copy button stacked on top of
the EnlighterJS toolbar.

Drop the parallel path and let one pipeline own code blocks:
- delete precode.js, single-post/code-blocks.css and the unused
  assets/js/enlighter copy that only precode.js referenced
- drop the now-dead has_code requirement (PHP scan + feature enqueue)
- render pre exactly as the reference does: bootstrap's pre box
  (9.5px padding / 13px / radius 4) plus main.css's muted background,
  with the EnlighterJS theme supplying the skin

Also scope the inline-code skin to :not(pre code) so it stops painting
the block background pink, and remap the toolbar copy glyph: \f0ea is
the clipboard in FA4 but "paste" in FA5; the clipboard is \f328.

Verified against the reference at 1440px both themes: pre 13px/18.57,
9.5px padding, radius 4, muted background; EnlighterJS block heights
151.31 / 60.38 match exactly.
2026-11-03 23:31:28 +08:00
kate522andClaude Opus 5 ba32e0ef9c fix(nav): match the theme toggle icon size and baseline to its neighbours
Measured in the live header: the toggle's glyph was 15.25px tall centred at
y=31.5, while the account and search icons are 14px tall centred at y=34.09.
Two causes stack up:

- `.toggle-radius` is a fixed 29x29 circle with no font-size, so the sun/moon
  pseudo-elements inherit `.navbar-top`'s 15px. The sibling `.jio-nav-icon`
  buttons pin 14px, so the toggle glyph renders a size larger.
- The toggle sits alone in a 29px `.navbar-form`, while the account cluster is
  34px (6px padding + 20px line box + 6px). Both clusters are top-aligned, so
  their centres differ by 2.5px.

Pin the toggle's glyph to 14px and give its cluster the same 34px flex line as
its neighbours. Scoped to `.navbar-top`, so the mobile drawer and the floating
rail (which also carry .toggle-theme) keep their own geometry.

Verified via headless Edge + CDP on https://localhost: glyph now 14px, centre
delta 0.09px (was 2.5px); pixel measurement shows the sun at 14.0px against
14.0px for the search icon.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-03 21:36:56 +08:00
kate522andClaude Opus 5 02246c7ec8 fix(nav): render the theme toggle with the outline sun/moon face
The top-bar theme switch looked heavier and larger than the icons beside it.
An earlier commit dropped the `far` modifier from fa-toggle-theme, which moved
the glyphs to the Solid face. The reference's glyphs are the outline variants
(FA4's sun-o / moon-o), and in FA5 those live in the Regular face — so the
Solid replacement reads bolder and visually bigger at the same 1em size, while
the neighbouring account icons stay on the lighter Regular-style SVG set.

Restore `fa far fa-toggle-theme` on all three placements: desktop navbar,
mobile header, and the floating rail. The .fa-toggle-theme::before/::after
composite rules only handle positioning and the slide transition, so they are
unaffected by the face swap.

legacy-navigation-contract already asserts `fa far fa-toggle-theme` and passes
again; full suite 999/1000 with the pre-existing frontend-lifecycle failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-03 19:58:54 +08:00
kate522andClaude Opus 5 6c3c0f7c6d refactor(admin): fold code highlight and system notice into 常用功能
The 代码高亮 and 系统公告 schemas existed but appeared in no menu, so their
routes were never generated (routes come from the sidebar constants). The
reference keeps both inside the 常用功能 panel rather than as their own
entries, so follow that structure instead of adding sidebar items.

- global/common now carries three groups after the lazy-load ones:
  代码高亮 (switch / line numbers / buttons / max height / light+dark theme)
  and 弹窗通知 (policy, content, 4 buttons, size, radius, icon, expiry)
- group keys are uniquified (code-highlight, code-theme, notice-*) so they do
  not collide with the existing performance group
- delete the orphan code-highlight.js / system-notice.js schemas and their
  section entries on both the Vue and PHP sides
- delete post-list/pay.js and its PHP counterpart: every post_pay_* and
  post_download_* key there had zero runtime consumers, and the reference has
  no such page (paid-content config lives under 支付&付费)

tools/health and tools/import-export turned out to already be reachable as
custom pages, and remain untouched.

Tests: admin contract audit 0 errors (warnings 56 → 51); SPA builds clean;
suite 967/968 with the pre-existing frontend-lifecycle failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-03 17:50:32 +08:00
kate522andClaude Opus 5 451b04160b fix(comments): restore syntax highlighting for comment code blocks
Comment [code] blocks rendered as bare <pre><code> with only Bootstrap's
grey box — no syntax colours, unlike the reference.

The runtime gates EnlighterJS on `_win.highlight_kg`, which was hardcoded to
false. With that false the selector degrades to `pre code.gl, pre code.special`,
and comment code carries neither class, so it never highlighted. The admin
already exposes 「全局功能 → 代码高亮」 (default on), and jioData.codeHighlight
reads it — only the runtime payload disagreed.

- _win.highlight_kg / highlight_hh / highlight_zt (plus white/dark theme
  names) now come from Settings::get_code_highlight_config(); the initial
  theme follows cookie → mode → time-auto via initial_theme_mode()
- define those values before the _win localize (it sits above jioData in the
  same method)
- inject the reference's two head rules the runtime path was missing:
  max-height from `highlight_maxheight`, and
  `.enlighter-toolbar{display:none}` when `highlight_btn` is off

Tests: comment-image-upload contract gains cases pinning the settings-backed
booleans (and their definition order) and the two inline CSS rules; suite
837/838 with the pre-existing frontend-lifecycle failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-03 17:31:42 +08:00
kate522andClaude Opus 5 0007e78bfe fix(comments): render image upload progress like the reference
While uploading, the confirm button turned into a thin 3px bar below the
label instead of the reference's striped overlay that fills the button.

Two causes:
- the markup used <span class="progress">; Bootstrap sizes the bar with a
  block-level .progress, so the swap produced a flattened line
- shell.css carried a bespoke override pinning .progress to
  `position: static; height: 3px`, discarding the absolute full-size
  overlay that main.css already provides

Changes:
- emit the reference skeleton: <div class="progress progress-striped active">
  > <div class="progress-bar progress-bar-success">, so the striped bar
  spans the button and the gradient shows through the translucent layer
- give the upload button `position: relative` so the overlay is contained by
  the button rather than the dropdown panel; delete the static-line override
- restore the reference's "正在处理请稍等..." toast and switch the label to
  "处理中 ..." at 100%, both keyed to the shared `miniupload_ajax` toast id
  so the completion message replaces the loading one
- notify() now forwards the toast id instead of dropping it

Tests: comment-image-upload contract gains two cases pinning the div-based
striped overlay and the deletion of the 3px override; suite 835/836 with the
pre-existing frontend-lifecycle failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-03 17:12:52 +08:00
kate522andClaude Opus 5 f6fde18857 fix(single): align author-bar publish time with the reference
The publish time under the author name diverged from the reference in four
ways: it rendered a leading bullet from `.icon-circle`, its tooltip opened
above instead of below, its wording was "2 天发布" (WordPress'
human_time_diff has no 前 and keeps spaces), and it ignored the
「时间显示数据源」 setting entirely.

- markup: drop `.icon-circle` from the time span and the description fallback;
  add `data-placement="bottom"` so the tooltip opens below like the reference
- wording: use jio_time_ago() (which already ends in 前) and append only
  发布 / 更新, yielding "2天前发布"
- branches: mirror zib_get_post_time_tooltip exactly (date source → publish
  time; unpublished → modified time; published-with-update → modified time
  labelled 更新 while the tooltip keeps the publish time; otherwise publish)
- the setting is read from post_time_source via get_post_list_style_settings()

Tests: new author-bar-time suite drives the real template through a stub
harness across all four branches (4/4); widget suite 833/834 with the
pre-existing frontend-lifecycle failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-03 16:58:05 +08:00
kate522andClaude Opus 5 f14e6bd88d fix(single): align cover breadcrumb with the article title
With a featured image the breadcrumb sits under the title inside the cover
overlay, but its first icon started 6px left of the title's left edge.

The overlay rule carried an extra `padding: 0` that this theme added on top of
the reference. The same node also matches `ul.breadcrumb { padding: 0 }`
(0,1,1 specificity), which outranks the base `.breadcrumb { padding: 0 8px }`
(0,1,0) — so the reference's `margin-left: -6px` nudge was applied with no
inset to compensate, pulling the whole row 6px to the left.

Writing the inset back as `padding: 0 8px` on the overlay rule restores the
reference geometry: -6px + 8px nets +2px from the panel edge. The global
`ul.breadcrumb` reset and the video footer variant are untouched, so archive
and video breadcrumbs keep their current padding.

Tests: new case in the single-cover contract suite pins the 8px inset (and the
global reset); cover suite 12/12, widget suite 829/830 with the pre-existing
frontend-lifecycle failure.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-03 16:48:38 +08:00
kate522andClaude Opus 5 93e83e1835 fix(single): align article like/favorite interaction UX with reference
The like and favorite buttons gave no in-flight feedback: the label just sat
there until the request returned, the lovestar animation never replayed on a
repeat like, duplicate clicks reported "success", and guest favorites silently
wrote a cookie instead of asking the user to sign in.

- pending state: replace the count with the reference inline spinner
  (`<i class="loading zts">`) and disable the button until the response lands
- animation: strip `action-animation` before re-adding it (forced reflow), so
  every successful like replays the reference lovestar keyframes
- duplicate: when the server reports the same state and count, skip the
  animation and surface the reference warning copy (已赞过此文章了!)
- failures: restore the original count and toast `danger`; guest-only actions
  now open the site login modal instead of dead-ending on an error
- favorite requires login server-side, matching the reference; the guest
  favourite cookie branch and its reader are removed as dead code
- unify like/favorite through one bindToggle path and keep every mirror
  button (article row + footer tabbar) in sync

Tests: new article-actions-ux suite (8 cases) drives assets/js/core/article.js
against a DOM/fetch shim — pending spinner, replay, duplicate no-op,
concurrency, rollback, guest login, mirror sync. Widget suite 828/829; the one
failure (frontend-lifecycle theme-init stub) predates this change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-03 16:36:45 +08:00
kate522andClaude Opus 5 3c07ef7a8d fix(widgets): restore the 20px bottom gap under the quote and search widgets
The reference wraps these two widgets in a middle .theme-box layer
(widget-container > .theme-box > title + card), which is what supplies the
20px bottom margin. Our outer .jio-widget-box-wrap is only a transparent
layout container, so dropping .theme-box left the quote card flush against
the following module.

- search/quote: emit the reference .theme-box middle layer; the card itself
  stays margin-free like the reference, so the gap is exactly 20px
- keep the generic .widget-content branch for user_card/author_card and the
  other reference widgets untouched (the string is contract-tested)
- contract test now pins the .theme-box layer and asserts the quote card
  carries no margin class of its own

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-03 16:09:46 +08:00
kate522andClaude Opus 5 f80ca1e27d fix(widgets): restore single-card reference UI for search and quote widgets
The 搜索框 and 一言 sidebar widgets fell into the generic renderer branch, so
they rendered a .jio-panel card wrapping a second bespoke card
(.jio-widget-search / .jio-widget-quote). In-article 一言 also used
divergent modifier classes and an extra hover rule.

- search: render through the shared jio_get_search_box() (legacy keyword,
  history and category pickers) inside one reference card, mirroring the
  reference widget's markup and title default 「搜索」
- quote: render the reference .yiyan-box card itself as the click root; the
  shell now emits only the reference widget-container/title wrapper
- article 一言: adopt the reference class lists for header, footer and the
  standalone after-article box; drop the invented yiyan--* modifiers and
  the duplicate hover/opacity rules
- client: fill .cn/.en in the reference order (first line .cn, second .en)
  and drop the stale window.JioYiyan bridge plus the dead qv-yiyan.php AJAX
  path in runtime main.js
- schema: replace the ad-hoc search_style radio with the reference switches
  (keywords, history, input category, more_cats) and default title
- assets: enqueue assets/js/yiyan.js through the quote widget's asset list

Tests: widget-system suite 820/821 (the one failure is the pre-existing
frontend-lifecycle theme-init stub); new search/quote card contracts pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-03 15:56:52 +08:00
kate522andClaude Fable 5 1549d5b25d fix(payment): honor schema defaults for balance recharge and fix withdrawal warning
Fixes two issues:

1. **Balance recharge disabled on fresh install**: The `pay_balance_s`
   setting has `default: true` in SchemaPayment but was treated as
   disabled when the key was absent from the database. This meant users
   had to toggle it off and back on to activate the feature.

   Fixed by using `!isset() || !empty()` pattern to honor the schema
   default when the key is missing.

2. **PHP Warning in withdrawal settings**: `get_withdrawal_settings()`
   passed `$s['affiliate_withdrawal_reset_time']` directly to
   `preg_match()` without extracting it first, causing "Undefined
   array key" warnings when the option was unset.

   Fixed by extracting to `$reset_time_raw` before the regex check.

Changed:
- `PaymentRuntime::get_payment_recharge_settings()`: enabled defaults to true
- `PaymentRuntime::get_payment_gateway_settings()`: balance_enabled defaults to true
- `PaymentRuntime::is_gateway_enabled('balance')`: defaults to true
- `PaymentRuntime::get_withdrawal_settings()`: no longer warns on missing key

Added test script `tools/test-payment-runtime-defaults.php` to verify
the fix works with empty settings (fresh install).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-11-02 22:33:18 +08:00
kate522andClaude Opus 5 030baa511e fix(nav): restore reference theme toggle geometry and single binding
The top-bar theme switch was bound twice — core/theme.js attached a click
handler to #theme-toggle-desktop while runtime/main.js delegated click on
.toggle-theme, and both ship in main.min.js, so one press toggled twice and
the button looked dead.

Also realign the control with the reference:

- Drop the far modifier from fa-toggle-theme so the sun/moon glyphs resolve
  against the Solid face at the reference weight instead of Regular.
- Restore .toggle-radius .fa/.icon to the reference form (left/right 0 +
  auto margin only). The extra position/top/transform/size/flex block and
  the > i selector were overriding the shared abs-* centering rule.
- Replace the per-ID bindings with one delegated listener so the floating
  rail, which carries no id, keeps working.
- Remove dead theme.js queries for selectors no markup or stylesheet ships
  (#theme-toggle, #dropdown-theme-toggle, .theme-icon-moon/.theme-icon-sun,
  [data-action='toggle-theme'], .is-dark).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-02 22:24:20 +08:00
kate522andClaude Opus 5 e30de6562c fix(routing): bootstrap permalink structure so fresh installs resolve single posts
A fresh install leaves `permalink_structure` empty, so WordPress snapshots
$wp_rewrite with no post/page permalink rules and every single post resolves
to 404 until someone re-saves Settings -> Permalinks.

The existing mitigation fired too late to help: routing.php wrote the option
and flushed from `init`, but WP_Rewrite was already constructed with the empty
structure, and core defers pre-`wp_loaded` flushes to a `wp_loaded` callback
that regenerates from that stale snapshot. The rewrite_rules option ended up
non-empty but broken, so the 404 persisted on every request.

New \Jio\Core\RewriteBootstrap owns this: on `after_setup_theme` (still inside
wp-settings.php, before wp-blog-header.php parses the query) it calls
$wp_rewrite->set_permalink_structure() — the same API wp-admin/options-permalink.php
uses — which persists the option *and* refreshes the in-memory snapshot, so both
the current request and the rules written for the next one are correct. A
structure an administrator chose is never overridden.

Also consolidates the two duplicated flush sites that lived inline in
functions.php (`after_switch_theme` closure and the `jio_flushed_version`
block, whose `delete_option('rewrite_rules')` forced a needless regenerate),
and drops the now-redundant permalink write from routing.php. The version
stamp moves to `wp_loaded` priority 20 so it sees the complete rule set
instead of the pre-`add_rewrite_rule()` snapshot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-02 19:50:24 +08:00
kate522andClaude Opus 5 39aed0d430 fix(admin): show the real default URL for level and VIP icons
用户等级配置的「等级图标 URL」和 VIP 的「会员图标」在后台显示为空白输入框,
但前台实际渲染的是主题自带图标——后台既看不到当前效果,也无法判断该字段
是否已被配置。根因是 schema default 写成空串,而运行时(Level::
get_level_icon_url() / PaymentRuntime::get_vip_icon_url())在值为空时回退
到 assets/img 下的内置图标。

把 schema default 改为直接写入主题自带图标 URL,与运行时回退链保持同一路径、
同一 is_file 守卫:后台展示的就是前台真正使用的图标,清空则回到文本/FA 回退。

- SchemaUsers::default_level_options():icon_img_N 写入 user-level-N.png
- SchemaPayment::vip_icon_default():vip_N_img_icon 写入 user/vip/vip-N.svg
- level.js / vip.js 同步默认值,help 与 placeholder 文案改为描述默认图标
- 两个测试 harness 补齐 get_theme_file_uri/path stub(schema 现在需要它们)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-02 14:06:52 +08:00
kate522andClaude Opus 5 427968a721 chore(branding): remove leftover upstream theme identifiers
The theme was renamed Zibll/子比 → AKSRT → Jio, but upstream branding
survived the sweeps in CSS classes, the SVG sprite prefix, a persisted
widget field ID, JS events, admin Vue class names, test infrastructure
and docblocks.

Renames:
- zib-widget / zib-widget-wrap / zib-slider → jio-widget-box /
  jio-widget-box-wrap / jio-slider-box (jio-widget is taken by the modern
  widget shell, jio-box-* by the card primitives)
- zib-partners-scroll + zps-* → jio-partners-scroll + jps-*
- zib-combo-info-card + zci-* → jio-combo-info-card + jci-*
- zibsvg- → jiosvg-, including the exported picker helpers and the
  fa-zibsvg- family-prefix guard
- zibpay_type → pay_type; the dual-read fallback in PostQuery collapses
  to a single key
- [zibajax="submit"] dropped (no PHP emitter); zib_ajax.success →
  jio_ajax.success
- aks-* admin Vue classes → jio-*
- zib_palette() → palette()

User-facing vendor traces are gone: the floating "more" button default no
longer ships a zibll.com link or 子比主题 marketing copy, the notice
widget ships no preset promo entries, and the four 官网教程 links are
removed from the post-list admin schema.

Test infrastructure renamed to neutral names (docker-compose.reference.yml,
Caddyfile, article-baseline.json, modern-widgets-contract.test.cjs) with
their DB/volume/env/slug identifiers following.

Kept intentionally, documented in tests/fixtures/README.md: the golden
fixture reference fingerprint (a SHA-256 record of an external directory,
read by validate-widget-golden / generate-widget-golden-fixture /
audit-widget-system), the doesNotMatch guards that prove the old tokens are
gone, and the reference theme's own meta key in the visual fixtures.

No backward-compat or data migration: saved widget configs holding old
icon keys or the old field ID silently lose them, per the project's
rewrite-over-compatibility rule.

Verified: 132/132 node contract tests, php tests/widget-system/run.php
(146 assertions), audit-admin-contracts, audit-psr4-paths and
validate-widget-golden all green; tracked bundles regenerated and in sync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-02 00:06:54 +08:00
kate522andClaude Opus 5 42cc64106d docs: point the theme folder references at Jio, not Jio-theme
The mechanical rename turned AKSRT-theme into Jio-theme, but the
directory itself is Jio. Fix the three places that name the folder
(README install steps and tree, wp-cli-visual-fixtures usage comment,
DocsPage docs deep-link) and rebuild the SPA bundle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:12:25 +08:00
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00
kate522andClaude Opus 5 5b16bc6e15 fix(frontend): route the VIP points-exchange entry through the custom currency name
/user/vip/ 档位购买卡下方的入口横条仍硬编码「积分兑换会员」,而后台
「积分配置 → 自定义货币名称」改名后不跟随。上一轮改名只覆盖了兑换弹窗
标题与会员弹窗内入口,页面自身的入口横条是另一处服务端渲染,漏改了。

入口横条(inc/helpers/aksrt-vip.php)
- aksrt_get_vip_points_exchange_entry_html 改取已加载的
  catalog.currency_name,回退 Settings::get_currency_name() 再回退「积分」

同一页面上其余漏改的兑换文案一并收口,命名与入口/弹窗保持一致:
- VipServiceModal:积分不足提示 → {货币名}不足,暂时无法兑换
- VipServicePurchase.exchange_with_points:兑换未启用 / 兑换无效 /
  不足三处 WP_Error、流水说明、订单备注(%1$s兑换会员 %2$d %1$s。)
  currency_name 在此单独解析,get_catalog 内的局部变量不在该作用域
- LedgerPresentation:vip_points_exchange 流水原因标签

契约测试新增源断言,并用两个 PHP harness 做运行时断言:入口横条
(改名后 / 已是会员不输出 / 缺 key 回退)与弹窗(余额充足 / 不足两分支)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 16:19:49 +08:00
XingandClaude Opus 5 4e62b325d2 fix(badges): keep the medal catalog from being cast to a string on save
user_medal_args was declared as type=json without sanitize_type, so
sanitize_field_value() fell through to sanitize_text_field((string) $value)
and stored the literal string "Array". validate() has no json shape check,
so the save reported success.

Consequence: parse_medal_catalog() json_decoded "Array" into an empty
catalog, the dictionary came back empty, and user_context() dropped every
owned badge — the front end showed "获取徽章" and the grant modal listed
nothing, hiding both the newly added and the pre-existing badges.

Add the missing sanitize_type to match its sibling level/integral fields,
and guard sanitize_field_value() so structured container types can never
reach the scalar fallback again. Mirrors the declaration in the Vue schema.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 15:47:09 +08:00
kate522andClaude Opus 5 5c6b422866 fix(frontend): route every user-center points label through the custom currency name
后台「积分配置 → 自定义货币名称」改名后,用户中心仍有大量硬编码
「积分」:做任务赚积分 / 购买积分 / 签到领积分、积分记录 / 积分任务、
每日可免费获取 XX 积分、我的积分、积分+1、创作中心积分收益等。

get_currency_name() 早已存在且可用,问题只是这些文案没有走它。
本PR 全部替换,无双路、无迁移:

服务端渲染(用户中心 renderers)
- credits-balance.php:购买/做任务赚/签到领 + 整个购买弹窗
- profile-credits.php:积分记录 / 积分任务 页签
- credits-points-tasks.php:每日上限三态与空态
- sidebar-creator.php / profile-creator.php:积分收益
- transfer-modal.php:积分转账提示
- Signin.php:签到详情弹窗(我的积分 / 奖励 tooltip / 积分+N)
- VipServiceModal.php:我的积分 / 积分兑换会员
- download-permission-card.php:购买价格

客户端渲染(JS 无法调用 PHP,注入 currencyName 到 aksrtData)
- signin.js:签到状态行「已签到 N 天 +1」
- credits.js:转账标题/金额标签 + 购买提示 toast
- cms-download.js:积分不足视图、我的积分 入口、余额不足文案
- pay-vip.js:积分兑换会员入口(优先 catalog.currency_name)
- order-center.js:订单行单位回退

契约测试同步改为断言模板构造而非死字符串,并新增两例:
改名后跟随、未注入时回退「积分」。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 15:09:49 +08:00
kate522andClaude Opus 5 1c95f94bca refactor(admin): drop the SPA credits asset record browser
「用户近期资产记录」卡片在 SPA 积分配置页里按用户 ID + 资产类型 +
日期查 user meta 历史,再伪装成统一账本行(entries / operation_key /
ref_key)。SPA 只承载配置,运营记录归原生 用户 → 积分/余额 屏幕
(CreditsAdmin / BalanceAdmin),这条平行路径删除,不留双读或迁移。

- CmsCredits.vue: 移除卡片、表格、分页及 ledger* 状态与
  formatAssetValue/loadLedger/searchLedger/resetLedger
- users.js: 删除仅服务该卡片的 getCreditLog()
- UserController: 删除 /assets/transactions 路由、rest_get_credits_log()
  及其唯一调用方 asset_value_to_minor()
- 契约测试改为断言该能力不得回归

points_record / balance_record 本身保留,前端账本与原生后台仍在消费。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 14:20:07 +08:00
kate522 324bcfdadc fix(admin): verify the whole SPA build graph, not just the entry
find_missing_admin_chunks() only parsed index.js, but a lazily
imported page chunk pulls in its own deps (shared-api, PageHeader,
sortable-options…). A deploy that drops one of those still leaves the
entry internally consistent, so the existing notice never fires and the
failure only shows up when an admin opens that menu — every schemaPage
keeps working, which makes the breakage look arbitrary.

The same scan never looked at CSS at all: 18 stylesheets are reachable
from the graph and none of them were checked, so a missing page
stylesheet ships an unstyled screen with no warning.

Walk the module graph transitively from index.js, queueing each
discovered dependency, and match .js and .css specifiers. Deleting
PageHeader now also reports every page that depends on it.

This is a diagnostic hardening only — it makes an incomplete deploy loud
instead of guessing at the cause of a broken page.
2026-11-01 13:41:58 +08:00
kate522 df12f51199 fix(admin): stop cropping the layout preview images in image_select
AksImageSelectField pinned every preview to a 60x60 box with
object-fit:cover, but these are wide layout strips: header layouts are
597x57 (10.47:1), the footer template is 800x190 (4.21:1) and the slide
align set is 200x101. A 10:1 strip inside a square crop keeps ~6% of its
width, so the layout it is supposed to illustrate is unrecognisable.

Constrain the width instead and let the height follow, which is what the
reference theme's CSF image_select does (max-width:100%; height:auto).
Each field now declares the width that suits its images, and an option
may still override it: header 200, footer 240, slide align 160, mobile
header 120.

Only presentation changed. No image files and no stored values moved,
so existing settings are unaffected.
2026-11-01 12:18:14 +08:00
kate522 4740078135 fix(icons): repair the FA5 generator and drop the 404 font references
generate-fa5-icons.php read fa-{brands,regular,solid}.svg from
webfonts/, but only woff/woff2 ship with the theme, so the generator
died on file_get_contents() and the committed fa5-icons.php could not
be regenerated from what the theme actually distributes.

Decode the style split from each shipped woff's cmap table instead.
The split has to come from the font: FA5 assigns different drawings to
the same codepoint per style, so fas/far/fab membership is unanswerable
from the CSS alone. Output is byte-identical to the committed catalog
(1001/151/456), so the picker is unaffected.

all.min.css also declared eot / eot?#iefix / ttf / svg fallbacks for all
three families while webfonts/ holds only woff2 + woff, so a browser
that stops at woff requested six files that do not exist. Add
tools/strip-fa5-legacy-fonts.mjs to keep the src: list to what ships;
it is idempotent and touches nothing but the @font-face blocks.
2026-11-01 11:52:14 +08:00
kate522 9c70d7929a chore(assets): drop the duplicated header-layout previews
assets/admin/public/images/header-layout duplicated
assets/images/header-layout byte for byte (all five PNGs, matching
md5). The directory only existed because assets/admin/public is Vite's
default publicDir, so every build copied it into build/images.

Nothing referenced it: SchemaPageDisplay builds its preview URLs from
assets/images/header-layout via self::asset(), and a repo-wide search
finds no reference to public/images anywhere. Remove the source and let
build/images stop being regenerated.
2026-11-01 11:52:11 +08:00
1107 changed files with 62248 additions and 32130 deletions

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@
/.workbuddy/
# Project planning, internal docs, and local-only development files
/.aksrt-chrome-profile/
/.jio-chrome-profile/
/.specs/
/docs/
+15 -15
View File
@@ -2,15 +2,15 @@
/**
* 404 template.
*
* @package AKSRT
* @package Jio
*/
defined('ABSPATH') || exit;
get_header();
$search_placeholder = function_exists('aksrt_get_search_placeholder') ? aksrt_get_search_placeholder() : __('开启精彩搜索', 'aksrt');
$hot_keywords = function_exists('aksrt_get_search_hot_keywords') ? aksrt_get_search_hot_keywords() : [];
$search_placeholder = function_exists('jio_get_search_placeholder') ? jio_get_search_placeholder() : __('开启精彩搜索', 'jio');
$hot_keywords = function_exists('jio_get_search_hot_keywords') ? jio_get_search_hot_keywords() : [];
$hot_query = new WP_Query([
'post_type' => 'post',
@@ -39,33 +39,33 @@ $top_cats = get_terms([
<i class="fa fas fa-compass"></i>
</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">
<?php esc_html_e('你访问的页面不存在或已被删除。试试搜索,或者从下方常用入口继续浏览。', 'aksrt'); ?>
<?php esc_html_e('你访问的页面不存在或已被删除。试试搜索,或者从下方常用入口继续浏览。', 'jio'); ?>
</p>
</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>
<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">
<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>
<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">
<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>
<?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">
<a href="<?php echo esc_url(home_url('/')); ?>" class="btn-primary"><?php esc_html_e('返回首页', 'aksrt'); ?></a>
<a href="javascript:history.back()" class="btn-outline"><?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('返回上一页', 'jio'); ?></a>
</div>
</section>
<?php if (!empty($top_cats) && !is_wp_error($top_cats)) : ?>
<section class="error-section" aria-label="<?php esc_attr_e('热门分类', 'aksrt'); ?>">
<h2 class="error-section__title"><?php esc_html_e('热门分类', 'aksrt'); ?></h2>
<section class="error-section" aria-label="<?php esc_attr_e('热门分类', 'jio'); ?>">
<h2 class="error-section__title"><?php esc_html_e('热门分类', 'jio'); ?></h2>
<div class="error-section__chips">
<?php foreach ($top_cats as $cat) : ?>
<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 if ($hot_query->have_posts()) : ?>
<section class="error-section" aria-label="<?php esc_attr_e('热门内容', 'aksrt'); ?>">
<h2 class="error-section__title"><?php esc_html_e('热门内容', 'aksrt'); ?></h2>
<section class="error-section" aria-label="<?php esc_attr_e('热门内容', 'jio'); ?>">
<h2 class="error-section__title"><?php esc_html_e('热门内容', 'jio'); ?></h2>
<ul class="error-section__list">
<?php while ($hot_query->have_posts()) : $hot_query->the_post(); ?>
<li>
+29 -29
View File
@@ -1,4 +1,4 @@
# AKSRT WordPress Theme
# Jio WordPress Theme
> **版本**: 3.4.0-dev | **架构**: 原生 PHP + WordPress (非 headless)
> **要求**: WordPress >= 6.4 | PHP >= 8.1
@@ -7,7 +7,7 @@
## 项目概述
AKSRT 是一款现代化的 WordPress 主题,采用**原生 WordPress 主题架构**开发(PHP + HTML + CSS + JavaScript),不依赖 REST API 进行前端渲染。
Jio 是一款现代化的 WordPress 主题,采用**原生 WordPress 主题架构**开发(PHP + HTML + CSS + JavaScript),不依赖 REST API 进行前端渲染。
### 核心特性
@@ -22,14 +22,14 @@ AKSRT 是一款现代化的 WordPress 主题,采用**原生 WordPress 主题
## 文件结构
```
AKSRT-theme/
Jio/
├── functions.php # 入口:常量 + Composer autoload + bootstrap + 单例 init
├── style.css # WordPress 主题声明头
├── composer.json # 第三方依赖 + 主题 PSR-4(AKSRT\ → src/)
├── composer.json # 第三方依赖 + 主题 PSR-4(Jio\ → src/)
│
├── *.php # WP 核心模板保留根目录
│
├── src/ # Composer PSR-4 类型根(namespace AKSRT\)
├── src/ # Composer PSR-4 类型根(namespace Jio\)
│ ├── Admin/ # SPA 编排 + 域控制器 + Schema + Native ops
│ ├── Auth/ # 登录 / OAuth / 审核 / 邮箱
│ ├── Content/ # 下载 token / 付费内容 / VIP / 可见性
@@ -89,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(必做)
@@ -106,16 +106,16 @@ composer dump-autoload -o
确认:
```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 后再访问站点。
### 方式一:直接上传(推荐)
1. 将整个 `AKSRT-theme` 文件夹上传到 `wp-content/themes/`
1. 将整个 `Jio` 文件夹上传到 `wp-content/themes/`
2. 在服务器主题目录执行上一节的 `composer install`(若打包时已带上完整 `vendor/` 且已 `dump-autoload`,可跳过)
3. WordPress 后台 → **外观** → **主题** → 找到 **AKSRT** → **激活**
3. WordPress 后台 → **外观** → **主题** → 找到 **Jio** → **激活**
### 方式二:ZIP 打包上传
@@ -126,7 +126,7 @@ php -r "require 'vendor/autoload.php'; echo class_exists('AKSRT\\\\Core\\\\Domai
SSH 进入主题目录后:
```bash
cd /path/to/wp-content/themes/AKsrt-theme # 以实际路径为准
cd /path/to/wp-content/themes/Jio # 以实际路径为准
composer install --no-dev --optimize-autoloader
# 或仅刷新映射:composer dump-autoload -o
# 可选:reload php-fpm / 清 OPcache
@@ -150,9 +150,9 @@ composer install --no-dev --optimize-autoloader
### 2. 后台配置路径
WordPress 后台左侧 **Admin Bar** → ⚡ **AKSRT 主题设置**
WordPress 后台左侧 **Admin Bar** → ⚡ **Jio 主题设置**
或直接访问:`/wp-admin/admin.php?page=aksrt-settings`
或直接访问:`/wp-admin/admin.php?page=jio-settings`
### 3. 当前后台域
@@ -167,13 +167,13 @@ WordPress 后台左侧 **Admin Bar** → ⚡ **AKSRT 主题设置**
### 4. 推广、邀请与佣金(用户中心 → 收益)
- **推广链接**:用户中心「收益」页展示带 `?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. 部署注意事项
- **佣金结算时效**:结算依赖小时级 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 日上限、验证码等均依赖该函数)。
---
@@ -181,20 +181,20 @@ WordPress 后台左侧 **Admin Bar** → ⚡ **AKSRT 主题设置**
### Settings Facade 分层
- `AKSRT_Settings` 仍是主题设置的**唯一静态 facade**,外部调用方式保持 `AKSRT_Settings::*`
- `JIO_Settings` 仍是主题设置的**唯一静态 facade**,外部调用方式保持 `JIO_Settings::*`
- 具体运行时 getter 已按领域拆分到 `inc/classes/settings/`,例如:
- `class-aksrt-settings-theme-search.php`
- `class-aksrt-settings-post-display.php`
- `class-aksrt-settings-home-cms.php`
- `class-aksrt-settings-user-runtime.php`
- `class-aksrt-settings-integration.php`
- `class-aksrt-settings-display-runtime.php`
- `class-aksrt-settings-extension-runtime.php`
- `class-aksrt-settings-credits-runtime.php`
- `class-aksrt-settings-category-runtime.php`
- `class-aksrt-settings-capabilities.php`
- `inc/bootstrap.php` 会先加载 `settings/class-aksrt-settings.php`,再按领域加载这些 delegate 类
- 新增设置运行时逻辑时,优先放入对应 settings 子类;仅把聚合输出、统一入口和跨域装配保留在 `AKSRT_Settings`
- `class-jio-settings-theme-search.php`
- `class-jio-settings-post-display.php`
- `class-jio-settings-home-cms.php`
- `class-jio-settings-user-runtime.php`
- `class-jio-settings-integration.php`
- `class-jio-settings-display-runtime.php`
- `class-jio-settings-extension-runtime.php`
- `class-jio-settings-credits-runtime.php`
- `class-jio-settings-category-runtime.php`
- `class-jio-settings-capabilities.php`
- `inc/bootstrap.php` 会先加载 `settings/class-jio-settings.php`,再按领域加载这些 delegate 类
- 新增设置运行时逻辑时,优先放入对应 settings 子类;仅把聚合输出、统一入口和跨域装配保留在 `JIO_Settings`
### 添加新辅助函数
@@ -202,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/frontend/` | 前端公共样式 | 所有前台页面 |
| `css/admin/` | 后台管理样式 | 仅 AKSRT 设置页面 |
| `css/admin/` | 后台管理样式 | 仅 Jio 设置页面 |
| `css/shared/` | 共用组件样式 | 前后端按需加载(如验证码) |
---
+26 -26
View File
@@ -2,9 +2,9 @@
/**
* 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;
@@ -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_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 = [
'date' => __('最新', 'aksrt'),
'downloads' => __('热门下载', 'aksrt'),
'views' => __('阅读最多', 'aksrt'),
'date' => __('最新', 'jio'),
'downloads' => __('热门下载', 'jio'),
'views' => __('阅读最多', 'jio'),
];
$price_options = [
'all' => __('全部', 'aksrt'),
'free' => __('免费', 'aksrt'),
'paid' => __('付费', 'aksrt'),
'all' => __('全部', 'jio'),
'free' => __('免费', 'jio'),
'paid' => __('付费', 'jio'),
];
$preserve = [];
@@ -59,18 +59,18 @@ $has_filter = $current_orderby !== 'date' || $current_price !== 'all' || $price_
<main id="main-content" class="site-main container" tabindex="-1">
<header class="resource-archive-header page-cover">
<div class="container">
<?php if (function_exists('aksrt_breadcrumbs')) : ?>
<nav class="resource-archive-header__breadcrumbs" aria-label="<?php esc_attr_e('面包屑', 'aksrt'); ?>">
<?php aksrt_breadcrumbs(); ?>
<?php if (function_exists('jio_breadcrumbs')) : ?>
<nav class="resource-archive-header__breadcrumbs" aria-label="<?php esc_attr_e('面包屑', 'jio'); ?>">
<?php jio_breadcrumbs(); ?>
</nav>
<?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">
<?php
global $wp_query;
/* 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>
</div>
@@ -79,9 +79,9 @@ $has_filter = $current_orderby !== 'date' || $current_price !== 'all' || $price_
<div class="content-wrap">
<div class="content-layout content-layout--no-sidebar">
<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">
<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">
<?php foreach ($sort_options as $key => $label) :
$args = $preserve;
@@ -105,7 +105,7 @@ $has_filter = $current_orderby !== 'date' || $current_price !== 'all' || $price_
</div>
<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">
<?php foreach ($price_options as $key => $label) :
$args = $preserve;
@@ -139,43 +139,43 @@ $has_filter = $current_orderby !== 'date' || $current_price !== 'all' || $price_
}
?>
<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); ?>">
</label>
<span class="resource-archive__price-sep">—</span>
<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); ?>">
</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>
<?php if ($has_filter) : ?>
<a class="resource-archive__reset" href="<?php echo esc_url($archive_url); ?>">
<i class="fa fas fa-times-circle" aria-hidden="true"></i>
<?php esc_html_e('清除筛选', 'aksrt'); ?>
<?php esc_html_e('清除筛选', 'jio'); ?>
</a>
<?php endif; ?>
</nav>
<?php if (have_posts()) : ?>
<?php
$render_config = aksrt_resolve_post_list_render_config([
$render_config = jio_resolve_post_list_render_config([
'context' => 'archive',
'overrides' => ['type' => 'card'],
]);
aksrt_render_post_list(array_merge($render_config, ['wrapper_class' => 'content-stream__list']));
aksrt_render_pagination(null, [
jio_render_post_list(array_merge($render_config, ['wrapper_class' => 'content-stream__list']));
jio_render_pagination(null, [
'query_context' => 'archive',
'render_config' => $render_config,
]);
?>
<?php else : ?>
<div class="no-results">
<p><?php esc_html_e('暂无资源。', 'aksrt'); ?></p>
<p><?php esc_html_e('暂无资源。', 'jio'); ?></p>
<?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; ?>
</div>
<?php endif; ?>
+19 -19
View File
@@ -2,7 +2,7 @@
/**
* Generic archive template.
*
* @package AKSRT
* @package Jio
*/
defined('ABSPATH') || exit;
@@ -11,31 +11,31 @@ get_header();
global $wp_query;
$render_config = function_exists('aksrt_archive_get_render_config')
? aksrt_archive_get_render_config(['context' => 'archive'])
$render_config = function_exists('jio_archive_get_render_config')
? jio_archive_get_render_config(['context' => 'archive'])
: ['context' => 'archive', 'mode' => 'list'];
$has_sidebar = function_exists('aksrt_archive_has_sidebar')
? aksrt_archive_has_sidebar()
: (function_exists('aksrt_is_show_sidebar') ? aksrt_is_show_sidebar() : false);
$has_sidebar = function_exists('jio_archive_has_sidebar')
? jio_archive_has_sidebar()
: (function_exists('jio_is_show_sidebar') ? jio_is_show_sidebar() : false);
$archive_description = trim(wp_strip_all_tags((string) get_the_archive_description()));
$archive_summary = '';
if (is_author()) {
$author = get_queried_object();
if ($author instanceof WP_User) {
$archive_summary = sprintf(__('作者:%s', 'aksrt'), $author->display_name);
$archive_summary = sprintf(__('作者:%s', 'jio'), $author->display_name);
}
} elseif ($archive_description !== '') {
$archive_summary = $archive_description;
} elseif (isset($wp_query->found_posts)) {
$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
);
}
if (function_exists('aksrt_archive_open_shell')) {
aksrt_archive_open_shell([
if (function_exists('jio_archive_open_shell')) {
jio_archive_open_shell([
'main_class' => 'site-main container',
'has_sidebar' => $has_sidebar,
'page_kind' => 'archive',
@@ -45,16 +45,16 @@ if (function_exists('aksrt_archive_open_shell')) {
}
?>
<div class="content-main content-main--archive">
<?php if (function_exists('aksrt_breadcrumbs')) { aksrt_breadcrumbs(); } ?>
<?php if (function_exists('jio_breadcrumbs')) { jio_breadcrumbs(); } ?>
<?php
ob_start();
if (function_exists('aksrt_render_admin_quick_link')) {
aksrt_render_admin_quick_link('post-list/list', __('列表样式设置', 'aksrt'));
if (function_exists('jio_render_admin_quick_link')) {
jio_render_admin_quick_link('post-list/list', __('列表样式设置', 'jio'));
}
$admin_link = trim((string) ob_get_clean());
get_template_part('templates/parts/page-header', null, [
'eyebrow' => __('内容归档', 'aksrt'),
'eyebrow' => __('内容归档', 'jio'),
'title' => get_the_archive_title(),
'desc' => $archive_summary,
'extra' => $admin_link,
@@ -62,19 +62,19 @@ if (function_exists('aksrt_archive_open_shell')) {
?>
<?php
if (function_exists('aksrt_archive_render_stream')) {
aksrt_archive_render_stream(null, $render_config, [
if (function_exists('jio_archive_render_stream')) {
jio_archive_render_stream(null, $render_config, [
'page_kind' => 'archive',
'query_context' => 'archive',
'stream_class' => 'content-stream--archive',
'empty_text' => __('暂无文章', 'aksrt'),
'empty_text' => __('暂无文章', 'jio'),
]);
}
?>
</div>
<?php
if (function_exists('aksrt_archive_close_shell')) {
aksrt_archive_close_shell(['has_sidebar' => $has_sidebar]);
if (function_exists('jio_archive_close_shell')) {
jio_archive_close_shell(['has_sidebar' => $has_sidebar]);
} else {
echo '</div></div>';
if ($has_sidebar) { get_sidebar(); }
+6 -6
View File
@@ -104,7 +104,7 @@ const COMPONENT_STYLES = [
]
async function extractStyles() {
console.log('\n[AKSRT] 开始提取 NaiveUI 组件样式...\n')
console.log('\n[Jio] 开始提取 NaiveUI 组件样式...\n')
const allCss = []
let successCount = 0
@@ -137,9 +137,9 @@ async function extractStyles() {
// 静默跳过的后果是组件样式整体缺失(取色器曾因此没有尺寸和边框,糊成一团),
// 而页面不报错,只能靠肉眼发现。
if (code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND') {
console.error(`[AKSRT] 样式路径不存在,已跳过: ${componentPath}`)
console.error(`[Jio] 样式路径不存在,已跳过: ${componentPath}`)
} else {
console.error(`[AKSRT] 样式提取异常,已跳过: ${componentPath}: ${err && err.message}`)
console.error(`[Jio] 样式提取异常,已跳过: ${componentPath}: ${err && err.message}`)
}
failCount++
}
@@ -148,7 +148,7 @@ async function extractStyles() {
const cssOutput = allCss.join('\n\n')
if (!cssOutput.trim()) {
console.warn('[AKSRT] 警告:未提取到任何 NaiveUI 样式!')
console.warn('[Jio] 警告:未提取到任何 NaiveUI 样式!')
return null
}
@@ -165,7 +165,7 @@ ${cssOutput}`
await writeFile(cssFile, fullContent, 'utf-8')
console.log(`[AKSRT] NaiveUI 样式提取完成:`)
console.log(`[Jio] NaiveUI 样式提取完成:`)
console.log(` 成功: ${successCount} 个组件`)
console.log(` 跳过: ${failCount} 个组件`)
console.log(` 输出: ${cssFile}`)
@@ -176,6 +176,6 @@ ${cssOutput}`
// 执行
extractStyles().catch((err) => {
console.error('[AKSRT] 样式提取失败:', err)
console.error('[Jio] 样式提取失败:', err)
process.exit(1)
})
+2 -2
View File
@@ -3,10 +3,10 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AKSRT 主题设置</title>
<title>Jio 主题设置</title>
</head>
<body>
<div id="aksrt-admin-app"></div>
<div id="jio-admin-app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+2 -2
View File
@@ -1,11 +1,11 @@
{
"name": "aksrt-admin",
"name": "jio-admin",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aksrt-admin",
"name": "jio-admin",
"version": "1.0.0",
"dependencies": {
"@codemirror/lang-css": "^6.3.1",
+1 -1
View File
@@ -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","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}}
{"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
View File
@@ -6,9 +6,9 @@
<n-message-provider>
<n-dialog-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
class="aksrt-admin-sider"
class="jio-admin-sider"
bordered
collapse-mode="width"
:collapsed-width="64"
@@ -19,16 +19,16 @@
@collapse="collapsed = true"
@expand="collapsed = false"
>
<div class="aksrt-logo">
<div class="jio-logo">
<span class="logo-icon">AK</span>
<span v-if="!collapsed" class="aksrt-logo__copy">
<span class="logo-text">AKSRT</span>
<span class="aksrt-logo__version">v{{ themeVersion }}</span>
<span v-if="!collapsed" class="jio-logo__copy">
<span class="logo-text">Jio</span>
<span class="jio-logo__version">v{{ themeVersion }}</span>
</span>
</div>
<n-menu
class="aksrt-admin-menu"
class="jio-admin-menu"
:value="activeMenuKey"
:collapsed="collapsed"
:collapsed-width="64"
@@ -37,39 +37,39 @@
/>
</n-layout-sider>
<n-layout :native-scrollbar="false" class="aksrt-admin-content">
<div class="aksrt-admin-content__inner">
<n-layout :native-scrollbar="false" class="jio-admin-content">
<div class="jio-admin-content__inner">
<router-view v-slot="{ Component }">
<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" />
</div>
</transition>
</router-view>
<div v-if="pageError" class="aksrt-error-fallback">
<p class="aksrt-error-title">页面渲染异常</p>
<p class="aksrt-error-msg">{{ pageError }}</p>
<div v-if="pageError" class="jio-error-fallback">
<p class="jio-error-title">页面渲染异常</p>
<p class="jio-error-msg">{{ pageError }}</p>
<n-button size="small" @click="pageError = null">重试</n-button>
</div>
</div>
</n-layout>
</n-layout>
<div v-else class="aksrt-admin-mobile">
<div v-else class="jio-admin-mobile">
<MobileTopBar :title="currentPageTitle" @toggle="drawerActive = true" />
<n-drawer v-model:show="drawerActive" placement="left" :width="280">
<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="aksrt-logo__copy">
<span class="logo-text">AKSRT</span>
<span class="aksrt-logo__version">v{{ themeVersion }}</span>
<span class="jio-logo__copy">
<span class="logo-text">Jio</span>
<span class="jio-logo__version">v{{ themeVersion }}</span>
</span>
</div>
<n-menu
class="aksrt-admin-menu"
class="jio-admin-menu"
:value="activeMenuKey"
:options="menuOptions"
@update:value="handleMenuClick"
@@ -77,18 +77,18 @@
</n-drawer-content>
</n-drawer>
<div class="aksrt-admin-mobile__body">
<div class="jio-admin-mobile__body">
<router-view v-slot="{ Component }">
<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" />
</div>
</transition>
</router-view>
<div v-if="pageError" class="aksrt-error-fallback">
<p class="aksrt-error-title">页面渲染异常</p>
<p class="aksrt-error-msg">{{ pageError }}</p>
<div v-if="pageError" class="jio-error-fallback">
<p class="jio-error-title">页面渲染异常</p>
<p class="jio-error-msg">{{ pageError }}</p>
<n-button size="small" @click="pageError = null">重试</n-button>
</div>
</div>
@@ -113,7 +113,7 @@ const collapsed = ref(false)
const pageError = ref(null)
const isMobile = 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 adminThemeOverrides = {
@@ -184,7 +184,7 @@ onBeforeUnmount(() => {
provide('collapsed', collapsed)
onErrorCaptured((err) => {
console.error('[AKSRT] 组件渲染错误:', err)
console.error('[Jio] 组件渲染错误:', err)
pageError.value = err?.message || String(err)
return false
})
@@ -224,7 +224,7 @@ const menuOptions = ADMIN_SECTIONS.filter((section) => !section.hidden).map((sec
children: section.children.map((child) => ({
label: child.secondaryEntry ? `${child.label} · 次级` : child.label,
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'
})
const currentPageTitle = computed(() => route.meta?.title || 'AKSRT')
const currentPageTitle = computed(() => route.meta?.title || 'Jio')
function handleMenuClick(key) {
if (keyToPath[key]) {
@@ -267,17 +267,17 @@ function handleMenuClick(key) {
</script>
<style scoped>
.aksrt-logo {
.jio-logo {
display: flex;
align-items: center;
justify-content: flex-start;
height: 64px;
gap: 11px;
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;
}
@@ -289,14 +289,14 @@ function handleMenuClick(key) {
height: 28px;
flex: 0 0 28px;
border-radius: 7px;
background: var(--aksrt-color-primary);
background: var(--jio-color-primary);
color: #fff;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
}
.aksrt-logo__copy {
.jio-logo__copy {
display: flex;
align-items: baseline;
gap: 7px;
@@ -306,56 +306,56 @@ function handleMenuClick(key) {
.logo-text {
font-size: 17px;
font-weight: 700;
color: var(--aksrt-color-text);
color: var(--jio-color-text);
letter-spacing: .08em;
}
.aksrt-logo__version {
color: var(--aksrt-color-muted);
.jio-logo__version {
color: var(--jio-color-muted);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 10px;
letter-spacing: .02em;
}
.aksrt-admin-content__inner {
.jio-admin-content__inner {
width: 100%;
max-width: 1236px;
margin: 0 auto;
padding: 26px 28px 42px;
}
.aksrt-admin-mobile {
background: var(--aksrt-color-canvas);
.jio-admin-mobile {
background: var(--jio-color-canvas);
min-height: 100vh;
}
.aksrt-admin-mobile__body {
.jio-admin-mobile__body {
padding: 14px 12px 32px;
}
.aksrt-page-wrapper {
.jio-page-wrapper {
min-height: calc(100vh - 120px);
}
.aksrt-error-fallback {
.jio-error-fallback {
padding: 48px 20px;
text-align: center;
}
.aksrt-error-title {
.jio-error-title {
margin: 0 0 10px;
font-size: 16px;
color: #d03050;
}
.aksrt-error-msg {
.jio-error-msg {
margin: 0 0 16px;
font-size: 13px;
color: #777;
}
@media (max-width: 1100px) {
.aksrt-admin-content__inner {
.jio-admin-content__inner {
padding: 22px 20px 38px;
}
}
+17 -17
View File
@@ -1,16 +1,16 @@
/**
* AKSRT Admin API Client
* Jio Admin API Client
*
* Communicates with the WordPress REST API at /wp-json/aksrt/v1
* (or index.php?rest_route=/aksrt/v1 when pretty permalinks are off).
* Communicates with the WordPress REST API at /wp-json/jio/v1
* (or index.php?rest_route=/jio/v1 when pretty permalinks are off).
* Methods are organized into domain modules while the client keeps the shared
* request pipeline and schema-domain normalization contract.
*/
import { apiMethods } from './modules/index.js'
const API_ROOT = window.aksrtAdminData?.apiUrl || '/wp-json/aksrt/v1'
const NONCE = window.aksrtAdminData?.nonce || ''
const API_ROOT = window.jioAdminData?.apiUrl || '/wp-json/jio/v1'
const NONCE = window.jioAdminData?.nonce || ''
function createIdempotencyKey() {
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.
*
* Pretty: https://site/wp-json/aksrt/v1 + /settings/schema/global/common
* → 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/jio/v1/settings/schema/global/common
*
* Plain: https://site/index.php?rest_route=/aksrt/v1 + /settings/schema/global/common
* → 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=/jio/v1/settings/schema/global/common
*
* @param {string} baseUrl
* @param {string} endpoint path starting with /, may include ?query
@@ -56,7 +56,7 @@ function joinRestUrl(baseUrl, endpoint) {
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
if (base.includes('rest_route=')) {
@@ -68,11 +68,11 @@ function joinRestUrl(baseUrl, endpoint) {
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
if (path === '/') {
fullRoute = baseRoute
} else if (path.startsWith('/aksrt/')) {
} else if (path.startsWith('/jio/')) {
fullRoute = path
} else {
fullRoute = baseRoute + path
@@ -110,7 +110,7 @@ class ApiClient {
async request(endpointOrConfig, options = {}) {
// Dual-signature support:
// 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
if (typeof endpointOrConfig === 'object' && endpointOrConfig !== null) {
@@ -134,10 +134,10 @@ class ApiClient {
delete rest.data; delete rest.headers
}
if (endpoint.startsWith('/wp-json/aksrt/v1')) {
endpoint = endpoint.replace('/wp-json/aksrt/v1', '') || '/'
} else if (endpoint.startsWith('/aksrt/v1')) {
endpoint = endpoint.replace('/aksrt/v1', '') || '/'
if (endpoint.startsWith('/wp-json/jio/v1')) {
endpoint = endpoint.replace('/wp-json/jio/v1', '') || '/'
} else if (endpoint.startsWith('/jio/v1')) {
endpoint = endpoint.replace('/jio/v1', '') || '/'
}
let url = joinRestUrl(this.baseUrl, endpoint)
-5
View File
@@ -12,11 +12,6 @@ export const globalApiMethods = {
getFloatingButtons() { return this.request('/settings/floating-buttons') },
saveFloatingButtons(data) { return this.request('/settings/floating-buttons', { method: 'POST', body: JSON.stringify(data) }) },
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') },
saveCmsFeatures(cms_features) { return this.request('/settings/cms-features', { method: 'POST', body: JSON.stringify({ cms_features }) }) },
}
+5 -5
View File
@@ -1,13 +1,13 @@
/**
* 工单管理端 REST(命名空间 aksrt/v1/admin)。
* 工单管理端 REST(命名空间 jio/v1/admin)。
*
* TicketRestAdmin 把三块字典 CRUD(工单状态 / 部门 / 预设回复)挂在
* aksrt/v1/admin 下,全部要求 manage_options。api 客户端的 baseUrl 只到
* aksrt/v1,因此这里必须用 /aksrt/ 全路径形式(joinRestUrl 的
* startsWith('/aksrt/') 分支会原样拼接,不再追加 base 前缀)。
* jio/v1/admin 下,全部要求 manage_options。api 客户端的 baseUrl 只到
* jio/v1,因此这里必须用 /jio/ 全路径形式(joinRestUrl 的
* startsWith('/jio/') 分支会原样拼接,不再追加 base 前缀)。
*/
const ADMIN = '/aksrt/v1/admin/tickets'
const ADMIN = '/jio/v1/admin/tickets'
export const ticketsApiMethods = {
/* ---------------- 工单状态 ---------------- */
-4
View File
@@ -27,8 +27,4 @@ export const usersApiMethods = {
adjustUserCredits(id, 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}` : ''))
},
}
+32 -32
View File
@@ -1,35 +1,35 @@
<template>
<div class="aksrt-deferred-page">
<div class="aksrt-deferred-page__icon">
<div class="jio-deferred-page">
<div class="jio-deferred-page__icon">
<FaIcon :icon="resolvedIcon" :size="48" />
</div>
<h2 class="aksrt-deferred-page__title">{{ resolvedTitle }}</h2>
<p class="aksrt-deferred-page__desc">{{ resolvedDesc }}</p>
<div class="aksrt-deferred-page__meta">
<h2 class="jio-deferred-page__title">{{ resolvedTitle }}</h2>
<p class="jio-deferred-page__desc">{{ resolvedDesc }}</p>
<div class="jio-deferred-page__meta">
<n-tag size="small" type="info">{{ resolvedStatus }}</n-tag>
<n-tag v-if="resolvedPhase" size="small" type="warning">Phase {{ resolvedPhase }}</n-tag>
</div>
<div v-if="resolvedDependsOn.length" class="aksrt-deferred-page__depends">
<span class="aksrt-deferred-page__label">依赖</span>
<div class="aksrt-deferred-page__depends-list">
<div v-if="resolvedDependsOn.length" class="jio-deferred-page__depends">
<span class="jio-deferred-page__label">依赖</span>
<div class="jio-deferred-page__depends-list">
<n-tag v-for="item in resolvedDependsOn" :key="item" size="small" type="default">{{ item }}</n-tag>
</div>
</div>
<div v-if="resolvedReason" class="aksrt-deferred-page__reason">
<span class="aksrt-deferred-page__label">原因</span>
<div v-if="resolvedReason" class="jio-deferred-page__reason">
<span class="jio-deferred-page__label">原因</span>
<p>{{ resolvedReason }}</p>
</div>
<div v-if="resolvedFeatures && resolvedFeatures.length" class="aksrt-deferred-page__features">
<div v-if="resolvedFeatures && resolvedFeatures.length" class="jio-deferred-page__features">
<div
v-for="(feat, i) in resolvedFeatures"
:key="i"
class="aksrt-deferred-page__feature"
class="jio-deferred-page__feature"
>
<FaIcon icon="fa fas fa-check-circle" :size="14" />
<span>{{ feat }}</span>
</div>
</div>
<div class="aksrt-deferred-page__badge">
<div class="jio-deferred-page__badge">
<n-tag type="info" size="small">即将推出</n-tag>
</div>
</div>
@@ -78,7 +78,7 @@ const resolvedFeatures = computed(() => {
</script>
<style scoped>
.aksrt-deferred-page {
.jio-deferred-page {
display: flex;
flex-direction: column;
align-items: center;
@@ -88,75 +88,75 @@ const resolvedFeatures = computed(() => {
padding: 40px 20px;
}
.aksrt-deferred-page__icon {
color: var(--aksrt-color-border-strong);
.jio-deferred-page__icon {
color: var(--jio-color-border-strong);
margin-bottom: 20px;
}
.aksrt-deferred-page__title {
.jio-deferred-page__title {
margin: 0 0 10px;
font-size: 20px;
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;
font-size: 14px;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
max-width: 400px;
line-height: 1.6;
}
.aksrt-deferred-page__meta {
.jio-deferred-page__meta {
display: flex;
gap: 10px;
margin-bottom: 18px;
}
.aksrt-deferred-page__depends,
.aksrt-deferred-page__reason {
.jio-deferred-page__depends,
.jio-deferred-page__reason {
width: min(520px, 100%);
margin-bottom: 18px;
text-align: left;
}
.aksrt-deferred-page__label {
.jio-deferred-page__label {
display: inline-block;
margin-bottom: 8px;
font-size: 12px;
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;
flex-wrap: wrap;
gap: 8px;
}
.aksrt-deferred-page__reason p {
.jio-deferred-page__reason p {
margin: 0;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
line-height: 1.7;
}
.aksrt-deferred-page__features {
.jio-deferred-page__features {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 20px;
}
.aksrt-deferred-page__feature {
.jio-deferred-page__feature {
display: flex;
align-items: center;
gap: 8px;
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;
}
</style>
+11 -11
View File
@@ -1,10 +1,10 @@
<template>
<div class="aksrt-empty-state">
<div class="aksrt-empty-state__icon">
<div class="jio-empty-state">
<div class="jio-empty-state__icon">
<i :class="icon" style="font-size: 48px;"></i>
</div>
<h3 class="aksrt-empty-state__title">{{ title }}</h3>
<p class="aksrt-empty-state__desc">{{ desc }}</p>
<h3 class="jio-empty-state__title">{{ title }}</h3>
<p class="jio-empty-state__desc">{{ desc }}</p>
</div>
</template>
@@ -17,7 +17,7 @@ defineProps({
</script>
<style scoped>
.aksrt-empty-state {
.jio-empty-state {
display: flex;
flex-direction: column;
align-items: center;
@@ -26,22 +26,22 @@ defineProps({
text-align: center;
}
.aksrt-empty-state__icon {
.jio-empty-state__icon {
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;
font-size: 18px;
font-weight: 600;
color: var(--aksrt-color-text);
color: var(--jio-color-text);
}
.aksrt-empty-state__desc {
.jio-empty-state__desc {
margin: 0;
font-size: 13px;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
max-width: 360px;
line-height: 1.65;
}
+16 -16
View File
@@ -1,7 +1,7 @@
<template>
<header class="aksrt-mobile-topbar">
<header class="jio-mobile-topbar">
<button
class="aksrt-mobile-topbar__menu"
class="jio-mobile-topbar__menu"
type="button"
aria-label="打开菜单"
@click="$emit('toggle')"
@@ -13,35 +13,35 @@
</svg>
</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>
</template>
<script setup>
defineProps({
title: { type: String, default: 'AKSRT' },
title: { type: String, default: 'Jio' },
})
defineEmits(['toggle'])
</script>
<style scoped>
.aksrt-mobile-topbar {
.jio-mobile-topbar {
display: flex;
align-items: center;
gap: 10px;
height: 48px;
padding: 0 14px;
background: var(--aksrt-color-surface);
border-bottom: 1px solid var(--aksrt-color-border);
background: var(--jio-color-surface);
border-bottom: 1px solid var(--jio-color-border);
position: sticky;
top: 0;
z-index: 50;
}
.aksrt-mobile-topbar__menu {
.jio-mobile-topbar__menu {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -50,26 +50,26 @@ defineEmits(['toggle'])
border: none;
border-radius: 8px;
background: transparent;
color: var(--aksrt-color-text);
color: var(--jio-color-text);
cursor: pointer;
margin-left: -6px;
}
.aksrt-mobile-topbar__menu:hover {
background: var(--aksrt-color-primary-soft);
.jio-mobile-topbar__menu:hover {
background: var(--jio-color-primary-soft);
}
.aksrt-mobile-topbar__title {
.jio-mobile-topbar__title {
flex: 1 1 auto;
font-size: 15px;
font-weight: 600;
color: var(--aksrt-color-text);
color: var(--jio-color-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.aksrt-mobile-topbar__brand {
.jio-mobile-topbar__brand {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
@@ -77,7 +77,7 @@ defineEmits(['toggle'])
width: 28px;
height: 28px;
border-radius: 8px;
background: var(--aksrt-color-primary);
background: var(--jio-color-primary);
color: #fff;
font-size: 11px;
font-weight: 700;
+17 -17
View File
@@ -1,15 +1,15 @@
<template>
<header class="aksrt-page-header">
<div class="aksrt-page-header__main">
<h2 class="aksrt-page-header__title">{{ title }}</h2>
<p v-if="desc" class="aksrt-page-header__desc">
<header class="jio-page-header">
<div class="jio-page-header__main">
<h2 class="jio-page-header__title">{{ title }}</h2>
<p v-if="desc" class="jio-page-header__desc">
<slot name="desc">{{ desc }}</slot>
</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" />
</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" />
</div>
</header>
@@ -23,7 +23,7 @@ defineProps({
</script>
<style scoped>
.aksrt-page-header {
.jio-page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
@@ -32,28 +32,28 @@ defineProps({
flex-wrap: wrap;
}
.aksrt-page-header__main {
.jio-page-header__main {
flex: 1 1 auto;
min-width: 0;
}
.aksrt-page-header__title {
.jio-page-header__title {
margin: 0 0 6px;
color: var(--aksrt-color-text);
color: var(--jio-color-text);
font-size: 22px;
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1.3;
}
.aksrt-page-header__desc {
.jio-page-header__desc {
margin: 0;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
font-size: 13px;
line-height: 1.65;
}
.aksrt-page-header__actions {
.jio-page-header__actions {
flex: 0 0 auto;
display: flex;
align-items: center;
@@ -61,20 +61,20 @@ defineProps({
}
@media (max-width: 768px) {
.aksrt-page-header {
.jio-page-header {
margin-bottom: 14px;
gap: 10px;
}
.aksrt-page-header__title {
.jio-page-header__title {
font-size: 18px;
}
.aksrt-page-header__desc {
.jio-page-header__desc {
font-size: 12px;
}
.aksrt-page-header__actions {
.jio-page-header__actions {
width: 100%;
}
}
@@ -38,8 +38,9 @@
<n-form-item label="AccessKey Secret">
<n-input
v-model:value="model.access_key_secret"
type="password"
type="text"
show-password-on="click"
class="jio-secret-input"
autocomplete="new-password"
placeholder="阿里云 RAM 用户 AccessKey Secret"
@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-form-item>
<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-form-item>
<n-form-item label="主题">
@@ -5,7 +5,7 @@
<n-input v-model:value="model.captcha_id" placeholder="极验 GeeTest CaptchaID" autocomplete="off" />
</n-form-item>
<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-form-item>
<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-form-item>
<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-form-item>
@@ -18,7 +18,7 @@
<n-input v-model:value="model.app_id" placeholder="从验证码控制台获取 CaptchaAppId" autocomplete="off" />
</n-form-item>
<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>
<template #feedback>请从「验证管理 → 基础配置」完整复制,注意区分 I/l/1/O/0</template>
</n-form-item>
@@ -1,5 +1,5 @@
<template>
<n-form label-placement="left" label-width="110" class="aksrt-form float-common-fields">
<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>
@@ -10,7 +10,7 @@
<div class="float-common-fields__colors">
<div class="float-common-fields__color">
<span>自定义图标颜色</span>
<AksColorPicker
<JioColorPicker
:model-value="modelValue.color?.color || ''"
alpha
clearable
@@ -19,7 +19,7 @@
</div>
<div class="float-common-fields__color">
<span>自定义背景颜色</span>
<AksColorPicker
<JioColorPicker
:model-value="modelValue.color?.bg || ''"
alpha
clearable
@@ -33,7 +33,7 @@
</template>
<script setup>
import AksColorPicker from '@/components/form/AksColorPicker.vue'
import JioColorPicker from '@/components/form/JioColorPicker.vue'
const props = defineProps({
modelValue: { type: Object, required: true },
@@ -1,10 +1,10 @@
<template>
<div class="float-btn-panel">
<div class="aksrt-sortable__head" v-bind="headAttrs">
<AksSortHandle @click.stop />
<div class="aksrt-sortable__main">
<div class="aksrt-sortable__title-row">
<span class="aksrt-sortable__title">{{ meta.title }}</span>
<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>
@@ -12,18 +12,18 @@
</template>
<n-tag v-else size="small" :bordered="false">{{ modelValue.buttons.length }} 个</n-tag>
</div>
<span v-if="meta.warn" class="aksrt-sortable__desc float-btn-panel__warn">
<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 aksrt-sortable__toggle"
class="fa fas jio-sortable__toggle"
:class="expanded ? 'fa-chevron-down' : 'fa-chevron-right'"
aria-hidden="true"
/>
</div>
<div v-show="expanded" class="aksrt-sortable__body">
<div v-show="expanded" class="jio-sortable__body">
<div class="float-btn-panel__subtitle">{{ meta.panel }}</div>
<FloatMoreButtonsEditor
@@ -35,7 +35,7 @@
<template v-else>
<FloatBtnCommonFields :model-value="modelValue" @update:model-value="emit('update:modelValue', $event)" />
<n-form label-placement="left" label-width="110" class="aksrt-form float-btn-panel__extra">
<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>
@@ -49,7 +49,7 @@
</n-form-item>
<n-form-item v-if="modelValue.type === 'service_wechat'" label="微信二维码">
<AksMediaField
<JioMediaField
:model-value="modelValue.wechat_img"
placeholder="选择二维码图片"
@update:model-value="patch({ wechat_img: $event })"
@@ -62,11 +62,11 @@
</template>
<script setup>
/** 悬浮按钮排序列表中的单个卡片;外层 .aksrt-sortable__item 由 FloatingButtonsSettings 提供。 */
/** 悬浮按钮排序列表中的单个卡片;外层 .jio-sortable__item 由 FloatingButtonsSettings 提供。 */
import { computed, ref } from 'vue'
import FaIconPicker from '@/components/form/FaIconPicker.vue'
import AksMediaField from '@/components/form/AksMediaField.vue'
import AksSortHandle from '@/components/form/AksSortHandle.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'
@@ -93,12 +93,12 @@ function patch(partial) {
<style scoped>
.float-btn-panel__warn {
color: var(--aksrt-color-warning, #D97706);
color: var(--jio-color-warning, #D97706);
}
.float-btn-panel__subtitle {
margin-bottom: 12px;
color: var(--aksrt-color-text);
color: var(--jio-color-text);
font-size: 13px;
font-weight: 600;
}
@@ -24,7 +24,7 @@
</n-button>
</template>
<n-form label-placement="left" label-width="110" class="aksrt-form">
<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>
@@ -16,32 +16,21 @@ export const FLOAT_BTN_META = {
more: { title: '更多按钮', panel: '添加更多自定义按钮' },
}
const themeUri = (window.aksrtAdminData?.themeUri || '').replace(/\/$/, '')
/** 「更多按钮」出厂默认项(对齐参考主题 float_btn.more 默认值)。 */
/** 「更多按钮」出厂默认项:不预设任何第三方指向,只给一个图标按钮。 */
export function giftMoreButton() {
return {
desc: '本站同款主题模板',
icon: 'zibsvg-gift-color',
desc: '',
icon: 'jiosvg-gift-color',
link: { url: '', target: '' },
hover: `<a href="https://www.zibll.com/" target="_blank">
<div class="flex c-red">
<img class="flex0" alt="zibll子比主题" src="${themeUri}/assets/img/logo.png" height="30">
<div class="flex1 ml10">
<dt>本站同款主题模板</dt>
<div class="px12 mt10 muted-color">zibll子比主题是一款漂亮优雅的网站主题模板,功能强大,配置简单。</div>
<div class="but mt10 p2-10 c-blue btn-block px12">查看详情</div>
</div>
</div>
</a>`,
hover_width: 240,
hover: '',
hover_width: 200,
pc_s: true,
m_s: true,
color: { color: '', bg: 'rgba(255, 111, 6, 0.2)' },
}
}
/** 「添加按钮」新增项默认值(参考主题 group 字段默认值)。 */
/** 「添加按钮」新增项默认值。 */
export function defaultMoreButton() {
return {
desc: '',
@@ -1,100 +0,0 @@
<template>
<div class="aksrt-image-select" role="radiogroup">
<button
v-for="opt in options"
:key="opt.value"
type="button"
role="radio"
class="aksrt-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="aksrt-image-select__img" />
<span v-else class="aksrt-image-select__placeholder">
<FaIcon :icon="opt.icon || 'fa fas fa-image'" :size="20" />
</span>
<span class="aksrt-image-select__label">{{ opt.label }}</span>
</button>
</div>
</template>
<script setup>
/** image_select:带预览图的单选。 */
import FaIcon from './FaIcon.vue'
defineProps({
modelValue: { default: null },
options: { type: Array, default: () => [] },
disabled: { type: Boolean, default: false },
})
const emit = defineEmits(['update:modelValue'])
</script>
<style scoped>
.aksrt-image-select {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.aksrt-image-select__item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
min-width: 80px;
padding: 8px;
border: 2px solid var(--aksrt-color-border);
border-radius: 8px;
background: var(--aksrt-color-surface);
cursor: pointer;
transition: border-color .15s ease, background-color .15s ease;
}
.aksrt-image-select__item:hover,
.aksrt-image-select__item:focus-visible {
outline: none;
border-color: var(--aksrt-color-primary);
}
.aksrt-image-select__item.is-active {
border-color: var(--aksrt-color-primary);
background: var(--aksrt-color-primary-soft);
}
.aksrt-image-select__item:disabled {
cursor: not-allowed;
}
.aksrt-image-select__img {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 4px;
}
.aksrt-image-select__placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 60px;
border-radius: 4px;
background: var(--aksrt-color-canvas);
color: var(--aksrt-color-border-strong);
}
.aksrt-image-select__label {
color: var(--aksrt-color-text-secondary);
font-size: 11px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.aksrt-image-select__item {
transition: none;
}
}
</style>
@@ -1,5 +1,5 @@
<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 }">
<template #trigger>
<n-input
@@ -19,8 +19,8 @@
</n-input>
</template>
<div class="aksrt-icon-picker__panel" @click="onPanelClick">
<div class="aksrt-icon-picker__search">
<div class="jio-icon-picker__panel" @click="onPanelClick">
<div class="jio-icon-picker__search">
<n-input v-model:value="search" placeholder="搜索图标..." size="small" clearable>
<template #prefix>
<FaIcon icon="fa fas fa-search" :size="12" />
@@ -28,12 +28,12 @@
</n-input>
</div>
<div class="aksrt-icon-picker__tabs">
<div class="jio-icon-picker__tabs">
<button
v-for="tab in tabs"
:key="tab.id"
type="button"
class="aksrt-icon-picker__tab"
class="jio-icon-picker__tab"
:class="{ 'is-active': activeTab === tab.id }"
@click="activeTab = tab.id"
>
@@ -42,23 +42,23 @@
</div>
<!-- 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='&lt;svg viewBox="0 0 1024 1024"&gt;&lt;path d="…"&gt;&lt;/path&gt;&lt;/svg&gt;' :rows="4" />
<div class="aksrt-icon-picker__custom-actions">
<span class="aksrt-icon-picker__custom-preview" v-html="customPreview"></span>
<div class="jio-icon-picker__custom-actions">
<span class="jio-icon-picker__custom-preview" v-html="customPreview"></span>
<n-button size="small" type="primary" :disabled="!customPreview" @click="insertCustom">
确认插入
</n-button>
</div>
<div
v-if="sessionCustom.length"
class="aksrt-icon-picker__grid aksrt-icon-picker__grid--session"
data-aksrt-icon-grid="custom"
class="jio-icon-picker__grid jio-icon-picker__grid--session"
data-jio-icon-grid="custom"
>
<div
v-for="(svg, i) in sessionCustom"
:key="i"
class="aksrt-icon-picker__item"
class="jio-icon-picker__item"
:class="{ 'is-active': modelValue === `customsvg-${svg}` }"
title="customsvg"
:data-icon="`customsvg-${svg}`"
@@ -68,11 +68,11 @@
</div>
<!-- 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
v-for="icon in filteredIcons"
:key="icon"
class="aksrt-icon-picker__item"
class="jio-icon-picker__item"
:class="{ 'is-active': modelValue === composeIconValue(activeGroup, icon) }"
:title="icon"
:data-icon="composeIconValue(activeGroup, icon)"
@@ -80,11 +80,11 @@
<i v-if="activeGroup.type === 'fa'" :class="[activeTab, 'fa-' + icon]"></i>
<span
v-else
class="aksrt-icon-picker__preview-svg"
v-html="zibsvgMarkup(`zibsvg-${icon}`)"
class="jio-icon-picker__preview-svg"
v-html="jiosvgMarkup(`jiosvg-${icon}`)"
></span>
</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>
@@ -96,7 +96,7 @@
<script setup>
/**
* 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.
*/
import { ref, computed, onMounted } from 'vue'
@@ -105,7 +105,7 @@ import IconValue from './IconValue.vue'
import api from '@/api/index.js'
import {
cleanSvg,
zibsvgMarkup,
jiosvgMarkup,
composeIconValue,
} from '@/utils/icons.js'
@@ -120,7 +120,7 @@ const FALLBACK_TABS = [
{ id: 'fas', title: 'Solid', type: 'fa', icons: [] },
{ id: 'far', title: 'Regular', 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: [] },
]
@@ -176,53 +176,53 @@ onMounted(async () => {
</script>
<style scoped>
.aksrt-icon-picker__panel {
/* The popover teleports to <body>, outside #aksrt-admin-app where the
.jio-icon-picker__panel {
/* The popover teleports to <body>, outside #jio-admin-app where the
palette is declared — re-declare every token the panel consumes. */
--aksrt-color-primary: #18A058;
--aksrt-color-primary-hover: #36AD6A;
--aksrt-color-primary-soft: rgba(24, 160, 88, 0.12);
--aksrt-color-text-secondary: #465149;
--aksrt-color-muted: #6F7A72;
--aksrt-color-border: #E2E8E3;
--jio-color-primary: #18A058;
--jio-color-primary-hover: #36AD6A;
--jio-color-primary-soft: rgba(24, 160, 88, 0.12);
--jio-color-text-secondary: #465149;
--jio-color-muted: #6F7A72;
--jio-color-border: #E2E8E3;
padding: 12px;
}
.aksrt-icon-picker__search {
.jio-icon-picker__search {
margin-bottom: 10px;
}
.aksrt-icon-picker__tabs {
.jio-icon-picker__tabs {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
}
.aksrt-icon-picker__tab {
.jio-icon-picker__tab {
padding: 3px 10px;
font-size: 13px;
line-height: 20px;
border: 1px solid var(--aksrt-color-border);
border: 1px solid var(--jio-color-border);
border-radius: 4px;
background: transparent;
color: var(--aksrt-color-text-secondary);
color: var(--jio-color-text-secondary);
cursor: pointer;
transition: all 0.15s;
}
.aksrt-icon-picker__tab:hover {
border-color: var(--aksrt-color-primary);
color: var(--aksrt-color-primary);
.jio-icon-picker__tab:hover {
border-color: var(--jio-color-primary);
color: var(--jio-color-primary);
}
.aksrt-icon-picker__tab.is-active {
border-color: var(--aksrt-color-primary);
background: var(--aksrt-color-primary);
.jio-icon-picker__tab.is-active {
border-color: var(--jio-color-primary);
background: var(--jio-color-primary);
color: #fff;
}
.aksrt-icon-picker__grid {
.jio-icon-picker__grid {
display: grid;
grid-template-columns: repeat(8, 1fr);
gap: 4px;
@@ -230,7 +230,7 @@ onMounted(async () => {
overflow-y: auto;
}
.aksrt-icon-picker__item {
.jio-icon-picker__item {
display: flex;
align-items: center;
justify-content: center;
@@ -239,60 +239,60 @@ onMounted(async () => {
border-radius: 6px;
cursor: pointer;
font-size: 16px;
color: var(--aksrt-color-text-secondary);
color: var(--jio-color-text-secondary);
transition: all 0.15s;
}
.aksrt-icon-picker__item:hover {
background: var(--aksrt-color-primary-soft);
color: var(--aksrt-color-primary);
.jio-icon-picker__item:hover {
background: var(--jio-color-primary-soft);
color: var(--jio-color-primary);
}
.aksrt-icon-picker__item.is-active {
background: var(--aksrt-color-primary);
.jio-icon-picker__item.is-active {
background: var(--jio-color-primary);
color: #fff;
}
.aksrt-icon-picker__item :deep(svg),
.aksrt-icon-picker__preview-svg :deep(svg),
.aksrt-icon-picker__preview-svg svg {
.jio-icon-picker__item :deep(svg),
.jio-icon-picker__preview-svg :deep(svg),
.jio-icon-picker__preview-svg svg {
width: 18px;
height: 18px;
fill: currentColor;
}
.aksrt-icon-picker__preview-svg {
.jio-icon-picker__preview-svg {
display: inline-flex;
}
.aksrt-icon-picker__empty {
.jio-icon-picker__empty {
grid-column: 1 / -1;
text-align: center;
padding: 20px;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
font-size: 13px;
}
.aksrt-icon-picker__custom-actions {
.jio-icon-picker__custom-actions {
display: flex;
align-items: center;
gap: 10px;
margin-top: 8px;
}
.aksrt-icon-picker__custom-preview {
.jio-icon-picker__custom-preview {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 1px dashed var(--aksrt-color-muted);
border: 1px dashed var(--jio-color-muted);
border-radius: 4px;
}
.aksrt-icon-picker__grid--session {
.jio-icon-picker__grid--session {
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid var(--aksrt-color-muted);
border-top: 1px solid var(--jio-color-muted);
}
</style>
@@ -3,25 +3,25 @@
<i v-if="isFaValue(value)" :class="faClass" aria-hidden="true"></i>
<!-- Built-in sprite value -->
<span
v-else-if="isZibsvgValue(value)"
class="aksrt-icon-value__svg"
v-html="zibsvgMarkup(value)"
v-else-if="isJiosvgValue(value)"
class="jio-icon-value__svg"
v-html="jiosvgMarkup(value)"
></span>
<!-- Inline custom SVG value -->
<span
v-else-if="isCustomSvgValue(value)"
class="aksrt-icon-value__svg"
class="jio-icon-value__svg"
v-html="cleanSvg(value.slice(10))"
></span>
</template>
<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.
*/
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({
value: { type: String, default: '' },
@@ -35,12 +35,12 @@ const faClass = computed(() => {
</script>
<style scoped>
.aksrt-icon-value__svg {
.jio-icon-value__svg {
display: inline-flex;
}
.aksrt-icon-value__svg :deep(svg),
.aksrt-icon-value__svg svg {
.jio-icon-value__svg :deep(svg),
.jio-icon-value__svg svg {
width: 1em;
height: 1em;
fill: currentColor;
@@ -1,5 +1,5 @@
<template>
<div ref="editorRef" class="aks-code-editor" :class="{ 'is-disabled': disabled }" />
<div ref="editorRef" class="jio-code-editor" :class="{ 'is-disabled': disabled }" />
</template>
<script setup>
@@ -86,19 +86,19 @@ onBeforeUnmount(() => {
</script>
<style scoped>
.aks-code-editor {
.jio-code-editor {
border: 1px solid var(--n-border-color, #e0e0e6);
border-radius: 4px;
overflow: hidden;
min-height: calc(var(--rows, 8) * 22px);
}
.aks-code-editor :deep(.cm-editor) {
.jio-code-editor :deep(.cm-editor) {
font-size: 13px;
}
.aks-code-editor :deep(.cm-editor.cm-focused) {
.jio-code-editor :deep(.cm-editor.cm-focused) {
outline: none;
}
.aks-code-editor.is-disabled {
.jio-code-editor.is-disabled {
opacity: 0.6;
pointer-events: none;
}
@@ -17,7 +17,7 @@
<script setup>
/**
* 后台唯一取色器:所有颜色字段都经由此组件,输出与
* PHP `AKSRT\Core\Color::sanitize($value, $allow_alpha)` 同一规则的规范值。
* PHP `Jio\Core\Color::sanitize($value, $allow_alpha)` 同一规则的规范值。
* 清空 / 非法值统一回传空串。
*/
import { computed } from 'vue'
@@ -1,6 +1,6 @@
<template>
<!-- Between number (min/max pair) -->
<div class="aksrt-composite aksrt-composite--between">
<div class="jio-composite jio-composite--between">
<n-input-number
:value="betweenValue('min')"
:min="schema.min"
@@ -11,7 +11,7 @@
style="flex: 1"
@update:value="update('min', $event)"
/>
<span class="aksrt-composite__sep">—</span>
<span class="jio-composite__sep">—</span>
<n-input-number
:value="betweenValue('max')"
:min="schema.min"
@@ -22,7 +22,7 @@
style="flex: 1"
@update:value="update('max', $event)"
/>
<span v-if="schema.unit" class="aksrt-composite__unit">{{ schema.unit }}</span>
<span v-if="schema.unit" class="jio-composite__unit">{{ schema.unit }}</span>
</div>
</template>
@@ -52,23 +52,23 @@ function betweenValue(part) {
</script>
<style scoped>
.aksrt-composite {
.jio-composite {
display: flex;
align-items: center;
gap: 8px;
}
.aksrt-composite--between {
.jio-composite--between {
max-width: 360px;
}
.aksrt-composite__sep {
color: var(--aksrt-color-border-strong);
.jio-composite__sep {
color: var(--jio-color-border-strong);
font-size: 14px;
}
.aksrt-composite__unit {
color: var(--aksrt-color-muted);
.jio-composite__unit {
color: var(--jio-color-muted);
font-size: 13px;
white-space: nowrap;
}
@@ -1,19 +1,19 @@
<template>
<div class="aksrt-field" :class="fieldClasses">
<div v-if="schema.label" class="aksrt-field__label-row">
<label class="aksrt-field__label">
<div class="jio-field" :class="fieldClasses">
<div v-if="schema.label" class="jio-field__label-row">
<label class="jio-field__label">
{{ schema.label }}
<span v-if="schema.required" class="aksrt-field__required">*</span>
<span v-if="schema.required" class="jio-field__required">*</span>
</label>
<n-tooltip v-if="schema.help" trigger="hover">
<template #trigger>
<FaIcon icon="fa fas fa-question-circle" :size="13" class="aksrt-field__help-icon" />
<FaIcon icon="fa fas fa-question-circle" :size="13" class="jio-field__help-icon" />
</template>
{{ schema.help }}
</n-tooltip>
</div>
<p v-if="schema.description && !schema.help" class="aksrt-field__desc">
<p v-if="schema.description && !schema.help" class="jio-field__desc">
{{ schema.description }}
</p>
@@ -25,22 +25,25 @@
/>
<!-- Input (text, email, url, password) -->
<div v-else-if="schema.type === 'input' || schema.type === 'text' || schema.type === 'url'" class="aksrt-field__input-wrap">
<div v-else-if="schema.type === 'input' || schema.type === 'text' || schema.type === 'url'" class="jio-field__input-wrap">
<n-input
:value="secretDisplayValue"
:placeholder="schema.placeholder || ''"
:type="schema.inputType || 'text'"
:type="schema.secret ? 'text' : (schema.inputType || 'text')"
:class="{ 'jio-secret-input': schema.secret }"
:disabled="schema.disabled"
:name="schema.secret ? 'jio-secret-' + schema.key : schema.key"
:input-props="schema.secret ? secretInputProps : undefined"
@update:value="emit('update:modelValue', $event)"
>
<template v-if="schema.prefix" #prefix>{{ schema.prefix }}</template>
<template v-if="schema.suffix" #suffix>{{ schema.suffix }}</template>
</n-input>
<AksSecretClear :visible="secretClearVisible" :marked="secretMarkedForClear" @toggle="toggleSecretClear" />
<JioSecretClear :visible="secretClearVisible" :marked="secretMarkedForClear" @toggle="toggleSecretClear" />
</div>
<!-- Textarea(secret 时不回显已配置值,聚焦后输入新值即覆盖) -->
<div v-else-if="schema.type === 'textarea'" class="aksrt-field__input-wrap">
<div v-else-if="schema.type === 'textarea'" class="jio-field__input-wrap">
<n-input
:value="secretTextareaValue"
type="textarea"
@@ -49,11 +52,11 @@
:disabled="schema.disabled"
@update:value="emit('update:modelValue', $event)"
/>
<AksSecretClear :visible="secretClearVisible" :marked="secretMarkedForClear" @toggle="toggleSecretClear" />
<JioSecretClear :visible="secretClearVisible" :marked="secretMarkedForClear" @toggle="toggleSecretClear" />
</div>
<!-- Number -->
<div v-else-if="schema.type === 'number' || schema.type === 'spinner'" class="aksrt-field__number">
<div v-else-if="schema.type === 'number' || schema.type === 'spinner'" class="jio-field__number">
<n-input-number
:value="modelValue"
:min="schema.min"
@@ -64,11 +67,11 @@
style="width: 100%"
@update:value="emit('update:modelValue', $event)"
/>
<span v-if="schema.unit" class="aksrt-field__unit">{{ schema.unit }}</span>
<span v-if="schema.unit" class="jio-field__unit">{{ schema.unit }}</span>
</div>
<!-- Select -->
<div v-else-if="schema.type === 'select'" class="aksrt-field__ordered-select">
<div v-else-if="schema.type === 'select'" class="jio-field__ordered-select">
<n-select
:value="modelValue"
:options="normalizedOptions"
@@ -78,7 +81,7 @@
:disabled="schema.disabled"
@update:value="emit('update:modelValue', $event)"
/>
<AksSelectedOrder
<JioSelectedOrder
v-if="schema.multiple && schema.sortable && Array.isArray(modelValue)"
:model-value="modelValue"
:options="normalizedOptions"
@@ -87,7 +90,7 @@
/>
</div>
<div v-else-if="schema.type === 'async_select'" class="aksrt-field__ordered-select">
<div v-else-if="schema.type === 'async_select'" class="jio-field__ordered-select">
<n-select
:value="schema.multiple === true && !Array.isArray(modelValue) ? [] : modelValue"
:options="asyncOptions"
@@ -100,7 +103,7 @@
@search="handleAsyncSearch"
@update:value="emit('update:modelValue', $event)"
/>
<AksSelectedOrder
<JioSelectedOrder
v-if="schema.multiple && schema.sortable && Array.isArray(modelValue)"
:model-value="modelValue"
:options="asyncOptions"
@@ -199,7 +202,7 @@
</n-checkbox-group>
<!-- Color picker -->
<AksColorPicker
<JioColorPicker
v-else-if="schema.type === 'color'"
:model-value="typeof modelValue === 'string' ? modelValue : ''"
:alpha="!!schema.showAlpha"
@@ -208,12 +211,12 @@
@update:model-value="emit('update:modelValue', $event)"
/>
<div v-else-if="schema.type === 'palette'" class="aksrt-field__palette">
<div v-else-if="schema.type === 'palette'" class="jio-field__palette">
<button
v-for="opt in normalizedOptions"
:key="opt.value"
type="button"
class="aksrt-field__palette-item"
class="jio-field__palette-item"
:class="{ 'is-active': modelValue === opt.value }"
:style="{ background: opt.label }"
:title="opt.value"
@@ -230,7 +233,7 @@
/>
<!-- Media upload -->
<AksMediaField
<JioMediaField
v-else-if="schema.type === 'media' || schema.type === 'upload'"
:model-value="modelValue"
:accept="schema.accept"
@@ -239,7 +242,7 @@
/>
<!-- WordPress image gallery (attachment ID array) -->
<AksGalleryField
<JioGalleryField
v-else-if="schema.type === 'gallery'"
:model-value="Array.isArray(modelValue) ? modelValue : []"
:title="schema.label || '选择图片'"
@@ -259,7 +262,7 @@
/>
<!-- Code editor with syntax highlighting (CodeMirror 6) -->
<AksCodeEditor
<JioCodeEditor
v-else-if="schema.type === 'code'"
:model-value="modelValue"
:language="schema.language || 'html'"
@@ -281,7 +284,7 @@
<!-- Pure HTML content (no form field, just display) -->
<div
v-else-if="schema.type === 'content'"
class="aksrt-field__content"
class="jio-field__content"
v-html="schema.content || modelValue || ''"
/>
@@ -316,7 +319,7 @@
/>
<!-- Object composite: between_number (min/max pair) -->
<AksCompositeField
<JioCompositeField
v-else-if="schema.type === 'between_number'"
:schema="schema"
:model-value="modelValue"
@@ -324,33 +327,34 @@
/>
<!-- Image select (radio with image preview) -->
<AksImageSelectField
<JioImageSelectField
v-else-if="schema.type === 'image_select'"
:model-value="modelValue"
:options="normalizedOptions"
:disabled="schema.disabled"
:preview-width="schema.previewWidth"
@update:model-value="emit('update:modelValue', $event)"
/>
<!-- Fieldset (single nested object) -->
<div v-else-if="schema.type === 'fieldset' || schema.type === 'accordion'" class="aksrt-field__fieldset">
<div v-else-if="schema.type === 'fieldset' || schema.type === 'accordion'" class="jio-field__fieldset">
<template v-if="fieldsetSections.length">
<div
v-for="(section, sIndex) in fieldsetSections"
:key="section.title"
class="aksrt-field__accordion-section"
class="jio-field__accordion-section"
:class="{ 'is-collapsed': !isFieldsetSectionOpen(sIndex) }"
>
<div class="aksrt-field__accordion-section-head" @click="toggleFieldsetSection(sIndex)">
<span class="aksrt-field__accordion-section-title">{{ section.title }}</span>
<div class="jio-field__accordion-section-head" @click="toggleFieldsetSection(sIndex)">
<span class="jio-field__accordion-section-title">{{ section.title }}</span>
<FaIcon
:icon="isFieldsetSectionOpen(sIndex) ? 'fa fas fa-chevron-down' : 'fa fas fa-chevron-right'"
:size="12"
class="aksrt-field__accordion-section-toggle"
class="jio-field__accordion-section-toggle"
/>
</div>
<div v-show="isFieldsetSectionOpen(sIndex)" class="aksrt-field__accordion-section-body">
<AksField
<div v-show="isFieldsetSectionOpen(sIndex)" class="jio-field__accordion-section-body">
<JioField
v-for="field in sectionFields(section)"
:key="field.key"
:schema="field"
@@ -362,7 +366,7 @@
</div>
</template>
<template v-if="!fieldsetSections.length">
<AksField
<JioField
v-for="field in visibleNestedFields"
:key="field.key"
:schema="field"
@@ -373,7 +377,7 @@
</template>
</div>
<div v-else-if="schema.type === 'link'" class="aksrt-field__link">
<div v-else-if="schema.type === 'link'" class="jio-field__link">
<n-input :value="modelValue?.url || ''" placeholder="https://" @update:value="updateLink('url', $event)" />
<n-select :value="modelValue?.target || ''" :options="linkTargetOptions" @update:value="updateLink('target', $event)" />
<n-input :value="modelValue?.text || ''" placeholder="链接文字(可选)" @update:value="updateLink('text', $event)" />
@@ -385,54 +389,54 @@
v-bind="SORTABLE_LIST_ATTRS"
item-key="key"
:disabled="schema.disabled"
class="aksrt-sortable"
class="jio-sortable"
>
<template #item="{ element: item, index }">
<div class="aksrt-sortable__item">
<div class="jio-sortable__item">
<div
class="aksrt-sortable__head"
class="jio-sortable__head"
v-bind="item.fields?.length ? listItemHeadAttrs(item.key) : {}"
>
<AksSortHandle @click.stop />
<span class="aksrt-sortable__index">{{ index + 1 }}</span>
<span class="aksrt-sortable__main">
<span class="aksrt-sortable__title">{{ item.label }}</span>
<JioSortHandle @click.stop />
<span class="jio-sortable__index">{{ index + 1 }}</span>
<span class="jio-sortable__main">
<span class="jio-sortable__title">{{ item.label }}</span>
</span>
<FaIcon
v-if="item.fields?.length"
:icon="isListItemOpen(item.key) ? 'fa fas fa-chevron-down' : 'fa fas fa-chevron-right'"
:size="12"
class="aksrt-sortable__toggle"
class="jio-sortable__toggle"
/>
</div>
<div v-if="item.fields?.length" v-show="isListItemOpen(item.key)" class="aksrt-sortable__body aksrt-field__sortable-body">
<AksField v-for="field in item.fields" :key="field.key" :schema="field" :model-value="modelValue?.[item.key]?.[field.key] ?? field.default" :form-data="formData" @update:model-value="updateSortable(item.key, field.key, $event)" />
<div v-if="item.fields?.length" v-show="isListItemOpen(item.key)" class="jio-sortable__body jio-field__sortable-body">
<JioField v-for="field in item.fields" :key="field.key" :schema="field" :model-value="modelValue?.[item.key]?.[field.key] ?? field.default" :form-data="formData" @update:model-value="updateSortable(item.key, field.key, $event)" />
</div>
</div>
</template>
</draggable>
<!-- Repeater (array of objects) — inline expand (default, draggable) -->
<div v-else-if="(schema.type === 'repeater' || schema.type === 'group') && schema.ui !== 'list-modal'" class="aksrt-field__repeater">
<div v-else-if="(schema.type === 'repeater' || schema.type === 'group') && schema.ui !== 'list-modal'" class="jio-field__repeater">
<draggable
v-if="repeaterDragList.length"
v-model="repeaterDragList"
v-bind="SORTABLE_LIST_ATTRS"
:item-key="repeaterDragKey"
:disabled="schema.disabled"
class="aksrt-sortable"
class="jio-sortable"
>
<template #item="{ element, index }">
<div class="aksrt-sortable__item">
<div class="aksrt-sortable__head" v-bind="repeaterHeadAttrs(repeaterDragKey(element))">
<AksSortHandle @click.stop />
<span class="aksrt-sortable__index">{{ index + 1 }}</span>
<span class="aksrt-sortable__main">
<span class="aksrt-sortable__title aksrt-field__repeater-summary">
<div class="jio-sortable__item">
<div class="jio-sortable__head" v-bind="repeaterHeadAttrs(repeaterDragKey(element))">
<JioSortHandle @click.stop />
<span class="jio-sortable__index">{{ index + 1 }}</span>
<span class="jio-sortable__main">
<span class="jio-sortable__title jio-field__repeater-summary">
{{ repeaterItemSummary(element) || '未填写' }}
</span>
</span>
<div class="aksrt-sortable__actions" @click.stop>
<div class="jio-sortable__actions" @click.stop>
<n-button
size="tiny"
type="error"
@@ -447,12 +451,12 @@
<FaIcon
:icon="isRepeaterItemOpen(repeaterDragKey(element)) ? 'fa fas fa-chevron-down' : 'fa fas fa-chevron-right'"
:size="12"
class="aksrt-sortable__toggle"
class="jio-sortable__toggle"
/>
</div>
<div v-show="isRepeaterItemOpen(repeaterDragKey(element))" class="aksrt-sortable__body aksrt-field__repeater-fields">
<div v-show="isRepeaterItemOpen(repeaterDragKey(element))" class="jio-sortable__body jio-field__repeater-fields">
<template v-for="field in (schema.fields || [])" :key="field.key">
<AksField
<JioField
v-if="matchDependency(field.dependsOn, element)"
:schema="field"
:model-value="element[field.key]"
@@ -465,7 +469,7 @@
</div>
</template>
</draggable>
<div v-else class="aksrt-sortable__empty">
<div v-else class="jio-sortable__empty">
<FaIcon icon="fa fas fa-inbox" :size="18" />
<span>{{ schema.emptyText || '暂无数据' }}</span>
</div>
@@ -482,7 +486,7 @@
</div>
<!-- Repeater list-modal: draggable card list + modal editor -->
<AksRepeaterListModal
<JioRepeaterListModal
v-else-if="(schema.type === 'repeater' || schema.type === 'group') && schema.ui === 'list-modal'"
:schema="schema"
:model-value="modelValue"
@@ -502,35 +506,35 @@
/>
<!-- Validation message -->
<p v-if="errorMessage" class="aksrt-field__error">{{ errorMessage }}</p>
<p v-if="errorMessage" class="jio-field__error">{{ errorMessage }}</p>
</div>
</template>
<script setup>
/**
* AksField — renders a single form field based on its schema definition.
* JioField — renders a single form field based on its schema definition.
*
* Supported types: switch, input/text, textarea, number, select, radio,
* checkbox, color, icon, media, gallery, slider, code, alert, fieldset, sortable,
* repeater (inline|list-modal), custom (slot).
*
* 所有可拖拽列表(sortable / 已选值排序 / repeater)共用 sortable-list.css +
* SORTABLE_LIST_ATTRS + AksSortHandle。
* SORTABLE_LIST_ATTRS + JioSortHandle。
*/
import { computed, ref } from 'vue'
import draggable from 'vuedraggable'
import FaIcon from './FaIcon.vue'
import FaIconPicker from './FaIconPicker.vue'
import AksMediaField from './AksMediaField.vue'
import AksGalleryField from './AksGalleryField.vue'
import AksCodeEditor from './AksCodeEditor.vue'
import AksCompositeField from './AksCompositeField.vue'
import AksColorPicker from './AksColorPicker.vue'
import AksImageSelectField from './AksImageSelectField.vue'
import AksSortHandle from './AksSortHandle.vue'
import AksSecretClear from './AksSecretClear.vue'
import AksSelectedOrder from './AksSelectedOrder.vue'
import AksRepeaterListModal from './AksRepeaterListModal.vue'
import JioMediaField from './JioMediaField.vue'
import JioGalleryField from './JioGalleryField.vue'
import JioCodeEditor from './JioCodeEditor.vue'
import JioCompositeField from './JioCompositeField.vue'
import JioColorPicker from './JioColorPicker.vue'
import JioImageSelectField from './JioImageSelectField.vue'
import JioSortHandle from './JioSortHandle.vue'
import JioSecretClear from './JioSecretClear.vue'
import JioSelectedOrder from './JioSelectedOrder.vue'
import JioRepeaterListModal from './JioRepeaterListModal.vue'
import { buildFieldsetSections } from './fieldset-sections.js'
import { matchDependency } from './field-dependency.js'
import { SORTABLE_LIST_ATTRS } from './sortable-options.js'
@@ -566,20 +570,39 @@ const {
} = useFieldRemoteOptions(props)
const fieldClasses = computed(() => ({
'aksrt-field--disabled': props.schema.disabled,
'aksrt-field--inline': props.schema.inline,
[`aksrt-field--${props.schema.type}`]: !!props.schema.type,
'jio-field--disabled': props.schema.disabled,
'jio-field--inline': props.schema.inline,
[`jio-field--${props.schema.type}`]: !!props.schema.type,
}))
const secretMarkedForClear = computed(() => props.schema.secret && props.modelValue === '__AKSRT_CLEAR_SECRET__')
/**
* 凭证字段不向浏览器暴露成密码输入。
*
* 设置页往往一次渲染二十多个 AppSecret/私钥,只要页面上出现 type="password",
* Chrome 就会把整页判定为登录表单,并把保存的账号密码灌进第一个文本框
* (顶部的设置项搜索框)。这里把凭证字段降级为 text 并用 CSS 的
* -webkit-text-security 做遮蔽:视觉上仍是密文,但浏览器不再识别成密码框。
*/
const secretMarkedForClear = computed(() => props.schema.secret && props.modelValue === '__JIO_CLEAR_SECRET__')
const secretDisplayValue = computed(() => secretMarkedForClear.value ? '' : props.modelValue)
/** 已配置密钥(服务端回传 '***'):textarea 不回显掩码,留空即保持原值。 */
const secretConfigured = computed(() => props.schema.secret && props.modelValue === '***')
const secretClearVisible = computed(() => secretConfigured.value || secretMarkedForClear.value)
const secretTextareaValue = computed(() => secretConfigured.value ? '' : secretDisplayValue.value)
/** 凭证字段的输入属性:彻底关掉自动填充与密码管理器的识别。 */
const secretInputProps = {
autocomplete: 'off',
autocorrect: 'off',
autocapitalize: 'off',
spellcheck: 'false',
'data-lpignore': 'true',
'data-1p-ignore': 'true',
'data-form-type': 'other',
}
function toggleSecretClear() {
emit('update:modelValue', secretMarkedForClear.value ? '***' : '__AKSRT_CLEAR_SECRET__')
emit('update:modelValue', secretMarkedForClear.value ? '***' : '__JIO_CLEAR_SECRET__')
}
/** Normalize options: support string[], {label, value}[], and dynamic formData-key references */
@@ -681,7 +704,7 @@ function updateSortable(itemKey, fieldKey, value) {
emit('update:modelValue', { ...current, [itemKey]: { ...(current[itemKey] || {}), [fieldKey]: value } })
}
// ============ Repeater(inline 展开;list-modal 见 AksRepeaterListModal) ============
// ============ Repeater(inline 展开;list-modal 见 JioRepeaterListModal) ============
const repeaterLength = computed(() => Array.isArray(props.modelValue) ? props.modelValue.length : 0)
const reachedRepeaterMinimum = computed(() => repeaterLength.value <= Number(props.schema.min || 0))
const reachedRepeaterMaximum = computed(() => Number(props.schema.max || 0) > 0 && repeaterLength.value >= Number(props.schema.max))
@@ -753,165 +776,165 @@ function updateRepeaterItem(index, fieldKey, value) {
</script>
<style scoped>
.aksrt-field {
.jio-field {
margin-bottom: 18px;
}
.aksrt-field:last-child {
.jio-field:last-child {
margin-bottom: 0;
}
/* 结构化分组(会员商品/续费商品/积分兑换/升级价格)与相邻字段之间留出更明确的间隔,
* 避免两个分组上下贴在一起,和参考主题的留白一致。 */
.aksrt-field--group,
.aksrt-field--repeater,
.aksrt-field--accordion,
.aksrt-field--fieldset {
.jio-field--group,
.jio-field--repeater,
.jio-field--accordion,
.jio-field--fieldset {
margin-bottom: 26px;
}
.aksrt-field__input-wrap {
.jio-field__input-wrap {
display: grid;
gap: 8px;
}
.aksrt-field__label-row {
.jio-field__label-row {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 6px;
}
.aksrt-field__label {
.jio-field__label {
font-size: 13px;
font-weight: 500;
color: var(--aksrt-color-text-secondary);
color: var(--jio-color-text-secondary);
}
.aksrt-field__required {
.jio-field__required {
color: #d03050;
margin-left: 2px;
}
.aksrt-field__help-icon {
color: var(--aksrt-color-border-strong);
.jio-field__help-icon {
color: var(--jio-color-border-strong);
cursor: help;
transition: color 0.15s;
}
.aksrt-field__help-icon:hover {
color: var(--aksrt-color-primary);
.jio-field__help-icon:hover {
color: var(--jio-color-primary);
}
.aksrt-field__desc {
.jio-field__desc {
margin: -2px 0 8px;
font-size: 12px;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
line-height: 1.5;
}
.aksrt-field__error {
.jio-field__error {
margin: 4px 0 0;
font-size: 12px;
color: #d03050;
}
.aksrt-field--disabled {
.jio-field--disabled {
opacity: 0.6;
pointer-events: none;
}
.aksrt-field__number {
.jio-field__number {
display: flex;
align-items: center;
gap: 8px;
max-width: 360px;
}
.aksrt-field__unit {
color: var(--aksrt-color-muted);
.jio-field__unit {
color: var(--jio-color-muted);
font-size: 13px;
white-space: nowrap;
}
.aksrt-field__palette {
.jio-field__palette {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.aksrt-field__palette-item {
.jio-field__palette-item {
width: 34px;
height: 26px;
padding: 0;
border: 3px solid var(--aksrt-color-surface);
border: 3px solid var(--jio-color-surface);
border-radius: 3px;
box-shadow: 0 0 0 1px var(--aksrt-color-border-strong);
box-shadow: 0 0 0 1px var(--jio-color-border-strong);
cursor: pointer;
}
.aksrt-field__palette-item.is-active {
box-shadow: 0 0 0 2px var(--aksrt-color-primary);
.jio-field__palette-item.is-active {
box-shadow: 0 0 0 2px var(--jio-color-primary);
}
.aksrt-field__link {
.jio-field__link {
display: grid;
grid-template-columns: minmax(220px, 1fr) 120px minmax(140px, .5fr);
gap: 8px;
}
/* sortable 卡片展开区:两列字段栅格(卡片本身见 styles/sortable-list.css) */
.aksrt-field__sortable-body {
.jio-field__sortable-body {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
/* Fieldset / Accordion */
.aksrt-field__fieldset {
.jio-field__fieldset {
padding: 14px;
border: 1px solid var(--aksrt-color-border);
border: 1px solid var(--jio-color-border);
border-radius: 8px;
background: var(--aksrt-color-canvas);
background: var(--jio-color-canvas);
}
.aksrt-field__accordion-section + .aksrt-field__accordion-section {
.jio-field__accordion-section + .jio-field__accordion-section {
margin-top: 14px;
}
.aksrt-field__accordion-section-head {
.jio-field__accordion-section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 10px;
border: 1px solid var(--aksrt-color-border);
border: 1px solid var(--jio-color-border);
border-radius: 6px;
background: var(--aksrt-color-surface);
background: var(--jio-color-surface);
cursor: pointer;
user-select: none;
font-size: 13px;
font-weight: 600;
color: var(--aksrt-color-text);
color: var(--jio-color-text);
transition: border-color 0.15s, color 0.15s;
}
.aksrt-field__accordion-section-head:hover {
border-color: var(--aksrt-color-primary);
background: var(--aksrt-color-primary-soft);
color: var(--aksrt-color-primary);
.jio-field__accordion-section-head:hover {
border-color: var(--jio-color-primary);
background: var(--jio-color-primary-soft);
color: var(--jio-color-primary);
}
.aksrt-field__accordion-section-title {
.jio-field__accordion-section-title {
min-width: 0;
}
.aksrt-field__accordion-section-toggle {
.jio-field__accordion-section-toggle {
flex-shrink: 0;
color: var(--aksrt-color-text-secondary);
color: var(--jio-color-text-secondary);
}
.aksrt-field__accordion-section-body {
.jio-field__accordion-section-body {
display: flex;
flex-direction: column;
gap: 8px;
@@ -919,58 +942,58 @@ function updateRepeaterItem(index, fieldKey, value) {
}
/* Repeater inline(卡片 / 手柄 / 拖拽态见 styles/sortable-list.css) */
.aksrt-field__repeater {
.jio-field__repeater {
display: flex;
flex-direction: column;
gap: 10px;
}
.aksrt-field__repeater-fields {
.jio-field__repeater-fields {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Content field */
.aksrt-field__content {
.jio-field__content {
font-size: 13px;
color: var(--aksrt-color-text-secondary);
color: var(--jio-color-text-secondary);
line-height: 1.6;
}
.aksrt-field__content :deep(p) {
.jio-field__content :deep(p) {
margin: 0;
}
.aksrt-field__content :deep(p + ul) {
.jio-field__content :deep(p + ul) {
margin-top: 6px;
}
.aksrt-field__content :deep(ul) {
.jio-field__content :deep(ul) {
margin: 0;
padding-left: 18px;
line-height: 1.9;
}
/* 说明文字的小号灰色段落与警示文字(schema content 里用 class 引用) */
.aksrt-field__content :deep(.aksrt-hint) {
color: var(--aksrt-color-muted);
.jio-field__content :deep(.jio-hint) {
color: var(--jio-color-muted);
font-size: 12px;
line-height: 1.7;
}
.aksrt-field__content :deep(.aksrt-hint + .aksrt-hint) {
.jio-field__content :deep(.jio-hint + .jio-hint) {
margin-top: 6px;
}
.aksrt-field__content :deep(.aksrt-warn) {
color: var(--aksrt-color-warning, #d97706);
.jio-field__content :deep(.jio-warn) {
color: var(--jio-color-warning, #d97706);
}
/* Mobile */
@media (max-width: 768px) {
.aksrt-field__link,
.aksrt-field__sortable-body {
.jio-field__link,
.jio-field__sortable-body {
grid-template-columns: 1fr;
}
}
@@ -1,26 +1,26 @@
<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
v-if="title"
class="aksrt-field-group__header"
class="jio-field-group__header"
:class="{ 'is-clickable': collapsible }"
@click="collapsible ? toggleCollapse() : undefined"
>
<div class="aksrt-field-group__header-left">
<FaIcon v-if="icon" :icon="icon" :size="16" class="aksrt-field-group__icon" />
<div class="jio-field-group__header-left">
<FaIcon v-if="icon" :icon="icon" :size="16" class="jio-field-group__icon" />
<div>
<h3 class="aksrt-field-group__title">{{ title }}</h3>
<p v-if="description" class="aksrt-field-group__desc">{{ description }}</p>
<h3 class="jio-field-group__title">{{ title }}</h3>
<p v-if="description" class="jio-field-group__desc">{{ description }}</p>
</div>
</div>
<div class="aksrt-field-group__header-right">
<div v-if="$slots.actions" class="aksrt-field-group__actions" @click.stop>
<div class="jio-field-group__header-right">
<div v-if="$slots.actions" class="jio-field-group__actions" @click.stop>
<slot name="actions" />
</div>
<button
v-if="collapsible"
type="button"
class="aksrt-field-group__collapse-btn"
class="jio-field-group__collapse-btn"
:aria-expanded="(!collapsed).toString()"
:title="collapsed ? '展开配置' : '收起配置'"
@click.stop="toggleCollapse"
@@ -34,8 +34,8 @@
</div>
</div>
<transition name="aksrt-collapse">
<div v-show="!collapsed" class="aksrt-field-group__body">
<transition name="jio-collapse">
<div v-show="!collapsed" class="jio-field-group__body">
<slot />
</div>
</transition>
@@ -44,7 +44,7 @@
<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.
*
* defaultCollapsed only seeds the initial state; afterwards the user toggles freely.
@@ -69,16 +69,16 @@ function toggleCollapse() {
</script>
<style scoped>
.aksrt-field-group {
.jio-field-group {
margin-bottom: 20px;
border: 1px solid var(--aksrt-color-border);
border: 1px solid var(--jio-color-border);
border-radius: 12px;
background: var(--aksrt-color-surface);
box-shadow: var(--aksrt-shadow-card);
background: var(--jio-color-surface);
box-shadow: var(--jio-shadow-card);
overflow: hidden;
}
.aksrt-field-group__header {
.jio-field-group__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
@@ -86,66 +86,66 @@ function toggleCollapse() {
gap: 12px;
}
.aksrt-field-group__header.is-clickable {
.jio-field-group__header.is-clickable {
cursor: pointer;
user-select: none;
}
.aksrt-field-group__header.is-clickable:hover .aksrt-field-group__title {
color: var(--aksrt-color-primary);
.jio-field-group__header.is-clickable:hover .jio-field-group__title {
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;
}
.aksrt-field-group__header-left {
.jio-field-group__header-left {
display: flex;
align-items: flex-start;
gap: 10px;
min-width: 0;
}
.aksrt-field-group__header-right {
.jio-field-group__header-right {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.aksrt-field-group__icon {
color: var(--aksrt-color-primary);
.jio-field-group__icon {
color: var(--jio-color-primary);
margin-top: 2px;
}
.aksrt-field-group__title {
.jio-field-group__title {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--aksrt-color-text);
color: var(--jio-color-text);
transition: color 0.15s;
}
.aksrt-field-group__desc {
.jio-field-group__desc {
margin: 4px 0 0;
font-size: 12px;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
line-height: 1.5;
}
.aksrt-field-group__actions {
.jio-field-group__actions {
display: flex;
align-items: center;
gap: 8px;
}
.aksrt-field-group__collapse-btn {
.jio-field-group__collapse-btn {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid var(--aksrt-color-border);
background: var(--aksrt-color-canvas);
color: var(--aksrt-color-text-secondary);
border: 1px solid var(--jio-color-border);
background: var(--jio-color-canvas);
color: var(--jio-color-text-secondary);
border-radius: 999px;
padding: 4px 10px;
font-size: 12px;
@@ -153,25 +153,25 @@ function toggleCollapse() {
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.aksrt-field-group__collapse-btn:hover {
color: var(--aksrt-color-primary);
border-color: var(--aksrt-color-primary);
background: var(--aksrt-color-primary-soft);
.jio-field-group__collapse-btn:hover {
color: var(--jio-color-primary);
border-color: var(--jio-color-primary);
background: var(--jio-color-primary-soft);
}
.aksrt-field-group__body {
.jio-field-group__body {
padding: 16px 20px 20px;
}
/* Collapse transition */
.aksrt-collapse-enter-active,
.aksrt-collapse-leave-active {
.jio-collapse-enter-active,
.jio-collapse-leave-active {
transition: all 0.2s ease;
overflow: hidden;
}
.aksrt-collapse-enter-from,
.aksrt-collapse-leave-to {
.jio-collapse-enter-from,
.jio-collapse-leave-to {
opacity: 0;
max-height: 0;
padding-top: 0;
@@ -1,20 +1,22 @@
<template>
<div class="aksrt-form-renderer">
<div class="jio-form-renderer">
<!-- Page header with title and description -->
<div v-if="title" class="aksrt-form-renderer__header">
<div class="aksrt-form-renderer__header-left">
<FaIcon v-if="icon" :icon="icon" :size="22" class="aksrt-form-renderer__header-icon" />
<div v-if="title" class="jio-form-renderer__header">
<div class="jio-form-renderer__header-left">
<FaIcon v-if="icon" :icon="icon" :size="22" class="jio-form-renderer__header-icon" />
<div>
<h1 class="aksrt-form-renderer__title">{{ title }}</h1>
<p v-if="description" class="aksrt-form-renderer__desc">{{ description }}</p>
<h1 class="jio-form-renderer__title">{{ title }}</h1>
<p v-if="description" class="jio-form-renderer__desc">{{ description }}</p>
</div>
</div>
<div v-if="showSearch" class="aksrt-form-renderer__search">
<div v-if="showSearch" class="jio-form-renderer__search">
<n-input
v-model:value="searchQuery"
placeholder="搜索设置项..."
size="small"
clearable
name="jio-settings-search"
:input-props="searchInputProps"
>
<template #prefix>
<FaIcon icon="fa fas fa-search" :size="12" />
@@ -24,7 +26,7 @@
</div>
<!-- Loading state -->
<div v-if="loading" class="aksrt-form-renderer__loading">
<div v-if="loading" class="jio-form-renderer__loading">
<n-spin size="medium" />
<span>加载中...</span>
</div>
@@ -45,7 +47,7 @@
<!-- Form content -->
<template v-else-if="schema && schema.length">
<template v-for="group in filteredSchema" :key="groupKey(group)">
<AksFieldGroup
<JioFieldGroup
v-if="isGroupVisible(group)"
:title="group.title"
:description="group.description"
@@ -59,7 +61,7 @@
<!-- Tabbed: renders tabs whose sub-fields are flat top-level keys -->
<div
v-if="field.type === 'tabbed' && (field.tabs || []).length"
class="aksrt-form-renderer__tabbed"
class="jio-form-renderer__tabbed"
>
<n-tabs type="line" animated>
<n-tab-pane
@@ -69,7 +71,7 @@
:tab="tabName(tab)"
>
<template v-for="sub in (tab.fields || [])" :key="sub.key">
<AksField
<JioField
v-if="isFieldVisible(sub)"
:schema="sub"
:model-value="getFieldValue(sub.key)"
@@ -82,7 +84,7 @@
</n-tab-pane>
</n-tabs>
</div>
<AksSorterField
<JioSorterField
v-else-if="field.type === 'custom' && field.renderer === 'sorter'"
:model-value="getFieldValue(field.key) || {}"
:options="field.options || []"
@@ -93,7 +95,7 @@
:name="`field-${field.key}`"
v-bind="{ field, value: getFieldValue(field.key), setValue: (value) => setFieldValue(field.key, value), formData }"
>
<AksField
<JioField
:schema="field"
:model-value="getFieldValue(field.key)"
:form-data="formData"
@@ -102,7 +104,7 @@
@action="emit('field-action', $event)"
/>
</slot>
<AksField
<JioField
v-else
:schema="field"
:model-value="getFieldValue(field.key)"
@@ -117,24 +119,24 @@
<!-- Custom group content slot -->
<slot :name="`group-${group.key}`" v-bind="{ group, formData }" />
</AksFieldGroup>
</JioFieldGroup>
</template>
</template>
<!-- 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" />
<p>暂无配置项</p>
</div>
<!-- Sticky save bar -->
<AksSaveBar :dirty="dirty" :saving="saving" @save="$emit('save')" @reload="$emit('reload')" />
<JioSaveBar :dirty="dirty" :saving="saving" @save="$emit('save')" @reload="$emit('reload')" />
</div>
</template>
<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,
* and renders a complete settings page with:
@@ -146,10 +148,10 @@
*/
import { ref, computed, useSlots } from 'vue'
import FaIcon from './FaIcon.vue'
import AksSaveBar from './AksSaveBar.vue'
import AksFieldGroup from './AksFieldGroup.vue'
import AksField from './AksField.vue'
import AksSorterField from './AksSorterField.vue'
import JioSaveBar from './JioSaveBar.vue'
import JioFieldGroup from './JioFieldGroup.vue'
import JioField from './JioField.vue'
import JioSorterField from './JioSorterField.vue'
const props = defineProps({
/** Page title */
@@ -185,6 +187,18 @@ const slots = useSlots()
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 */
function matchDependsOn(dependsOn) {
if (!dependsOn) return true
@@ -308,7 +322,7 @@ const filteredSchema = computed(() => {
</script>
<style scoped>
.aksrt-form-renderer__header {
.jio-form-renderer__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
@@ -317,73 +331,73 @@ const filteredSchema = computed(() => {
flex-wrap: wrap;
}
.aksrt-form-renderer__header-left {
.jio-form-renderer__header-left {
display: flex;
align-items: flex-start;
gap: 12px;
}
.aksrt-form-renderer__header-icon {
color: var(--aksrt-color-primary);
.jio-form-renderer__header-icon {
color: var(--jio-color-primary);
margin-top: 4px;
}
.aksrt-form-renderer__title {
.jio-form-renderer__title {
margin: 0;
font-size: 22px;
font-weight: 700;
color: var(--aksrt-color-text);
color: var(--jio-color-text);
letter-spacing: -0.02em;
}
.aksrt-form-renderer__desc {
.jio-form-renderer__desc {
margin: 4px 0 0;
font-size: 13px;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
line-height: 1.6;
}
.aksrt-form-renderer__tabbed {
.jio-form-renderer__tabbed {
margin-bottom: 8px;
}
.aksrt-form-renderer__search {
.jio-form-renderer__search {
min-width: 200px;
max-width: 280px;
}
.aksrt-form-renderer__loading {
.jio-form-renderer__loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 60px 0;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
font-size: 14px;
}
.aksrt-form-renderer__empty {
.jio-form-renderer__empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
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;
font-size: 14px;
}
/* Mobile */
@media (max-width: 768px) {
.aksrt-form-renderer__header {
.jio-form-renderer__header {
flex-direction: column;
gap: 10px;
}
.aksrt-form-renderer__search {
.jio-form-renderer__search {
max-width: 100%;
width: 100%;
}
@@ -1,23 +1,23 @@
<template>
<div class="aksrt-gallery-field">
<div v-if="attachmentIds.length" class="aksrt-gallery-field__grid">
<div class="jio-gallery-field">
<div v-if="attachmentIds.length" class="jio-gallery-field__grid">
<article
v-for="(attachmentId, index) in attachmentIds"
:key="attachmentId"
class="aksrt-gallery-field__item"
class="jio-gallery-field__item"
>
<img
v-if="previews[attachmentId]?.url"
:src="previews[attachmentId].url"
: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" />
<span>#{{ attachmentId }}</span>
</div>
<div class="aksrt-gallery-field__actions">
<div class="jio-gallery-field__actions">
<n-button
size="tiny"
quaternary
@@ -49,12 +49,12 @@
</article>
</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" />
<span>尚未选择图片</span>
</div>
<div class="aksrt-gallery-field__buttons">
<div class="jio-gallery-field__buttons">
<n-button type="primary" secondary @click="openMediaLibrary">
<FaIcon icon="fa fas fa-plus" />
{{ attachmentIds.length ? '编辑图片' : '选择图片' }}
@@ -64,7 +64,7 @@
</n-button>
</div>
<p v-if="mediaError" class="aksrt-gallery-field__error">{{ mediaError }}</p>
<p v-if="mediaError" class="jio-gallery-field__error">{{ mediaError }}</p>
</div>
</template>
@@ -172,25 +172,25 @@ watch(attachmentIds, hydratePreviews, { immediate: true })
</script>
<style scoped>
.aksrt-gallery-field__grid {
.jio-gallery-field__grid {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: 8px;
}
.aksrt-gallery-field__item {
.jio-gallery-field__item {
display: inline-flex;
flex-direction: column;
max-width: 100%;
padding: 4px 4px 0;
overflow: hidden;
border: 1px solid var(--aksrt-color-border);
border: 1px solid var(--jio-color-border);
border-radius: 8px;
background: var(--aksrt-color-canvas);
background: var(--jio-color-canvas);
}
.aksrt-gallery-field__image {
.jio-gallery-field__image {
display: block;
width: auto;
height: auto;
@@ -199,7 +199,7 @@ watch(attachmentIds, hydratePreviews, { immediate: true })
border-radius: 4px;
}
.aksrt-gallery-field__placeholder {
.jio-gallery-field__placeholder {
display: flex;
flex-direction: column;
width: 90px;
@@ -207,35 +207,35 @@ watch(attachmentIds, hydratePreviews, { immediate: true })
align-items: center;
justify-content: center;
gap: 4px;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
}
.aksrt-gallery-field__actions {
.jio-gallery-field__actions {
display: flex;
justify-content: center;
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;
min-height: 116px;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 8px;
border: 1px dashed var(--aksrt-color-border-strong);
border: 1px dashed var(--jio-color-border-strong);
border-radius: 8px;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
}
.aksrt-gallery-field__buttons {
.jio-gallery-field__buttons {
display: flex;
gap: 8px;
margin-top: 10px;
}
.aksrt-gallery-field__error {
.jio-gallery-field__error {
margin: 8px 0 0;
color: #d03050;
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>
@@ -1,17 +1,17 @@
<template>
<div class="aksrt-media-field">
<div v-if="modelValue" class="aksrt-media-field__preview">
<div class="jio-media-field">
<div v-if="modelValue" class="jio-media-field__preview">
<img
v-if="isImage"
:src="modelValue"
: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" />
<span class="aksrt-media-field__filename">{{ fileName }}</span>
<span class="jio-media-field__filename">{{ fileName }}</span>
</div>
<div class="aksrt-media-field__overlay">
<div class="jio-media-field__overlay">
<n-button size="tiny" secondary circle title="更换" @click="openMediaLibrary">
<FaIcon icon="fa fas fa-exchange-alt" />
</n-button>
@@ -21,7 +21,7 @@
</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" />
<span>{{ placeholder }}</span>
</div>
@@ -30,7 +30,7 @@
:value="modelValue"
:placeholder="'或直接输入 URL'"
size="small"
class="aksrt-media-field__url"
class="jio-media-field__url"
@update:value="emit('update:modelValue', $event)"
/>
</div>
@@ -38,7 +38,7 @@
<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.
* For now, it supports direct URL input with preview.
@@ -95,7 +95,7 @@ function clearMedia() {
</script>
<style scoped>
.aksrt-media-field__preview {
.jio-media-field__preview {
position: relative;
display: inline-flex;
max-width: 100%;
@@ -104,13 +104,13 @@ function clearMedia() {
padding: 4px;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--aksrt-color-border);
background: var(--aksrt-color-canvas);
border: 1px solid var(--jio-color-border);
background: var(--jio-color-canvas);
margin-bottom: 8px;
vertical-align: top;
}
.aksrt-media-field__image {
.jio-media-field__image {
display: block;
width: auto;
height: auto;
@@ -119,21 +119,21 @@ function clearMedia() {
border-radius: 4px;
}
.aksrt-media-field__file {
.jio-media-field__file {
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
background: var(--aksrt-color-canvas);
background: var(--jio-color-canvas);
}
.aksrt-media-field__filename {
.jio-media-field__filename {
font-size: 13px;
color: var(--aksrt-color-text-secondary);
color: var(--jio-color-text-secondary);
word-break: break-all;
}
.aksrt-media-field__overlay {
.jio-media-field__overlay {
position: absolute;
top: 0;
right: 0;
@@ -144,33 +144,33 @@ function clearMedia() {
transition: opacity 0.2s;
}
.aksrt-media-field__preview:hover .aksrt-media-field__overlay {
.jio-media-field__preview:hover .jio-media-field__overlay {
opacity: 1;
}
.aksrt-media-field__empty {
.jio-media-field__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 32px;
border: 2px dashed var(--aksrt-color-border-strong);
border: 2px dashed var(--jio-color-border-strong);
border-radius: 8px;
cursor: pointer;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
font-size: 13px;
transition: all 0.2s;
margin-bottom: 8px;
}
.aksrt-media-field__empty:hover {
border-color: var(--aksrt-color-primary);
color: var(--aksrt-color-primary);
background: var(--aksrt-color-primary-soft);
.jio-media-field__empty:hover {
border-color: var(--jio-color-primary);
color: var(--jio-color-primary);
background: var(--jio-color-primary-soft);
}
.aksrt-media-field__url {
.jio-media-field__url {
margin-top: 4px;
}
</style>
@@ -1,27 +1,27 @@
<template>
<div class="aksrt-repeater-list">
<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="aksrt-sortable"
class="jio-sortable"
>
<template #item="{ element, index }">
<div class="aksrt-sortable__item">
<div class="aksrt-sortable__head">
<AksSortHandle />
<span class="aksrt-sortable__icon" :class="preview(element).iconClass">
<span v-if="preview(element).customIcon" class="aksrt-repeater-list__icon-html" v-html="preview(element).customIcon" />
<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="aksrt-sortable__main">
<span class="aksrt-sortable__title">{{ preview(element).title }}</span>
<span class="aksrt-sortable__desc">{{ preview(element).desc }}</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="aksrt-sortable__actions">
<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>
编辑
@@ -35,7 +35,7 @@
</div>
</template>
</draggable>
<div v-else class="aksrt-sortable__empty">
<div v-else class="jio-sortable__empty">
<FaIcon icon="fa fas fa-inbox" :size="18" />
<span>{{ schema.emptyText || '暂无数据' }}</span>
</div>
@@ -50,11 +50,11 @@
:mask-closable="false"
preset="card"
:title="editingIndex >= 0 ? (schema.editText || '编辑项目') : (schema.addText || '添加项目')"
class="aksrt-modal-card"
class="jio-modal-card"
style="max-width: 640px; width: 92vw;"
>
<div class="aksrt-repeater-list__modal-fields">
<AksField
<div class="jio-repeater-list__modal-fields">
<JioField
v-for="field in draftFields"
:key="field.key"
:schema="field"
@@ -63,7 +63,7 @@
@update:model-value="draft = { ...draft, [field.key]: $event }"
/>
</div>
<div class="aksrt-modal-footer">
<div class="jio-modal-footer">
<n-button @click="closeModal">取消</n-button>
<n-button type="primary" @click="confirmModal">确定</n-button>
</div>
@@ -76,8 +76,8 @@
import { computed, ref } from 'vue'
import draggable from 'vuedraggable'
import FaIcon from './FaIcon.vue'
import AksField from './AksField.vue'
import AksSortHandle from './AksSortHandle.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'
@@ -151,13 +151,13 @@ function confirmModal() {
</script>
<style scoped>
.aksrt-repeater-list {
.jio-repeater-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.aksrt-repeater-list__icon-html {
.jio-repeater-list__icon-html {
display: inline-flex;
align-items: center;
justify-content: center;
@@ -165,14 +165,14 @@ function confirmModal() {
height: 100%;
}
.aksrt-repeater-list__icon-html :deep(svg),
.aksrt-repeater-list__icon-html :deep(img) {
.jio-repeater-list__icon-html :deep(svg),
.jio-repeater-list__icon-html :deep(img) {
width: 20px;
height: 20px;
object-fit: contain;
}
.aksrt-repeater-list__modal-fields {
.jio-repeater-list__modal-fields {
display: flex;
flex-direction: column;
gap: 4px;
@@ -1,11 +1,11 @@
<template>
<div v-if="dirty" class="aksrt-form-renderer__save-bar">
<div class="aksrt-form-renderer__save-bar__inner">
<div class="aksrt-form-renderer__save-bar__info">
<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="aksrt-form-renderer__save-bar__actions">
<div class="jio-form-renderer__save-bar__actions">
<n-button size="small" :disabled="saving" @click="emit('reload')">放弃更改</n-button>
<n-button
size="small"
@@ -23,8 +23,8 @@
<script setup>
/**
* AksSaveBar — the admin-wide sticky save bar (shown only when dirty).
* Shared by AksFormRenderer (schema pages) and custom settings pages.
* 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'
@@ -37,19 +37,19 @@ const emit = defineEmits(['save', 'reload'])
</script>
<style scoped>
.aksrt-form-renderer__save-bar {
.jio-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);
background: var(--jio-color-surface);
border-top: 1px solid var(--jio-color-border);
box-shadow: var(--jio-shadow-floating);
animation: slideUp 0.2s ease;
}
.aksrt-form-renderer__save-bar__inner {
.jio-form-renderer__save-bar__inner {
display: flex;
align-items: center;
justify-content: space-between;
@@ -59,7 +59,7 @@ const emit = defineEmits(['save', 'reload'])
gap: 16px;
}
.aksrt-form-renderer__save-bar__info {
.jio-form-renderer__save-bar__info {
display: flex;
align-items: center;
gap: 8px;
@@ -67,7 +67,7 @@ const emit = defineEmits(['save', 'reload'])
font-size: 13px;
}
.aksrt-form-renderer__save-bar__actions {
.jio-form-renderer__save-bar__actions {
display: flex;
gap: 8px;
}
@@ -84,17 +84,17 @@ const emit = defineEmits(['save', 'reload'])
}
@media (max-width: 768px) {
.aksrt-form-renderer__save-bar__inner {
.jio-form-renderer__save-bar__inner {
flex-direction: column;
padding: 10px 14px;
gap: 8px;
}
.aksrt-form-renderer__save-bar__actions {
.jio-form-renderer__save-bar__actions {
width: 100%;
}
.aksrt-form-renderer__save-bar__actions .n-button {
.jio-form-renderer__save-bar__actions .n-button {
flex: 1;
}
}
@@ -1,9 +1,9 @@
<template>
<div v-if="visible" class="aksrt-secret-clear">
<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="aksrt-secret-clear__hint">保存后将删除此密钥</span>
<span v-if="marked" class="jio-secret-clear__hint">保存后将删除此密钥</span>
</div>
</template>
@@ -22,15 +22,15 @@ defineEmits(['toggle'])
</script>
<style scoped>
.aksrt-secret-clear {
.jio-secret-clear {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.aksrt-secret-clear__hint {
color: var(--aksrt-color-warning, #d97706);
.jio-secret-clear__hint {
color: var(--jio-color-warning, #d97706);
font-size: 12px;
}
</style>
@@ -4,14 +4,14 @@
v-bind="SORTABLE_LIST_ATTRS"
:item-key="valueKey"
:disabled="disabled"
class="aksrt-sortable aksrt-sortable--compact aksrt-selected-order"
class="jio-sortable jio-sortable--compact jio-selected-order"
>
<template #item="{ element, index }">
<div class="aksrt-sortable__item">
<div class="aksrt-sortable__head">
<AksSortHandle />
<span class="aksrt-sortable__index">{{ index + 1 }}</span>
<span class="aksrt-sortable__title">{{ labelOf(element) }}</span>
<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>
@@ -22,7 +22,7 @@
/** 多选 + sortable 的已选值排序列表(select / async_select 下方)。 */
import { computed } from 'vue'
import draggable from 'vuedraggable'
import AksSortHandle from './AksSortHandle.vue'
import JioSortHandle from './JioSortHandle.vue'
import { SORTABLE_LIST_ATTRS } from './sortable-options.js'
const props = defineProps({
@@ -47,7 +47,7 @@ function labelOf(value) {
</script>
<style scoped>
.aksrt-selected-order {
.jio-selected-order {
margin-top: 8px;
}
</style>
@@ -1,5 +1,5 @@
<template>
<button type="button" class="aksrt-sortable__handle" :title="label" :aria-label="label">
<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>
@@ -1,12 +1,12 @@
<template>
<div class="aksrt-sorter">
<div class="jio-sorter">
<div
v-for="column in COLUMNS"
:key="column.key"
class="aksrt-sorter__column"
:class="`aksrt-sorter__column--${column.key}`"
class="jio-sorter__column"
:class="`jio-sorter__column--${column.key}`"
>
<div class="aksrt-sorter__title">
<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>
@@ -14,21 +14,21 @@
v-model="columns[column.key]"
v-bind="SORTABLE_LIST_ATTRS"
item-key="value"
group="aksrt-sorter"
class="aksrt-sortable aksrt-sortable--compact aksrt-sorter__list"
group="jio-sorter"
class="jio-sortable jio-sortable--compact jio-sorter__list"
@change="commit"
>
<template #item="{ element }">
<div class="aksrt-sortable__item">
<div class="aksrt-sortable__head">
<AksSortHandle />
<i v-if="element.icon" :class="element.icon" class="aksrt-sorter__icon" aria-hidden="true"></i>
<span class="aksrt-sortable__title">{{ element.label }}</span>
<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="aksrt-sortable__empty aksrt-sorter__empty">
<div v-if="columns[column.key].length === 0" class="jio-sortable__empty jio-sorter__empty">
{{ column.empty }}
</div>
</div>
@@ -39,7 +39,7 @@
/** 启用 / 未启用两列拖拽排序器;样式见 styles/sortable-list.css。 */
import { reactive, watch } from 'vue'
import draggable from 'vuedraggable'
import AksSortHandle from './AksSortHandle.vue'
import JioSortHandle from './JioSortHandle.vue'
import { SORTABLE_LIST_ATTRS } from './sortable-options.js'
const COLUMNS = [
@@ -89,37 +89,37 @@ watch(() => [props.modelValue, props.options], syncColumns, { immediate: true, d
</script>
<style scoped>
.aksrt-sorter {
.jio-sorter {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.aksrt-sorter__column {
.jio-sorter__column {
position: relative;
min-height: 210px;
padding: 12px;
border: 1px solid var(--aksrt-color-border);
border: 1px solid var(--jio-color-border);
border-radius: 8px;
background: var(--aksrt-color-canvas);
background: var(--jio-color-canvas);
}
.aksrt-sorter__title {
.jio-sorter__title {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
color: var(--aksrt-color-text);
color: var(--jio-color-text);
font-size: 13px;
font-weight: 600;
}
/* 空列也要留出可放置区域,拖拽目标不能塌成 0 高度。 */
.aksrt-sorter__list {
.jio-sorter__list {
min-height: 44px;
}
.aksrt-sorter__empty {
.jio-sorter__empty {
position: absolute;
right: 12px;
bottom: 12px;
@@ -128,15 +128,15 @@ watch(() => [props.modelValue, props.options], syncColumns, { immediate: true, d
pointer-events: none;
}
.aksrt-sorter__icon {
.jio-sorter__icon {
flex: 0 0 auto;
width: 16px;
color: var(--aksrt-color-primary);
color: var(--jio-color-primary);
text-align: center;
}
@media (max-width: 720px) {
.aksrt-sorter {
.jio-sorter {
grid-template-columns: 1fr;
}
}
@@ -1,15 +1,15 @@
/**
* 所有后台 vuedraggable 列表共用的 Sortable 选项(v-bind 到 <draggable>)。
* 配套样式见 src/styles/sortable-list.css,拖拽手柄统一用 AksSortHandle。
* 配套样式见 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: '.aksrt-sortable__handle',
ghostClass: 'aksrt-sortable__ghost',
chosenClass: 'aksrt-sortable__chosen',
dragClass: 'aksrt-sortable__drag',
handle: '.jio-sortable__handle',
ghostClass: 'jio-sortable__ghost',
chosenClass: 'jio-sortable__chosen',
dragClass: 'jio-sortable__drag',
animation: prefersReducedMotion ? 0 : 180,
})
@@ -2,7 +2,7 @@ import { onMounted, ref } from 'vue'
import api from '@/api'
/**
* AksField 的远程选项加载:async_select / taxonomy_select / user_select / page_select。
* JioField 的远程选项加载:async_select / taxonomy_select / user_select / page_select。
* 仅按 schema.type 在挂载时拉取对应数据源。
*/
export function useFieldRemoteOptions(props) {
@@ -47,7 +47,7 @@ export function useFieldRemoteOptions(props) {
value: t.term_id,
}))
} catch (e) {
console.warn('[AksField] taxonomy load failed:', e)
console.warn('[JioField] taxonomy load failed:', e)
taxonomyOptions.value = []
} finally {
taxonomyLoading.value = false
@@ -64,7 +64,7 @@ export function useFieldRemoteOptions(props) {
value: u.id,
}))
} catch (e) {
console.warn('[AksField] user load failed:', e)
console.warn('[JioField] user load failed:', e)
userOptions.value = []
} finally {
userLoading.value = false
@@ -84,7 +84,7 @@ export function useFieldRemoteOptions(props) {
const items = res?.data || res || []
pageOptions.value = Array.isArray(items) ? items : []
} catch (e) {
console.warn('[AksField] page load failed:', e)
console.warn('[JioField] page load failed:', e)
pageOptions.value = []
} finally {
pageLoading.value = false
+11 -11
View File
@@ -14,35 +14,35 @@ app.use(NaiveUI)
// 全局错误处理
app.config.errorHandler = (err, vm, info) => {
console.error('[AKSRT] 全局错误:', err, info)
console.error('[Jio] 全局错误:', err, info)
}
// 兜底:立即显示加载状态(防止白屏)
function showFallback(el) {
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>'
+ '</div>'
}
showFallback(document.getElementById('aksrt-admin-app') || document.body.appendChild(
Object.assign(document.createElement('div'), { id: 'aksrt-admin-app' })
showFallback(document.getElementById('jio-admin-app') || document.body.appendChild(
Object.assign(document.createElement('div'), { id: 'jio-admin-app' })
))
/**
* 获取或创建一个干净的挂载容器
* 核心问题:WordPress 后台的 jQuery/脚本会在页面加载后持续操作 DOM,
* 导致 PHP 输出的 <div id="aksrt-admin-app"> 可能被外部修改或其兄弟节点被移除,
* 导致 PHP 输出的 <div id="jio-admin-app"> 可能被外部修改或其兄弟节点被移除,
* Vue patch 时遍历到 null 节点 → nextSibling 报错。
*
* 解决方案:在挂载前一刻,动态创建一个全新的容器元素替换旧的,
* 确保该节点及其父级在 Vue 接管前不被任何外部脚本污染。
*/
function getCleanContainer() {
const existing = document.getElementById('aksrt-admin-app')
const existing = document.getElementById('jio-admin-app')
if (!existing) {
// 极端情况:PHP 没有输出容器,手动创建
const fresh = document.createElement('div')
fresh.id = 'aksrt-admin-app'
fresh.id = 'jio-admin-app'
document.body.appendChild(fresh)
return fresh
}
@@ -53,13 +53,13 @@ function getCleanContainer() {
if (!parent) {
// 旧容器已脱离文档树,重新创建
const fresh = document.createElement('div')
fresh.id = 'aksrt-admin-app'
fresh.id = 'jio-admin-app'
document.body.appendChild(fresh)
return fresh
}
const fresh = document.createElement('div')
fresh.id = 'aksrt-admin-app'
fresh.id = 'jio-admin-app'
// 将旧容器的位置信息转移给新容器(保留 fallback 内容)
fresh.innerHTML = existing.innerHTML || ''
@@ -91,12 +91,12 @@ router.isReady().then(() => {
styleMountTarget.value = container
app.mount(container)
} catch (err) {
console.error('[AKSRT] Vue 挂载失败:', err)
console.error('[Jio] Vue 挂载失败:', err)
showError(getCleanContainer(), err)
}
})
})
})
}).catch((err) => {
console.error('[AKSRT] Router 初始化失败:', err)
console.error('[Jio] Router 初始化失败:', err)
})
+22 -122
View File
@@ -1,20 +1,20 @@
<template>
<div class="aksrt-page-container">
<h2 class="aksrt-page-title">积分配置</h2>
<p class="aksrt-page-desc">
<div class="jio-page-container">
<h2 class="jio-page-title">积分配置</h2>
<p class="jio-page-desc">
配置积分规则与发放策略。可自定义货币名称(如”金币”、”魔石”等)。用户余额与手工调整在 WordPress 原生”用户”菜单中管理。
</p>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>主开关 + 基础设置</h3></div>
<n-form :model="cfg" label-placement="left" label-width="200" class="aksrt-form">
<n-card class="jio-section-card">
<div class="jio-section-header"><h3>主开关 + 基础设置</h3></div>
<n-form :model="cfg" label-placement="left" label-width="200" class="jio-form">
<n-form-item label="启用积分系统">
<n-switch v-model:value="cfg.enabled" />
</n-form-item>
<n-form-item label="自定义货币名称">
<n-input v-model:value="cfg.currency_name" placeholder="积分" maxlength="20" />
<template #feedback>
<span class="aksrt-hint-text">自定义货币显示名称,如"金币"、"魔石"等。留空则使用默认名称"积分"。</span>
<span class="jio-hint-text">自定义货币显示名称,如"金币"、"魔石"等。留空则使用默认名称"积分"。</span>
</template>
</n-form-item>
<n-form-item label="注册赠送积分">
@@ -23,36 +23,9 @@
</n-form>
</n-card>
<n-card title="用户近期资产记录" class="aksrt-section-card">
<n-space vertical :size="12">
<n-space wrap>
<n-input v-model:value="ledgerFilters.user_id" placeholder="用户 ID" clearable style="width: 130px" />
<n-select v-model:value="ledgerFilters.asset" :options="assetOptions" placeholder="资产" style="width: 130px" />
<n-input v-model:value="ledgerFilters.date_from" type="date" placeholder="开始日期" style="width: 160px" />
<n-input v-model:value="ledgerFilters.date_to" type="date" placeholder="结束日期" style="width: 160px" />
<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="1100"
size="small"
/>
<n-pagination
v-model:page="ledgerPage"
:page-count="ledgerPageCount"
:page-size="10"
@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-card class="jio-section-card">
<div class="jio-section-header"><h3>每日签到</h3></div>
<n-form :model="cfg.daily_signin" label-placement="left" label-width="200" class="jio-form">
<n-form-item label="启用">
<n-switch v-model:value="cfg.daily_signin.enabled" />
</n-form-item>
@@ -67,15 +40,15 @@
:autosize="{ minRows: 2 }"
/>
<template #feedback>
<span class="aksrt-hint-text">连续 N 天时额外发放的积分。</span>
<span class="jio-hint-text">连续 N 天时额外发放的积分。</span>
</template>
</n-form-item>
</n-form>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>评论奖励</h3></div>
<n-form :model="cfg.comment_reward" label-placement="left" label-width="200" class="aksrt-form">
<n-card class="jio-section-card">
<div class="jio-section-header"><h3>评论奖励</h3></div>
<n-form :model="cfg.comment_reward" label-placement="left" label-width="200" class="jio-form">
<n-form-item label="启用">
<n-switch v-model:value="cfg.comment_reward.enabled" />
</n-form-item>
@@ -94,9 +67,9 @@
</n-form>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>Ref 推广奖励</h3></div>
<n-form :model="cfg.referral_reward" label-placement="left" label-width="200" class="aksrt-form">
<n-card class="jio-section-card">
<div class="jio-section-header"><h3>Ref 推广奖励</h3></div>
<n-form :model="cfg.referral_reward" label-placement="left" label-width="200" class="jio-form">
<n-form-item label="启用">
<n-switch v-model:value="cfg.referral_reward.enabled" />
</n-form-item>
@@ -118,19 +91,19 @@
</n-form>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>下载消费(参考)</h3></div>
<n-form :model="cfg" label-placement="left" label-width="200" class="aksrt-form">
<n-card class="jio-section-card">
<div class="jio-section-header"><h3>下载消费(参考)</h3></div>
<n-form :model="cfg" label-placement="left" label-width="200" class="jio-form">
<n-form-item :label="`默认积分价参考(${cfg.currency_name || '积分'})`">
<n-input-number v-model:value="cfg.download_default_cost" :min="0" :max="100000" />
<template #feedback>
<span class="aksrt-hint-text">实际价格以文章「付费内容设置」的积分价或现金价为准;下载解锁走收银台订单,不按次扣费。</span>
<span class="jio-hint-text">实际价格以文章「付费内容设置」的积分价或现金价为准;下载解锁走收银台订单,不按次扣费。</span>
</template>
</n-form-item>
</n-form>
</n-card>
<div class="aksrt-form-footer">
<div class="jio-form-footer">
<n-button type="primary" :loading="saving" @click="saveCfg">保存配置</n-button>
</div>
</div>
@@ -162,79 +135,6 @@ const cfg = reactive({
})
const streakJson = ref('{}')
const saving = ref(false)
const ledgerLoading = ref(false)
const ledgerRows = ref([])
const ledgerPage = ref(1)
const ledgerPageCount = ref(1)
const ledgerFilters = reactive({
user_id: '',
asset: 'points',
date_from: '',
date_to: '',
})
const assetOptions = [
{ label: '积分', value: 'points' },
{ label: '余额', value: 'balance' },
]
const ledgerColumns = [
{ title: '类型', key: 'reason', width: 150 },
{ title: '订单号', key: 'ref_key', width: 200 },
{ title: '变动 / 变动后', key: 'delta_label', width: 190 },
{ title: '说明', key: 'remark', width: 240, ellipsis: { tooltip: true } },
{ title: '时间', key: 'created_at', width: 170 },
]
function formatAssetValue(asset, value) {
const raw = Number(value ?? 0)
if (!Number.isFinite(raw)) return asset === 'balance' ? '0.00' : '0'
if (asset !== 'balance') return String(raw)
const negative = raw < 0
const digits = String(Math.abs(Math.trunc(raw))).padStart(3, '0')
const whole = digits.slice(0, -2).replace(/^0+(?=\d)/, '')
return (negative ? '-' : '') + whole + '.' + digits.slice(-2)
}
async function loadLedger() {
ledgerLoading.value = true
try {
if (!/^\d+$/.test(String(ledgerFilters.user_id).trim())) {
message.warning('请输入用户 ID 后查询该用户近期记录')
return
}
const params = { page: ledgerPage.value, per_page: 10 }
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 : []
const entry = entries[0] || {}
return {
...row,
delta_label: `${formatAssetValue(entry.asset, entry.delta)} / ${formatAssetValue(entry.asset, entry.balance_after)}`,
remark: row.remark || '—',
}
})
ledgerPageCount.value = Math.max(1, Math.ceil(Number(data.total || 0) / Number(data.per_page || 10)))
} catch (e) {
message.error(e.message || '加载资产记录失败')
} finally {
ledgerLoading.value = false
}
}
function searchLedger() {
ledgerPage.value = 1
loadLedger()
}
function resetLedger() {
Object.assign(ledgerFilters, { user_id: '', asset: 'points', date_from: '', date_to: '' })
ledgerRows.value = []
ledgerPage.value = 1
ledgerPageCount.value = 1
}
async function loadCfg() {
try {
+15 -15
View File
@@ -1,6 +1,6 @@
<template>
<div class="aksrt-schema-page">
<AksFormRenderer
<div class="jio-schema-page">
<JioFormRenderer
:title="pageMeta.title"
:description="pageMeta.description"
:icon="pageMeta.icon"
@@ -24,7 +24,7 @@
v-model:value="captchaProviderTab"
type="segment"
animated
class="aksrt-tabs aksrt-captcha-provider-tabs"
class="jio-tabs jio-captcha-provider-tabs"
>
<n-tab-pane
v-for="provider in captchaProviders"
@@ -48,18 +48,18 @@
#field-geoip_block_countries="{ 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>
<!-- Slot for custom group content (override per page if needed) -->
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
</AksFormRenderer>
</JioFormRenderer>
<div
v-if="isPermissionsLogin && !loading"
class="aksrt-schema-page__tool-actions"
class="jio-schema-page__tool-actions"
>
<n-button
:disabled="!formData?.enabled || !formData?.active_provider"
@@ -80,7 +80,7 @@
<n-spin />
<p style="margin-top:10px;color:#999;">加载中...</p>
</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>
</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.
*
* 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):
* <SchemaPage domain="global" section="logo" />
@@ -106,7 +106,7 @@
import { computed, watch, ref, nextTick, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router'
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 { getSchema, normalizeSchemaDomain } from '@/schema/index.js'
import api from '@/api/index.js'
@@ -221,7 +221,7 @@ const {
const payload = { ...data }
const clearSecrets = []
Object.keys(payload).forEach((key) => {
if (payload[key] === '__AKSRT_CLEAR_SECRET__') {
if (payload[key] === '__JIO_CLEAR_SECRET__') {
clearSecrets.push(key)
delete payload[key]
}
@@ -417,7 +417,7 @@ function loadProviderSDK(provider, cfg) {
sitekey: cfg.site_key,
theme: cfg.theme || 'auto',
size: cfg.size || 'normal',
action: 'aksrt-admin-test',
action: 'jio-admin-test',
cdata: 'admin-captcha-test',
callback: function(token) {
submitCaptchaTest('cloudflare', { cf_turnstile_response: token })
@@ -539,21 +539,21 @@ onBeforeUnmount(() => {
</script>
<style scoped>
.aksrt-schema-page {
.jio-schema-page {
max-width: none;
margin: 0;
}
.aksrt-schema-page__tool-actions {
.jio-schema-page__tool-actions {
display: flex;
justify-content: flex-end;
margin-top: 16px;
}
.aksrt-schema-page__field-hint {
.jio-schema-page__field-hint {
margin: 8px 0 0;
font-size: 12px;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
}
.captcha-test-overlay {
@@ -1,5 +1,5 @@
<template>
<div class="aksrt-page-container content-page">
<div class="jio-page-container content-page">
<PageHeader title="右侧悬浮按钮" desc="显示在页面右侧的悬浮按钮,支持排序及添加自定义按钮。" />
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
@@ -7,8 +7,8 @@
<b>全局悬浮按钮:</b>显示在页面右侧的悬浮按钮,支持排序及添加自定义按钮
</n-alert>
<n-card class="aksrt-section-card">
<n-form :model="form" label-placement="left" label-width="160" class="aksrt-form">
<n-card class="jio-section-card">
<n-form :model="form" label-placement="left" label-width="160" class="jio-form">
<n-form-item label="按钮样式">
<n-radio-group v-model:value="form.style">
<n-space>
@@ -53,7 +53,7 @@
v-model="form.items"
v-bind="SORTABLE_LIST_ATTRS"
item-key="type"
class="aksrt-sortable"
class="jio-sortable"
>
<template #item="{ element, index }">
<FloatBtnItemPanel :model-value="element" @update:model-value="updateItem(index, $event)" />
@@ -63,7 +63,7 @@
</n-card>
</n-spin>
<AksSaveBar :dirty="dirty" :saving="saving" @save="handleSave" @reload="loadSettings" />
<JioSaveBar :dirty="dirty" :saving="saving" @save="handleSave" @reload="loadSettings" />
</div>
</template>
@@ -73,7 +73,7 @@ import { useMessage } from 'naive-ui'
import draggable from 'vuedraggable'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
import AksSaveBar from '@/components/form/AksSaveBar.vue'
import JioSaveBar from '@/components/form/JioSaveBar.vue'
import { SORTABLE_LIST_ATTRS } from '@/components/form/sortable-options.js'
import FloatBtnItemPanel from '@/components/floating/FloatBtnItemPanel.vue'
import { defaultSettings, normalizeItems } from '@/components/floating/floating-defaults.js'
@@ -162,13 +162,13 @@ onMounted(loadSettings)
.float-settings__desc {
font-size: 12px;
color: var(--aksrt-color-muted, #6F7A72);
color: var(--jio-color-muted, #6F7A72);
}
.float-settings__label {
margin-left: 12px;
font-size: 13px;
color: var(--aksrt-color-muted, #6F7A72);
color: var(--jio-color-muted, #6F7A72);
}
.float-settings__section {
@@ -1,5 +1,5 @@
<template>
<div class="aksrt-page-container content-page">
<div class="jio-page-container content-page">
<PageHeader title="底部Tab配置" desc="自定义移动端底部导航栏的按钮,支持排序、图标配置和返回顶部功能。">
<template #actions>
<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-card class="aksrt-section-card">
<n-card class="jio-section-card">
<template #header>
<div class="aksrt-card-header-row">
<div class="aksrt-card-header-row__left">
<div class="jio-card-header-row">
<div class="jio-card-header-row__left">
<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>
</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-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 label="向下滚动隐藏">
<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 label="文章页专属Tab">
<n-select
@@ -36,18 +36,18 @@
]"
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>
</n-card>
<!-- Tab按钮配置 -->
<n-card class="aksrt-section-card">
<n-card class="jio-section-card">
<template #header>
<div class="aksrt-card-header-row">
<div class="aksrt-card-header-row__left">
<div class="jio-card-header-row">
<div class="jio-card-header-row__left">
<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>
<n-button type="primary" size="small" @click="addItem">
<template #icon><i class="fa fas fa-plus"></i></template>
@@ -63,27 +63,27 @@
v-model="form.items"
v-bind="SORTABLE_LIST_ATTRS"
item-key="id"
class="aksrt-sortable"
class="jio-sortable"
@end="resetSortOrder"
>
<template #item="{ element, index }">
<div class="aksrt-sortable__item">
<div class="aksrt-sortable__head">
<AksSortHandle />
<span class="aksrt-sortable__icon">
<div class="jio-sortable__item">
<div class="jio-sortable__head">
<JioSortHandle />
<span class="jio-sortable__icon">
<IconValue v-if="element.icon" :value="element.icon" />
</span>
<div class="aksrt-sortable__main">
<div class="aksrt-sortable__title-row">
<span class="aksrt-sortable__title">{{ element.text || '按钮' }}</span>
<div class="jio-sortable__main">
<div class="jio-sortable__title-row">
<span class="jio-sortable__title">{{ element.text || '按钮' }}</span>
<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.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>
</div>
<div class="aksrt-sortable__desc">{{ getItemSummary(element) }}</div>
<div class="jio-sortable__desc">{{ getItemSummary(element) }}</div>
</div>
<div class="aksrt-sortable__actions">
<div class="jio-sortable__actions">
<n-button size="tiny" quaternary type="primary" @click="editItem(index)">
<template #icon><i class="fa fas fa-edit" style="font-size:12px;" /></template>
编辑
@@ -100,7 +100,7 @@
<n-empty v-else description="暂无Tab按钮,请点击上方按钮添加" />
</n-card>
<div class="aksrt-form-actions">
<div class="jio-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存底部Tab配置</n-button>
</div>
</n-spin>
@@ -111,10 +111,10 @@
:mask-closable="false"
preset="card"
:title="editingIndex >= 0 ? '编辑Tab按钮' : '添加Tab按钮'"
class="aksrt-modal-card"
class="jio-modal-card"
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-select
v-model:value="editingItem.type"
@@ -214,12 +214,12 @@
v-model="editingItem.add_items"
v-bind="SORTABLE_LIST_ATTRS"
item-key="id"
class="aksrt-sortable aksrt-sortable--compact"
class="jio-sortable jio-sortable--compact"
>
<template #item="{ element, index: subIndex }">
<div class="aksrt-sortable__item">
<div class="aksrt-sortable__head add-items-editor__row">
<AksSortHandle />
<div class="jio-sortable__item">
<div class="jio-sortable__head add-items-editor__row">
<JioSortHandle />
<n-input v-model:value="element.label" placeholder="名称,例如:写文章" />
<FaIconPicker v-model="element.icon" placeholder="选择图标" />
<n-input v-model:value="element.url" placeholder="链接,例如:/wp-admin/post-new.php" />
@@ -236,7 +236,7 @@
<n-form-item v-if="editingItem.type === 'home'" label="返回顶部功能">
<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 label="启用状态">
@@ -245,7 +245,7 @@
</n-form>
<template #footer>
<div class="aksrt-modal-footer">
<div class="jio-modal-footer">
<n-button @click="showModal = false">取消</n-button>
<n-button type="primary" @click="saveItem">保存</n-button>
</div>
@@ -262,7 +262,7 @@ import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
import FaIconPicker from '@/components/form/FaIconPicker.vue'
import IconValue from '@/components/form/IconValue.vue'
import AksSortHandle from '@/components/form/AksSortHandle.vue'
import JioSortHandle from '@/components/form/JioSortHandle.vue'
import { SORTABLE_LIST_ATTRS } from '@/components/form/sortable-options.js'
const message = useMessage()
@@ -606,7 +606,7 @@ onMounted(loadSettings)
}
.icon-preview-row a {
color: var(--aksrt-color-primary, #18A058);
color: var(--jio-color-primary, #18A058);
text-decoration: none;
}
@@ -615,12 +615,12 @@ onMounted(loadSettings)
}
.muted-text {
color: var(--aksrt-color-muted, #6F7A72);
color: var(--jio-color-muted, #6F7A72);
font-size: 12px;
}
.section-desc {
color: var(--aksrt-color-muted, #6F7A72);
color: var(--jio-color-muted, #6F7A72);
font-size: 13px;
margin: 0 0 16px;
}
@@ -650,7 +650,7 @@ onMounted(loadSettings)
.add-items-editor__presets-label {
font-size: 12px;
color: var(--aksrt-color-muted, #6F7A72);
color: var(--jio-color-muted, #6F7A72);
}
.add-items-editor__row {
@@ -1,5 +1,5 @@
<template>
<div class="aksrt-page-container content-page">
<div class="jio-page-container content-page">
<PageHeader title="通知管理" desc="管理通知内容开关、user 徽标统计口径和自动清理策略。">
<template #actions>
<n-space>
@@ -10,7 +10,7 @@
</PageHeader>
<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-item label="评论回复通知">
<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-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-item label="启用 user 动态徽标">
<n-switch v-model:value="settings.user_badge_enabled" />
</n-form-item>
<n-form-item label="徽标包含状态提醒">
<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>
</n-card>
</n-grid-item>
<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-item label="已读通知保留天数">
<n-input-number v-model:value="settings.read_retention_days" :min="1" :max="365" style="width: 180px;" />
+42 -42
View File
@@ -1,12 +1,12 @@
<template>
<div class="aksrt-page-container">
<div class="jio-page-container">
<PageHeader title="文档中心" desc="主题使用文档、开发指南、迁移说明和更新日志。" />
<!-- 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-item>
<div class="aksrt-docs__action">
<div class="jio-docs__action">
<FaIcon icon="fa fas fa-paint-brush" :size="16" />
<div>
<strong>全局设置</strong>
@@ -16,7 +16,7 @@
</div>
</n-grid-item>
<n-grid-item>
<div class="aksrt-docs__action">
<div class="jio-docs__action">
<FaIcon icon="fa fas fa-home" :size="16" />
<div>
<strong>首页配置</strong>
@@ -26,7 +26,7 @@
</div>
</n-grid-item>
<n-grid-item>
<div class="aksrt-docs__action">
<div class="jio-docs__action">
<FaIcon icon="fa fas fa-users" :size="16" />
<div>
<strong>用户管理</strong>
@@ -36,7 +36,7 @@
</div>
</n-grid-item>
<n-grid-item>
<div class="aksrt-docs__action">
<div class="jio-docs__action">
<FaIcon icon="fa fas fa-wrench" :size="16" />
<div>
<strong>工具</strong>
@@ -49,30 +49,30 @@
</n-card>
<!-- Reference Docs -->
<div class="aksrt-docs__grid">
<div class="jio-docs__grid">
<n-card
v-for="doc in docs"
:key="doc.key"
class="aksrt-docs__card"
class="jio-docs__card"
size="small"
hoverable
@click="openDoc(doc)"
>
<div class="aksrt-docs__card-inner">
<div class="aksrt-docs__card-icon" :style="{ background: doc.color }">
<div class="jio-docs__card-inner">
<div class="jio-docs__card-icon" :style="{ background: doc.color }">
<FaIcon :icon="doc.icon" :size="20" color="#fff" />
</div>
<div class="aksrt-docs__card-body">
<h4 class="aksrt-docs__card-title">{{ doc.title }}</h4>
<p class="aksrt-docs__card-desc">{{ doc.description }}</p>
<div class="jio-docs__card-body">
<h4 class="jio-docs__card-title">{{ doc.title }}</h4>
<p class="jio-docs__card-desc">{{ doc.description }}</p>
</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>
</n-card>
</div>
<!-- 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-item label="主题版本">
<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 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 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: 'legacy-gap-matrix', title: '对标差异矩阵', description: 'AKSRT 当前 → 原主题参考 → AKSRT 目标三层对照表。', icon: 'fa fas fa-columns', color: '#722ed1', file: 'legacy-gap-matrix.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: '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: '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' },
@@ -117,35 +117,35 @@ onMounted(async () => {
function openDoc(doc) {
// Link to local docs directory (admin can open in new tab)
const base = window.aksrtAdminData?.siteUrl || window.location.origin
window.open(`${base}/wp-content/themes/AKSRT-theme/docs/${doc.file}`, '_blank')
const base = window.jioAdminData?.siteUrl || window.location.origin
window.open(`${base}/wp-content/themes/Jio/docs/${doc.file}`, '_blank')
}
</script>
<style scoped>
.aksrt-docs__grid {
.jio-docs__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.aksrt-docs__card {
.jio-docs__card {
cursor: pointer;
transition: box-shadow 0.2s;
}
.aksrt-docs__card:hover {
.jio-docs__card:hover {
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
}
.aksrt-docs__card-inner {
.jio-docs__card-inner {
display: flex;
align-items: center;
gap: 12px;
}
.aksrt-docs__card-icon {
.jio-docs__card-icon {
flex-shrink: 0;
width: 40px;
height: 40px;
@@ -155,38 +155,38 @@ function openDoc(doc) {
justify-content: center;
}
.aksrt-docs__card-body {
.jio-docs__card-body {
flex: 1;
min-width: 0;
}
.aksrt-docs__card-title {
.jio-docs__card-title {
margin: 0 0 2px;
font-size: 14px;
font-weight: 600;
color: var(--aksrt-color-text);
color: var(--jio-color-text);
}
.aksrt-docs__card-desc {
.jio-docs__card-desc {
margin: 0;
font-size: 12px;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.aksrt-docs__card-arrow {
.jio-docs__card-arrow {
flex-shrink: 0;
color: #c0c4cc;
}
.aksrt-docs__section {
.jio-docs__section {
margin-bottom: 16px;
}
.aksrt-docs__action {
.jio-docs__action {
display: flex;
gap: 12px;
align-items: flex-start;
@@ -195,31 +195,31 @@ function openDoc(doc) {
background: #fafafa;
}
.aksrt-docs__action i {
color: var(--aksrt-color-primary);
.jio-docs__action i {
color: var(--jio-color-primary);
margin-top: 2px;
}
.aksrt-docs__action strong {
.jio-docs__action strong {
display: block;
font-size: 14px;
color: var(--aksrt-color-text);
color: var(--jio-color-text);
margin-bottom: 2px;
}
.aksrt-docs__action p {
.jio-docs__action p {
margin: 0 0 4px;
font-size: 12px;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
}
.aksrt-docs__action a {
.jio-docs__action a {
font-size: 12px;
color: var(--aksrt-color-primary);
color: var(--jio-color-primary);
text-decoration: none;
}
.aksrt-docs__action a:hover {
.jio-docs__action a:hover {
text-decoration: underline;
}
</style>
@@ -1,5 +1,5 @@
<template>
<div class="aksrt-page-container">
<div class="jio-page-container">
<PageHeader :title="title" :desc="desc">
<template #actions>
<n-space v-if="links.length">
@@ -10,7 +10,7 @@
</template>
</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">
{{ note }}
</n-alert>
@@ -1,5 +1,5 @@
<template>
<div class="aksrt-page-container">
<div class="jio-page-container">
<PageHeader
title="基本权限 / 能力矩阵"
desc="统一管理用户治理、投稿和评论的角色能力矩阵。"
@@ -17,8 +17,8 @@
<n-spin :show="loading" style="width: 100%; min-height: 240px;">
<n-card class="aksrt-section-card" v-if="summary.cap_count">
<div class="aksrt-section-header">
<n-card class="jio-section-card" v-if="summary.cap_count">
<div class="jio-section-header">
<h3>矩阵概览</h3>
</div>
<n-space>
@@ -28,12 +28,12 @@
</n-space>
</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>
<div class="aksrt-cap-matrix__header">
<div class="jio-cap-matrix__header">
<div>
<h3 class="aksrt-cap-matrix__title">{{ group.title }}</h3>
<p class="aksrt-cap-matrix__desc">共 {{ group.items.length }} 项能力</p>
<h3 class="jio-cap-matrix__title">{{ group.title }}</h3>
<p class="jio-cap-matrix__desc">共 {{ group.items.length }} 项能力</p>
</div>
</div>
</template>
@@ -48,7 +48,7 @@
/>
</n-card>
<div class="aksrt-form-actions">
<div class="jio-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存能力矩阵</n-button>
</div>
</n-spin>
@@ -117,7 +117,7 @@ function buildColumns(group) {
key: 'label',
fixed: 'left',
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('code', row.cap_id),
]),
@@ -148,7 +148,7 @@ function buildColumns(group) {
function renderCapCell(row, col) {
const hidden = !(row.visible_columns || []).includes(col)
if (hidden) {
return h('span', { class: 'aksrt-cap-matrix__empty' }, '—')
return h('span', { class: 'jio-cap-matrix__empty' }, '—')
}
if (!userCaps[row.cap_id]) {
@@ -209,40 +209,40 @@ async function handleSave() {
</script>
<style scoped>
.aksrt-cap-matrix__header {
.jio-cap-matrix__header {
display: flex;
align-items: center;
justify-content: space-between;
}
.aksrt-cap-matrix__title {
.jio-cap-matrix__title {
margin: 0;
font-size: 15px;
font-weight: 600;
}
.aksrt-cap-matrix__desc {
.jio-cap-matrix__desc {
margin: 4px 0 0;
font-size: 12px;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
}
.aksrt-cap-matrix__cap {
.jio-cap-matrix__cap {
display: flex;
flex-direction: column;
gap: 4px;
}
.aksrt-cap-matrix__cap code {
.jio-cap-matrix__cap code {
font-size: 11px;
color: #667085;
}
.aksrt-cap-matrix__empty {
.jio-cap-matrix__empty {
color: #c0c4cc;
}
.aksrt-form-actions {
.jio-form-actions {
display: flex;
justify-content: flex-end;
margin-top: 16px;
@@ -1,5 +1,5 @@
<template>
<div class="aks-native-ops-redirect">
<div class="jio-native-ops-redirect">
<h2>{{ title }}</h2>
<p class="desc">
此运营功能已迁移到 WordPress 原生后台侧栏(对齐 legacy)。正在跳转…
@@ -21,7 +21,7 @@ const title = computed(() => route.meta?.title || '运营功能已迁移')
const nativeUrl = computed(() => {
const path = route.meta?.nativeAdminPath || ''
if (!path) return ''
const base = (window.aksrtAdminData?.adminUrl || '/wp-admin/').replace(/\/?$/, '/')
const base = (window.jioAdminData?.adminUrl || '/wp-admin/').replace(/\/?$/, '/')
return base + path.replace(/^\//, '')
})
@@ -33,13 +33,13 @@ onMounted(() => {
</script>
<style scoped>
.aks-native-ops-redirect {
.jio-native-ops-redirect {
max-width: 640px;
margin: 48px auto;
padding: 24px;
background: var(--aks-surface, #fff);
background: var(--jio-surface, #fff);
border-radius: 8px;
border: 1px solid var(--aks-border, #e5e7eb);
border: 1px solid var(--jio-border, #e5e7eb);
}
.desc {
color: #646970;
@@ -1,5 +1,5 @@
<template>
<div class="aksrt-page-container">
<div class="jio-page-container">
<PageHeader
title="主题授权"
desc="当前版本按产品决策不实现在线授权/撤销授权,仅保留主题信息与更新能力。"
@@ -12,14 +12,14 @@
</template>
</PageHeader>
<n-card class="aksrt-section-card">
<n-card class="jio-section-card">
<n-alert type="info" title="按设计省略" :show-icon="true">
本主题当前不实现 原主题风格的授权码校验、域名绑定、撤销授权、本地免授权等机制。
`AKSRT_Update_Checker` 仅负责匿名版本检测与更新提示,不与授权系统耦合。
`JIO_Update_Checker` 仅负责匿名版本检测与更新提示,不与授权系统耦合。
</n-alert>
</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-item>不提供授权码输入框或“立即授权”按钮</n-list-item>
<n-list-item>不提供“已授权 / 未授权 / 本地免授权”三态逻辑</n-list-item>
+27 -27
View File
@@ -1,14 +1,14 @@
<template>
<div class="aksrt-page-container">
<div class="jio-page-container">
<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" />
</div>
<template v-else>
<!-- 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-item label="主题版本">
<n-tag type="success" size="small">{{ info.themeVersion }}</n-tag>
@@ -33,7 +33,7 @@
</n-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-item label="站点地址">
<n-text code>{{ info.siteUrl }}</n-text>
@@ -61,14 +61,14 @@
</n-card>
<!-- Modules Card -->
<n-card class="aksrt-theme-info__card" title="已启用模块" size="small">
<div class="aksrt-theme-info__modules">
<n-card class="jio-theme-info__card" title="已启用模块" size="small">
<div class="jio-theme-info__modules">
<n-tag
v-for="mod in info.modules"
:key="mod.key"
:type="mod.enabled ? 'success' : 'default'"
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" />
{{ mod.label }}
@@ -80,7 +80,7 @@
</n-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
:columns="tableColumns"
:data="info.tables"
@@ -91,25 +91,25 @@
</n-card>
<!-- Docs Links Card -->
<n-card class="aksrt-theme-info__card" title="快速入口" size="small">
<div class="aksrt-theme-info__links">
<router-link to="/docs" class="aksrt-theme-info__link">
<n-card class="jio-theme-info__card" title="快速入口" size="small">
<div class="jio-theme-info__links">
<router-link to="/docs" class="jio-theme-info__link">
<FaIcon icon="fa fas fa-book" :size="16" />
<span>文档中心</span>
</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" />
<span>更新中心</span>
</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" />
<span>数据备份</span>
</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" />
<span>健康检查</span>
</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" />
<span>导入导出</span>
</router-link>
@@ -173,13 +173,13 @@ onMounted(async () => {
const data = res?.data || res || {}
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 || '',
phpVersion: data.php_version || '',
dbSchemaVersion: data.db_schema_version || '',
emailBackfillDone: !!data.email_backfill_done,
siteUrl: data.site_url || window.aksrtAdminData?.siteUrl || '',
apiUrl: data.api_url || window.aksrtAdminData?.apiUrl || '',
siteUrl: data.site_url || window.jioAdminData?.siteUrl || '',
apiUrl: data.api_url || window.jioAdminData?.apiUrl || '',
themeDir: data.theme_dir || '',
uploadDir: data.upload_dir || '',
isMultisite: !!data.is_multisite,
@@ -191,8 +191,8 @@ onMounted(async () => {
console.error('[ThemeInfo] Failed to load health data:', err)
// Fallback with minimal info
info.value.themeVersion = '3.4.0-dev'
info.value.siteUrl = window.aksrtAdminData?.siteUrl || ''
info.value.apiUrl = window.aksrtAdminData?.apiUrl || ''
info.value.siteUrl = window.jioAdminData?.siteUrl || ''
info.value.apiUrl = window.jioAdminData?.apiUrl || ''
} finally {
loading.value = false
}
@@ -218,35 +218,35 @@ function normalizeTables(raw) {
</script>
<style scoped>
.aksrt-theme-info__loading {
.jio-theme-info__loading {
display: flex;
justify-content: center;
padding: 60px 0;
}
.aksrt-theme-info__card {
.jio-theme-info__card {
margin-bottom: 16px;
}
.aksrt-theme-info__modules {
.jio-theme-info__modules {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.aksrt-theme-info__module-tag {
.jio-theme-info__module-tag {
display: inline-flex;
align-items: center;
gap: 5px;
}
.aksrt-theme-info__links {
.jio-theme-info__links {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.aksrt-theme-info__link {
.jio-theme-info__link {
display: inline-flex;
align-items: center;
gap: 6px;
@@ -259,7 +259,7 @@ function normalizeTables(raw) {
transition: background 0.2s;
}
.aksrt-theme-info__link:hover {
.jio-theme-info__link:hover {
background: #e8eaed;
}
</style>
+13 -13
View File
@@ -1,5 +1,5 @@
<template>
<div class="aksrt-page-container">
<div class="jio-page-container">
<PageHeader
title="数据备份"
desc="管理设置快照、创建手动备份,并执行恢复或删除。"
@@ -12,8 +12,8 @@
</PageHeader>
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<n-card class="jio-section-card">
<div class="jio-section-header">
<h3>备份策略</h3>
</div>
<n-space vertical size="small" style="width: 100%;">
@@ -26,19 +26,19 @@
<n-tag round :type="backups.length ? 'success' : 'default'">现有快照:{{ backups.length }} 份</n-tag>
</n-space>
<n-space align="center" wrap>
<span class="aksrt-backup__label">自动备份</span>
<span class="jio-backup__label">自动备份</span>
<n-switch v-model:value="settings.backup_auto" />
<span class="aksrt-backup__label">频率</span>
<span class="jio-backup__label">频率</span>
<n-select v-model:value="settings.backup_frequency" :options="frequencyOptions" style="width: 140px;" />
<span class="aksrt-backup__label">保留</span>
<span class="jio-backup__label">保留</span>
<n-input-number v-model:value="settings.backup_retain" :min="3" :max="20" style="width: 140px;" />
<n-button type="primary" :loading="savingPolicy" @click="handleSavePolicy">保存策略</n-button>
</n-space>
</n-space>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<n-card class="jio-section-card">
<div class="jio-section-header">
<h3>手动创建快照</h3>
</div>
<n-space vertical size="small" style="width: 100%;">
@@ -50,8 +50,8 @@
</n-space>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<n-card class="jio-section-card">
<div class="jio-section-header">
<h3>快照列表</h3>
</div>
<n-empty v-if="!backups.length" description="暂无设置快照" />
@@ -64,7 +64,7 @@
:single-line="false"
:scroll-x="900"
/>
<p class="aksrt-backup__hint">
<p class="jio-backup__hint">
恢复非当前网站 / 非当前版本的快照可能导致配置异常,恢复前请确认站点环境一致。
</p>
</n-card>
@@ -264,14 +264,14 @@ async function handleDelete(row) {
</script>
<style scoped>
.aksrt-backup__hint {
.jio-backup__hint {
margin: 12px 0 0;
color: #667085;
font-size: 12px;
line-height: 1.7;
}
.aksrt-backup__label {
.jio-backup__label {
color: #344054;
font-size: 12px;
}
+5 -5
View File
@@ -1,5 +1,5 @@
<template>
<div class="aksrt-page-container">
<div class="jio-page-container">
<PageHeader
title="健康检查"
desc="集中查看运行环境、数据库表和系统诊断状态。"
@@ -12,9 +12,9 @@
</PageHeader>
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
<div class="aksrt-health-grid">
<n-card v-for="check in checkItems" :key="check.key" class="aksrt-section-card">
<div class="aksrt-section-header">
<div class="jio-health-grid">
<n-card v-for="check in checkItems" :key="check.key" class="jio-section-card">
<div class="jio-section-header">
<h3>{{ check.label }}</h3>
<n-tag round :type="statusToTag(check.status)">{{ check.status }}</n-tag>
</div>
@@ -76,7 +76,7 @@ onMounted(load)
</script>
<style scoped>
.aksrt-health-grid {
.jio-health-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
@@ -1,5 +1,5 @@
<template>
<div class="aksrt-page-container">
<div class="jio-page-container">
<PageHeader
title="导入导出"
desc="导出当前设置快照、执行 dry-run 导入校验,并支持按域重置到 schema 默认值。"
@@ -12,8 +12,8 @@
</template>
</PageHeader>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<n-card class="jio-section-card">
<div class="jio-section-header">
<h3>导出与导入</h3>
</div>
<n-space style="margin-bottom: 16px;">
@@ -27,13 +27,13 @@
:autosize="{ minRows: 12, maxRows: 20 }"
placeholder="导出的 JSON 会出现在这里,也可以直接粘贴导入。"
/>
<p class="aksrt-tools-hint">
<p class="jio-tools-hint">
正式导入前会自动创建一份“导入前自动快照”,与“数据备份”页的快照体系共用,便于回滚。
</p>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<n-card class="jio-section-card">
<div class="jio-section-header">
<h3>按域重置</h3>
</div>
<n-space align="center" wrap>
@@ -41,14 +41,14 @@
<n-button :loading="resetting" @click="handleReset">重置所选域</n-button>
<n-button type="error" tertiary :loading="resettingAll" @click="handleResetAll">重置全部</n-button>
</n-space>
<p class="aksrt-tools-hint">仅支持重置 schema 托管的业务域。健康检查和迁移状态属于运维面板,不参与按域回滚。</p>
<p class="jio-tools-hint">仅支持重置 schema 托管的业务域。健康检查和迁移状态属于运维面板,不参与按域回滚。</p>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<n-card class="jio-section-card">
<div class="jio-section-header">
<h3>最近结果</h3>
</div>
<pre class="aksrt-tools-output">{{ resultText || '尚未执行操作。' }}</pre>
<pre class="jio-tools-output">{{ resultText || '尚未执行操作。' }}</pre>
</n-card>
</div>
</template>
@@ -169,7 +169,7 @@ async function handleResetAll() {
</script>
<style scoped>
.aksrt-tools-output {
.jio-tools-output {
margin: 0;
padding: 14px;
border-radius: 12px;
@@ -181,7 +181,7 @@ async function handleResetAll() {
word-break: break-word;
}
.aksrt-tools-hint {
.jio-tools-hint {
margin: 12px 0 0;
color: #667085;
font-size: 12px;
+18 -18
View File
@@ -1,5 +1,5 @@
<template>
<div class="aksrt-page-container">
<div class="jio-page-container">
<PageHeader
title="更新中心"
desc="查看版本状态、检测更新并浏览更新日志与教程入口。"
@@ -13,8 +13,8 @@
</PageHeader>
<n-spin :show="loading" style="width: 100%; min-height: 220px;">
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<n-card class="jio-section-card">
<div class="jio-section-header">
<h3>版本状态</h3>
</div>
<n-space>
@@ -26,8 +26,8 @@
</n-space>
</n-card>
<n-card class="aksrt-section-card" v-if="updateInfo.update_available">
<div class="aksrt-section-header">
<n-card class="jio-section-card" v-if="updateInfo.update_available">
<div class="jio-section-header">
<h3>可用更新</h3>
</div>
<n-alert type="warning" style="margin-bottom: 12px;">
@@ -39,18 +39,18 @@
</n-button>
<n-button @click="handleDismiss" :loading="dismissing">忽略此版本</n-button>
</n-space>
<div v-if="updateInfo.changelog" class="aksrt-update__changelog">
<div v-if="updateInfo.changelog" class="jio-update__changelog">
<h4>更新日志</h4>
<pre>{{ updateInfo.changelog }}</pre>
</div>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<n-card class="jio-section-card">
<div class="jio-section-header">
<h3>常用教程入口</h3>
</div>
<div class="aksrt-update__grid">
<a v-for="item in guides" :key="item.key" class="aksrt-update__guide" :href="item.url" target="_blank" rel="noreferrer">
<div class="jio-update__grid">
<a v-for="item in guides" :key="item.key" class="jio-update__guide" :href="item.url" target="_blank" rel="noreferrer">
<strong>{{ item.title }}</strong>
<span>{{ item.desc }}</span>
</a>
@@ -152,11 +152,11 @@ async function handleDismiss() {
</script>
<style scoped>
.aksrt-update__changelog {
.jio-update__changelog {
margin-top: 16px;
}
.aksrt-update__changelog pre {
.jio-update__changelog pre {
margin: 8px 0 0;
padding: 12px 14px;
border-radius: 12px;
@@ -168,30 +168,30 @@ async function handleDismiss() {
word-break: break-word;
}
.aksrt-update__grid {
.jio-update__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.aksrt-update__guide {
.jio-update__guide {
display: flex;
flex-direction: column;
gap: 4px;
padding: 14px;
border: 1px solid var(--aksrt-color-border);
border: 1px solid var(--jio-color-border);
border-radius: 12px;
text-decoration: none;
color: inherit;
background: #fafafa;
}
.aksrt-update__guide:hover {
border-color: var(--aksrt-color-primary);
.jio-update__guide:hover {
border-color: var(--jio-color-primary);
background: #f5faff;
}
.aksrt-update__guide span {
.jio-update__guide span {
font-size: 12px;
color: #667085;
}
+12 -12
View File
@@ -1,5 +1,5 @@
<template>
<div class="aksrt-page-container">
<div class="jio-page-container">
<PageHeader title="用户徽章" desc="配置勋章分类、自动授予条件与前台公开展示。">
<template #actions>
<n-space>
@@ -10,11 +10,11 @@
</PageHeader>
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<n-card class="jio-section-card">
<div class="jio-section-header">
<h3>徽章展示</h3>
</div>
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
<n-form :model="form" label-placement="left" label-width="180" class="jio-form">
<n-form-item label="启用徽章/勋章系统">
<n-switch v-model:value="form.user_medal_s" />
</n-form-item>
@@ -31,8 +31,8 @@
</n-form>
</n-card>
<n-card v-if="form.user_medal_s" class="aksrt-section-card">
<div class="aksrt-section-header users-badge__header">
<n-card v-if="form.user_medal_s" class="jio-section-card">
<div class="jio-section-header users-badge__header">
<h3>徽章明细</h3>
<span class="users-badge__stats">{{ form.user_medal_args.length }} 个分类 · {{ itemCount }} 枚徽章</span>
</div>
@@ -40,7 +40,7 @@
拖动手柄调整分类与条目顺序,点击行头展开编辑。获取条件选择「仅手动授予」时不自动发放。
</p>
<div v-if="!form.user_medal_args.length" class="aksrt-sortable__empty">
<div v-if="!form.user_medal_args.length" class="jio-sortable__empty">
<i class="fa fas fa-trophy" aria-hidden="true" />
<span>暂无分类,请添加</span>
</div>
@@ -49,7 +49,7 @@
v-model="form.user_medal_args"
v-bind="SORTABLE_LIST_ATTRS"
:item-key="rowKey"
class="aksrt-sortable"
class="jio-sortable"
>
<template #item="{ element, index }">
<MedalCategoryCard
@@ -68,7 +68,7 @@
</n-card>
</n-spin>
<AksSaveBar :dirty="dirty" :saving="saving" @save="handleSave" @reload="load" />
<JioSaveBar :dirty="dirty" :saving="saving" @save="handleSave" @reload="load" />
</div>
</template>
@@ -79,7 +79,7 @@ import { useMessage } from 'naive-ui'
import draggable from 'vuedraggable'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
import AksSaveBar from '@/components/form/AksSaveBar.vue'
import JioSaveBar from '@/components/form/JioSaveBar.vue'
import { SORTABLE_LIST_ATTRS } from '@/components/form/sortable-options.js'
import { useCollapsibleItems } from '@/components/form/useCollapsibleItems.js'
import { defaultMedalArgs } from '@/schema/sections/users/badge.js'
@@ -176,13 +176,13 @@ onMounted(load)
}
.users-badge__stats {
color: var(--aksrt-color-muted, #6F7A72);
color: var(--jio-color-muted, #6F7A72);
font-size: 12px;
}
.users-badge__hint {
margin: 0 0 12px;
color: var(--aksrt-color-muted, #6F7A72);
color: var(--jio-color-muted, #6F7A72);
font-size: 13px;
}
+13 -13
View File
@@ -1,5 +1,5 @@
<template>
<div class="aksrt-page-container">
<div class="jio-page-container">
<PageHeader
title="用户等级"
desc="配置等级开关、最高等级、升级经验阈值与经验获取矩阵(对齐 legacy 用户等级 / 经验值)。"
@@ -13,8 +13,8 @@
</PageHeader>
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>当前摘要</h3></div>
<n-card class="jio-section-card">
<div class="jio-section-header"><h3>当前摘要</h3></div>
<n-space>
<n-tag round :type="form.user_level_s ? 'success' : 'default'">
等级系统:{{ form.user_level_s ? '已启用' : '已关闭' }}
@@ -24,9 +24,9 @@
</n-space>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>等级显示</h3></div>
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
<n-card class="jio-section-card">
<div class="jio-section-header"><h3>等级显示</h3></div>
<n-form :model="form" label-placement="left" label-width="180" class="jio-form">
<n-form-item label="启用用户等级">
<n-switch v-model:value="form.user_level_s" />
</n-form-item>
@@ -42,17 +42,17 @@
</n-form>
</n-card>
<n-card v-if="form.user_level_s" class="aksrt-section-card">
<div class="aksrt-section-header"><h3>升级所需经验(累计)</h3></div>
<n-card v-if="form.user_level_s" class="jio-section-card">
<div class="jio-section-header"><h3>升级所需经验(累计)</h3></div>
<n-alert type="info" :show-icon="false" style="margin-bottom: 12px;">
Lv.1 通常为 0。数值为升到该级所需的累计经验。修改最高等级后会自动补齐/截断行。
</n-alert>
<n-data-table :columns="levelColumns" :data="levelRows" :bordered="false" size="small" />
</n-card>
<n-card v-if="form.user_level_s" class="aksrt-section-card">
<div class="aksrt-section-header"><h3>经验值得分</h3></div>
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
<n-card v-if="form.user_level_s" class="jio-section-card">
<div class="jio-section-header"><h3>经验值得分</h3></div>
<n-form :model="form" label-placement="left" label-width="180" class="jio-form">
<n-form-item label="每日上限">
<n-input-number v-model:value="form.user_integral_opt.day_max" :min="0" :max="10000" style="width: 180px;" />
</n-form-item>
@@ -71,7 +71,7 @@
</div>
</n-card>
<div class="aksrt-form-actions">
<div class="jio-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存配置</n-button>
</div>
</n-spin>
@@ -140,7 +140,7 @@ const levelColumns = [
h(NInput, {
value: form.user_level_opt['icon_img_' + row.level] ?? '',
size: 'small',
placeholder: row.level <= 10 ? '留空使用主题内置图标' : '请输入图片 URL',
placeholder: row.level <= 10 ? '默认使用主题自带等级图标' : '请输入图片 URL',
onUpdateValue: (v) => {
form.user_level_opt['icon_img_' + row.level] = v
},
+5 -5
View File
@@ -1,5 +1,5 @@
<template>
<div class="aksrt-page-container">
<div class="jio-page-container">
<PageHeader
title="工单中心"
desc="工单功能开关、提交公告与提交表单配置,以及部门、状态、预设回复三块字典维护。工单的日常处理在 WordPress 用户菜单的「工单管理」中完成。"
@@ -11,7 +11,7 @@
</template>
</PageHeader>
<n-tabs v-model:value="activeTab" type="segment" animated class="aksrt-ticket-tabs">
<n-tabs v-model:value="activeTab" type="segment" animated class="jio-ticket-tabs">
<n-tab-pane name="settings" tab="基础配置">
<SchemaPage domain="users" section="ticket" />
</n-tab-pane>
@@ -57,13 +57,13 @@ watch(activeTab, (name) => {
})
function openNativeList() {
const base = (window.aksrtAdminData?.adminUrl || '/wp-admin/').replace(/\/?$/, '/')
window.location.href = base + 'users.php?page=aksrt_tickets'
const base = (window.jioAdminData?.adminUrl || '/wp-admin/').replace(/\/?$/, '/')
window.location.href = base + 'users.php?page=jio_tickets'
}
</script>
<style scoped>
.aksrt-ticket-tabs {
.jio-ticket-tabs {
margin-bottom: 4px;
}
</style>
+3 -3
View File
@@ -1,8 +1,8 @@
<template>
<div class="aksrt-page-container">
<div class="jio-page-container">
<PageHeader title="身份认证" desc="配置通用认证申请规则;申请审核和用户认证资料管理位于 WordPress 用户菜单。" />
<n-spin :show="loading" style="width: 100%; min-height: 240px">
<n-card class="aksrt-section-card" title="认证申请设置">
<n-card class="jio-section-card" title="认证申请设置">
<n-form :model="form" label-placement="left" label-width="190">
<n-form-item label="启用身份认证"><n-switch v-model:value="form.user_auth_s" /></n-form-item>
<n-form-item label="允许用户前台申请"><n-switch v-model:value="form.user_auth_apply_s" :disabled="!form.user_auth_s" /></n-form-item>
@@ -18,7 +18,7 @@
</n-form-item>
</n-form>
</n-card>
<div class="aksrt-form-actions">
<div class="jio-form-actions">
<n-button :disabled="!dirty || saving" @click="load">放弃更改</n-button>
<n-button type="primary" :disabled="!dirty" :loading="saving" @click="save">保存设置</n-button>
</div>
@@ -1,80 +1,80 @@
<template>
<div class="aksrt-page-container">
<div class="jio-page-container">
<PageHeader title="微信模板消息" desc="配置微信公众号模板消息推送。" />
<!-- 状态卡片 -->
<div class="aksrt-wechat__status">
<div class="aksrt-wechat__status-card">
<div class="aksrt-wechat__status-icon" :class="status.configured ? 'aksrt-wechat__status-icon--ok' : 'aksrt-wechat__status-icon--warn'">
<div class="jio-wechat__status">
<div class="jio-wechat__status-card">
<div class="jio-wechat__status-icon" :class="status.configured ? 'jio-wechat__status-icon--ok' : 'jio-wechat__status-icon--warn'">
<i :class="status.configured ? 'fa fas fa-check' : 'fa fas fa-exclamation'"></i>
</div>
<div class="aksrt-wechat__status-info">
<div class="aksrt-wechat__status-label">配置状态</div>
<div class="aksrt-wechat__status-value">{{ status.configured ? '已配置' : '未配置' }}</div>
<div class="jio-wechat__status-info">
<div class="jio-wechat__status-label">配置状态</div>
<div class="jio-wechat__status-value">{{ status.configured ? '已配置' : '未配置' }}</div>
</div>
</div>
<div class="aksrt-wechat__status-card">
<div class="aksrt-wechat__status-icon" :class="status.has_token ? 'aksrt-wechat__status-icon--ok' : 'aksrt-wechat__status-icon--warn'">
<div class="jio-wechat__status-card">
<div class="jio-wechat__status-icon" :class="status.has_token ? 'jio-wechat__status-icon--ok' : 'jio-wechat__status-icon--warn'">
<i :class="status.has_token ? 'fa fas fa-key' : 'fa fas fa-key'"></i>
</div>
<div class="aksrt-wechat__status-info">
<div class="aksrt-wechat__status-label">Access Token</div>
<div class="aksrt-wechat__status-value">{{ status.has_token ? '已获取' : '未获取' }}</div>
<div class="jio-wechat__status-info">
<div class="jio-wechat__status-label">Access Token</div>
<div class="jio-wechat__status-value">{{ status.has_token ? '已获取' : '未获取' }}</div>
</div>
</div>
<div class="aksrt-wechat__status-card">
<div class="aksrt-wechat__status-icon" :class="status.has_template ? 'aksrt-wechat__status-icon--ok' : 'aksrt-wechat__status-icon--warn'">
<div class="jio-wechat__status-card">
<div class="jio-wechat__status-icon" :class="status.has_template ? 'jio-wechat__status-icon--ok' : 'jio-wechat__status-icon--warn'">
<i :class="status.has_template ? 'fa fas fa-file-alt' : 'fa fas fa-file-alt'"></i>
</div>
<div class="aksrt-wechat__status-info">
<div class="aksrt-wechat__status-label">模板 ID</div>
<div class="aksrt-wechat__status-value">{{ status.has_template ? '已配置' : '未配置' }}</div>
<div class="jio-wechat__status-info">
<div class="jio-wechat__status-label">模板 ID</div>
<div class="jio-wechat__status-value">{{ status.has_template ? '已配置' : '未配置' }}</div>
</div>
</div>
<div class="aksrt-wechat__status-card">
<div class="aksrt-wechat__status-icon aksrt-wechat__status-icon--info">
<div class="jio-wechat__status-card">
<div class="jio-wechat__status-icon jio-wechat__status-icon--info">
<i class="fa fas fa-users"></i>
</div>
<div class="aksrt-wechat__status-info">
<div class="aksrt-wechat__status-label">绑定用户</div>
<div class="aksrt-wechat__status-value">{{ status.bound_users || 0 }} 人</div>
<div class="jio-wechat__status-info">
<div class="jio-wechat__status-label">绑定用户</div>
<div class="jio-wechat__status-value">{{ status.bound_users || 0 }} 人</div>
</div>
</div>
</div>
<!-- 配置表单 -->
<div class="aksrt-wechat__form-card">
<div class="jio-wechat__form-card">
<h3>基本配置</h3>
<div class="aksrt-form-group">
<div class="jio-form-group">
<label>
<input type="checkbox" v-model="form.enabled"> 启用微信模板消息
</label>
</div>
<div class="aksrt-form-group">
<div class="jio-form-group">
<label>AppID</label>
<input v-model="form.app_id" type="text" class="aksrt-input" placeholder="微信公众号 AppID">
<input v-model="form.app_id" type="text" class="jio-input" placeholder="微信公众号 AppID">
</div>
<div class="aksrt-form-group">
<div class="jio-form-group">
<label>AppSecret</label>
<input v-model="form.app_secret" type="password" class="aksrt-input" placeholder="微信公众号 AppSecret">
<p class="aksrt-form-hint">保存后不会回显完整密钥</p>
<input v-model="form.app_secret" type="text" class="jio-input jio-secret-input" placeholder="微信公众号 AppSecret">
<p class="jio-form-hint">保存后不会回显完整密钥</p>
</div>
<div class="aksrt-form-group">
<div class="jio-form-group">
<label>模板 ID</label>
<input v-model="form.template_id" type="text" class="aksrt-input" placeholder="微信模板消息模板 ID">
<input v-model="form.template_id" type="text" class="jio-input" placeholder="微信模板消息模板 ID">
</div>
<div class="aksrt-form-actions">
<button class="aksrt-button aksrt-button--primary" :disabled="saving" @click="saveSettings">
<div class="jio-form-actions">
<button class="jio-button jio-button--primary" :disabled="saving" @click="saveSettings">
{{ saving ? '保存中...' : '保存设置' }}
</button>
</div>
</div>
<!-- 通知类型 -->
<div class="aksrt-wechat__form-card">
<div class="jio-wechat__form-card">
<h3>支持的通知类型</h3>
<p class="aksrt-form-hint">以下事件类型可触发微信模板消息推送(需用户绑定微信且开启对应订阅):</p>
<table class="aksrt-table aksrt-table--sm">
<p class="jio-form-hint">以下事件类型可触发微信模板消息推送(需用户绑定微信且开启对应订阅):</p>
<table class="jio-table jio-table--sm">
<thead>
<tr>
<th>事件类型</th>
@@ -87,7 +87,7 @@
<td><code>{{ evt.key }}</code></td>
<td>{{ evt.desc }}</td>
<td>
<span class="aksrt-badge" :class="evt.ready ? 'aksrt-badge--success' : 'aksrt-badge--warning'">
<span class="jio-badge" :class="evt.ready ? 'jio-badge--success' : 'jio-badge--warning'">
{{ evt.ready ? '可用' : '待接入' }}
</span>
</td>
@@ -192,13 +192,13 @@ export default {
</script>
<style scoped>
.aksrt-wechat__status {
.jio-wechat__status {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.aksrt-wechat__status-card {
.jio-wechat__status-card {
display: flex;
align-items: center;
gap: 12px;
@@ -207,7 +207,7 @@ export default {
border-radius: 8px;
border: 1px solid #e8e8e8;
}
.aksrt-wechat__status-icon {
.jio-wechat__status-icon {
width: 40px;
height: 40px;
border-radius: 8px;
@@ -216,40 +216,40 @@ export default {
justify-content: center;
font-size: 16px;
}
.aksrt-wechat__status-icon--ok {
.jio-wechat__status-icon--ok {
background: #d4edda;
color: #155724;
}
.aksrt-wechat__status-icon--warn {
.jio-wechat__status-icon--warn {
background: #fff3cd;
color: #856404;
}
.aksrt-wechat__status-icon--info {
.jio-wechat__status-icon--info {
background: #d1ecf1;
color: #0c5460;
}
.aksrt-wechat__status-value {
.jio-wechat__status-value {
font-size: 18px;
font-weight: 600;
color: var(--aksrt-color-text);
color: var(--jio-color-text);
}
.aksrt-wechat__status-label {
.jio-wechat__status-label {
font-size: 12px;
color: #888;
}
.aksrt-wechat__form-card {
.jio-wechat__form-card {
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 24px;
margin-bottom: 16px;
}
.aksrt-wechat__form-card h3 {
.jio-wechat__form-card h3 {
margin: 0 0 16px;
font-size: 16px;
font-weight: 600;
}
.aksrt-form-hint {
.jio-form-hint {
font-size: 12px;
color: #888;
margin-top: 4px;
@@ -1,16 +1,16 @@
<template>
<div class="aksrt-sortable__item">
<div class="aksrt-sortable__head" v-bind="headAttrs">
<AksSortHandle @click.stop />
<span class="aksrt-sortable__index">{{ index + 1 }}</span>
<div class="aksrt-sortable__main">
<div class="aksrt-sortable__title-row">
<span class="aksrt-sortable__title">{{ category.cat_name || '未命名分类' }}</span>
<div class="jio-sortable__item">
<div class="jio-sortable__head" v-bind="headAttrs">
<JioSortHandle @click.stop />
<span class="jio-sortable__index">{{ index + 1 }}</span>
<div class="jio-sortable__main">
<div class="jio-sortable__title-row">
<span class="jio-sortable__title">{{ category.cat_name || '未命名分类' }}</span>
<n-tag size="small" :bordered="false">{{ category.items.length }} 枚</n-tag>
</div>
<span class="aksrt-sortable__desc">{{ previewNames || '该分类暂无条目' }}</span>
<span class="jio-sortable__desc">{{ previewNames || '该分类暂无条目' }}</span>
</div>
<div class="aksrt-sortable__actions">
<div class="jio-sortable__actions">
<n-popconfirm @positive-click="emit('remove')">
<template #trigger>
<n-button size="tiny" quaternary type="error" title="删除分类" @click.stop>
@@ -20,17 +20,17 @@
删除分类「{{ category.cat_name || '未命名分类' }}」及其全部条目?
</n-popconfirm>
</div>
<i class="fa fas aksrt-sortable__toggle" :class="open ? 'fa-chevron-down' : 'fa-chevron-right'" aria-hidden="true" />
<i class="fa fas jio-sortable__toggle" :class="open ? 'fa-chevron-down' : 'fa-chevron-right'" aria-hidden="true" />
</div>
<div v-if="open" class="aksrt-sortable__body">
<n-form label-placement="left" label-width="96" class="aksrt-form">
<div v-if="open" class="jio-sortable__body">
<n-form label-placement="left" label-width="96" class="jio-form">
<n-form-item label="分类名称">
<n-input v-model:value="category.cat_name" placeholder="如:成长" style="max-width: 320px;" />
</n-form-item>
</n-form>
<div v-if="!category.items.length" class="aksrt-sortable__empty">
<div v-if="!category.items.length" class="jio-sortable__empty">
<i class="fa fas fa-medal" aria-hidden="true" />
<span>该分类暂无条目</span>
</div>
@@ -39,7 +39,7 @@
v-model="category.items"
v-bind="SORTABLE_LIST_ATTRS"
:item-key="rowKey"
class="aksrt-sortable aksrt-sortable--compact"
class="jio-sortable jio-sortable--compact"
>
<template #item="{ element, index: ii }">
<MedalItemCard
@@ -62,7 +62,7 @@
/** 徽章分类卡片:行头显示条目数与名称预览,展开后编辑分类名并拖拽排序条目。 */
import { computed } from 'vue'
import draggable from 'vuedraggable'
import AksSortHandle from '@/components/form/AksSortHandle.vue'
import JioSortHandle from '@/components/form/JioSortHandle.vue'
import { SORTABLE_LIST_ATTRS } from '@/components/form/sortable-options.js'
import { useCollapsibleItems } from '@/components/form/useCollapsibleItems.js'
import { emptyItem, rowKey } from './badge-model.js'
@@ -15,7 +15,7 @@
<img :src="src" :alt="`预设图标 ${i + 1}`" />
</button>
</div>
<AksMediaField
<JioMediaField
:model-value="modelValue"
placeholder="或从媒体库选择自定义图标"
@update:model-value="emit('update:modelValue', $event)"
@@ -25,7 +25,7 @@
<script setup>
/** 勋章图标选择:主题预设 SVG 一键选用,或走媒体库 / 直接填 URL。 */
import AksMediaField from '@/components/form/AksMediaField.vue'
import JioMediaField from '@/components/form/JioMediaField.vue'
import { MEDAL_PRESET_ICONS } from '@/schema/sections/users/badge.js'
defineProps({
@@ -55,9 +55,9 @@ const emit = defineEmits(['update:modelValue'])
justify-content: center;
height: 36px;
padding: 0;
border: 1px solid var(--aksrt-color-border, #E2E8E3);
border: 1px solid var(--jio-color-border, #E2E8E3);
border-radius: 8px;
background: var(--aksrt-color-surface, #fff);
background: var(--jio-color-surface, #fff);
cursor: pointer;
transition: border-color .15s ease, background-color .15s ease;
}
@@ -70,13 +70,13 @@ const emit = defineEmits(['update:modelValue'])
.medal-icon-picker__preset:hover,
.medal-icon-picker__preset:focus-visible {
border-color: var(--aksrt-color-primary, #18A058);
border-color: var(--jio-color-primary, #18A058);
outline: none;
}
.medal-icon-picker__preset.is-active {
border-color: var(--aksrt-color-primary, #18A058);
background: var(--aksrt-color-primary-soft, #EAF7EF);
box-shadow: 0 0 0 1px var(--aksrt-color-primary, #18A058);
border-color: var(--jio-color-primary, #18A058);
background: var(--jio-color-primary-soft, #EAF7EF);
box-shadow: 0 0 0 1px var(--jio-color-primary, #18A058);
}
</style>
@@ -1,31 +1,31 @@
<template>
<div class="aksrt-sortable__item">
<div class="aksrt-sortable__head" v-bind="headAttrs">
<AksSortHandle @click.stop />
<span class="aksrt-sortable__icon">
<div class="jio-sortable__item">
<div class="jio-sortable__head" v-bind="headAttrs">
<JioSortHandle @click.stop />
<span class="jio-sortable__icon">
<img v-if="item.icon" :src="item.icon" alt="" />
<i v-else class="fa fas fa-medal" aria-hidden="true" />
</span>
<div class="aksrt-sortable__main">
<div class="aksrt-sortable__title-row">
<span class="aksrt-sortable__title">{{ item.name || '未命名徽章' }}</span>
<div class="jio-sortable__main">
<div class="jio-sortable__title-row">
<span class="jio-sortable__title">{{ item.name || '未命名徽章' }}</span>
<n-tag size="small" :bordered="false" :type="item.get_type === 'manually_add' ? 'default' : 'info'">
{{ conditionSummary(item) }}
</n-tag>
<n-tag v-if="!item.slug" size="small" type="warning" :bordered="false">缺少 slug</n-tag>
</div>
<span class="aksrt-sortable__desc">{{ item.slug || '—' }}{{ item.desc ? ` · ${item.desc}` : '' }}</span>
<span class="jio-sortable__desc">{{ item.slug || '—' }}{{ item.desc ? ` · ${item.desc}` : '' }}</span>
</div>
<div class="aksrt-sortable__actions">
<div class="jio-sortable__actions">
<n-button size="tiny" quaternary type="error" title="删除条目" @click.stop="emit('remove')">
<i class="fa fas fa-trash-alt" aria-hidden="true" />
</n-button>
</div>
<i class="fa fas aksrt-sortable__toggle" :class="open ? 'fa-chevron-down' : 'fa-chevron-right'" aria-hidden="true" />
<i class="fa fas jio-sortable__toggle" :class="open ? 'fa-chevron-down' : 'fa-chevron-right'" aria-hidden="true" />
</div>
<div v-if="open" class="aksrt-sortable__body">
<n-form label-placement="left" label-width="96" class="aksrt-form medal-item-card__form">
<div v-if="open" class="jio-sortable__body">
<n-form label-placement="left" label-width="96" class="jio-form medal-item-card__form">
<n-grid cols="1 m:2" :x-gap="16" responsive="screen">
<n-form-item-gi label="名称">
<n-input v-model:value="item.name" placeholder="如:签到达人" />
@@ -59,7 +59,7 @@
<script setup>
/** 徽章条目卡片:行头摘要(图标 / 名称 / 条件),展开后编辑字段。条目对象由父级直接持有并原地修改。 */
import AksSortHandle from '@/components/form/AksSortHandle.vue'
import JioSortHandle from '@/components/form/JioSortHandle.vue'
import { MEDAL_GET_TYPES } from '@/schema/sections/users/badge.js'
import { conditionSummary } from './badge-model.js'
import MedalIconPicker from './MedalIconPicker.vue'
@@ -1,6 +1,6 @@
<template>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<n-card class="jio-section-card">
<div class="jio-section-header">
<h3>预设回复</h3>
<n-space>
<n-button size="small" :disabled="saving || !reorderable" :loading="savingOrder" @click="saveOrder">
@@ -21,17 +21,17 @@
v-model="rows"
v-bind="SORTABLE_LIST_ATTRS"
item-key="id"
class="aksrt-sortable"
class="jio-sortable"
>
<template #item="{ element, index }">
<div class="aksrt-sortable__item">
<div class="aksrt-sortable__head">
<AksSortHandle />
<span class="aksrt-sortable__index">{{ index + 1 }}</span>
<div class="aksrt-sortable__main">
<span class="aksrt-ticket-prereply__content">{{ element.content }}</span>
<div class="jio-sortable__item">
<div class="jio-sortable__head">
<JioSortHandle />
<span class="jio-sortable__index">{{ index + 1 }}</span>
<div class="jio-sortable__main">
<span class="jio-ticket-prereply__content">{{ element.content }}</span>
</div>
<div class="aksrt-sortable__actions">
<div class="jio-sortable__actions">
<n-button size="tiny" type="primary" quaternary @click="openEdit(element)">编辑</n-button>
<n-button size="tiny" type="error" quaternary @click="handleDelete(element)">删除</n-button>
</div>
@@ -74,7 +74,7 @@ import { computed, onMounted, ref } from 'vue'
import { NButton, NSpace, useDialog, useMessage } from 'naive-ui'
import draggable from 'vuedraggable'
import api from '@/api'
import AksSortHandle from '@/components/form/AksSortHandle.vue'
import JioSortHandle from '@/components/form/JioSortHandle.vue'
import { SORTABLE_LIST_ATTRS } from '@/components/form/sortable-options.js'
const message = useMessage()
@@ -193,8 +193,8 @@ defineExpose({ reload: load })
</script>
<style scoped>
.aksrt-ticket-prereply__content {
color: var(--aksrt-color-text, #1F2923);
.jio-ticket-prereply__content {
color: var(--jio-color-text, #1F2923);
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
@@ -1,6 +1,6 @@
<template>
<n-card class="aksrt-section-card aksrt-ticket-status-panel">
<div class="aksrt-section-header">
<n-card class="jio-section-card jio-ticket-status-panel">
<div class="jio-section-header">
<h3>工单状态</h3>
<n-button size="small" type="primary" :disabled="saving" @click="openCreate">新增状态</n-button>
</div>
@@ -24,13 +24,13 @@
v-model:show="visible"
:title="editing ? '编辑状态' : '新增状态'"
preset="card"
class="aksrt-status-modal"
class="jio-status-modal"
style="width: 460px; max-width: 92vw;"
:mask-closable="false"
>
<div class="aksrt-status-preview" aria-live="polite">
<span class="aksrt-status-preview__label">预览</span>
<span class="aksrt-ticket-badge" :style="{ '--aksrt-ticket-color': previewColor }">
<div class="jio-status-preview" aria-live="polite">
<span class="jio-status-preview__label">预览</span>
<span class="jio-ticket-badge" :style="{ '--jio-ticket-color': previewColor }">
{{ form.name.trim() || '状态名称' }}
</span>
</div>
@@ -40,30 +40,30 @@
<n-input v-model:value="form.name" placeholder="如:处理中" maxlength="60" show-count />
</n-form-item>
<n-form-item label="标识" path="status_key">
<div class="aksrt-status-key">
<div class="jio-status-key">
<n-input
v-model:value="form.status_key"
class="aksrt-status-key__input"
class="jio-status-key__input"
:placeholder="editing ? '' : '留空则按名称自动生成'"
maxlength="24"
show-count
:input-props="{ autocomplete: 'off', spellcheck: false }"
@blur="form.status_key = normalizeKey(form.status_key)"
/>
<span class="aksrt-ticket-status__hint">
<span class="jio-ticket-status__hint">
小写字母开头,仅含小写字母、数字与下划线(2–24 位)。修改后,使用该状态的工单会同步改为新标识。
</span>
</div>
</n-form-item>
<n-form-item label="状态颜色" path="color">
<div class="aksrt-status-color">
<div class="aksrt-status-color__swatches" role="radiogroup" aria-label="状态颜色">
<div class="jio-status-color">
<div class="jio-status-color__swatches" role="radiogroup" aria-label="状态颜色">
<button
v-for="swatch in SWATCHES"
:key="swatch"
type="button"
role="radio"
class="aksrt-status-color__swatch"
class="jio-status-color__swatch"
:class="{ 'is-active': currentHex === swatch }"
:style="{ '--swatch': swatch }"
:aria-checked="currentHex === swatch"
@@ -72,7 +72,7 @@
@click="form.color = swatch"
/>
<!-- 自定义色:未选中时显示色环,选中后显示当前色并带选中环。 -->
<AksColorPicker
<JioColorPicker
:model-value="previewColor"
@update:model-value="setColor"
>
@@ -81,7 +81,7 @@
:ref="setTriggerRef"
type="button"
role="radio"
class="aksrt-status-color__swatch aksrt-status-color__swatch--custom"
class="jio-status-color__swatch jio-status-color__swatch--custom"
:class="{ 'is-active': isCustomColor }"
:style="{ '--swatch': previewColor }"
:aria-checked="isCustomColor"
@@ -90,25 +90,25 @@
@click="onClick"
/>
</template>
</AksColorPicker>
</JioColorPicker>
</div>
<n-input
v-model:value="form.color"
class="aksrt-status-color__hex"
class="jio-status-color__hex"
maxlength="7"
placeholder="#6b7280"
@blur="form.color = toHex(form.color) || form.color.trim()"
>
<template #prefix>
<i class="aksrt-status-color__chip" :style="{ background: previewColor }" />
<i class="jio-status-color__chip" :style="{ background: previewColor }" />
</template>
</n-input>
</div>
</n-form-item>
<n-form-item label="完结状态" path="is_closed" :show-feedback="false">
<div class="aksrt-status-closed">
<div class="jio-status-closed">
<n-switch v-model:value="form.is_closed" />
<span class="aksrt-ticket-status__hint">开启后该状态视为终态,工单不可再流转</span>
<span class="jio-ticket-status__hint">开启后该状态视为终态,工单不可再流转</span>
</div>
</n-form-item>
</n-form>
@@ -126,7 +126,7 @@
import { computed, h, onMounted, ref } from 'vue'
import { NButton, NSpace, useDialog, useMessage } from 'naive-ui'
import api from '@/api'
import AksColorPicker from '@/components/form/AksColorPicker.vue'
import JioColorPicker from '@/components/form/JioColorPicker.vue'
import { toHex } from '@/utils/color.js'
const message = useMessage()
@@ -194,7 +194,7 @@ const currentHex = computed(() => toHex(form.value.color))
const previewColor = computed(() => currentHex.value || DEFAULT_COLOR)
const isCustomColor = computed(() => currentHex.value !== '' && !SWATCHES.includes(currentHex.value))
/** AksColorPicker 已归一化为 hex;清空(空串)时保留当前色。 */
/** JioColorPicker 已归一化为 hex;清空(空串)时保留当前色。 */
function setColor(value) {
const hex = toHex(value)
if (hex) form.value.color = hex
@@ -208,15 +208,15 @@ const columns = computed(() => [
minWidth: 160,
// 注意:原生元素用 h('span', ...) 时,文本必须作为 children 直接传入;
// 写成 () => row.name 的插槽函数对原生元素不生效,单元格会是空的。
render: (row) => h('span', { class: 'aksrt-ticket-badge', style: { '--aksrt-ticket-color': row.color } }, row.name),
render: (row) => h('span', { class: 'jio-ticket-badge', style: { '--jio-ticket-color': row.color } }, row.name),
},
{
title: '颜色',
key: 'color',
width: 140,
render: (row) => h('span', { class: 'aksrt-ticket-color-cell' }, [
h('i', { class: 'aksrt-ticket-color-dot', style: { background: row.color } }),
h('span', { class: 'aksrt-ticket-color-value' }, row.color),
render: (row) => h('span', { class: 'jio-ticket-color-cell' }, [
h('i', { class: 'jio-ticket-color-dot', style: { background: row.color } }),
h('span', { class: 'jio-ticket-color-value' }, row.color),
]),
},
{
@@ -235,7 +235,7 @@ const columns = computed(() => [
title: '标识',
key: 'status_key',
minWidth: 140,
render: (row) => h('span', { class: 'aksrt-ticket-key' }, row.status_key),
render: (row) => h('span', { class: 'jio-ticket-key' }, row.status_key),
},
{
title: '操作',
@@ -348,13 +348,13 @@ defineExpose({ reload: load })
(此前色块 <i> 因此没有尺寸,颜色列只剩色值文本。)
-->
<style>
.aksrt-ticket-status-panel .aksrt-ticket-color-cell {
.jio-ticket-status-panel .jio-ticket-color-cell {
display: inline-flex;
align-items: center;
gap: 8px;
}
.aksrt-ticket-status-panel .aksrt-ticket-color-dot {
.jio-ticket-status-panel .jio-ticket-color-dot {
display: inline-block;
width: 16px;
height: 16px;
@@ -363,21 +363,21 @@ defineExpose({ reload: load })
flex: 0 0 auto;
}
.aksrt-ticket-status-panel .aksrt-ticket-color-value,
.aksrt-ticket-status-panel .aksrt-ticket-key {
.jio-ticket-status-panel .jio-ticket-color-value,
.jio-ticket-status-panel .jio-ticket-key {
font-family: var(--n-font-family-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
font-size: 12px;
color: var(--aksrt-color-muted);
color: var(--jio-color-muted);
}
/* 徽章与前台 assets/css/routes/tickets.css 的 .aksrt-ticket-badge 同构;表格与弹窗预览共用。 */
.aksrt-ticket-status-panel .aksrt-ticket-badge,
.aksrt-status-modal .aksrt-ticket-badge {
/* 徽章与前台 assets/css/routes/tickets.css 的 .jio-ticket-badge 同构;表格与弹窗预览共用。 */
.jio-ticket-status-panel .jio-ticket-badge,
.jio-status-modal .jio-ticket-badge {
display: inline-block;
padding: 2px 10px;
border-radius: 999px;
background: color-mix(in srgb, var(--aksrt-ticket-color, #6b7280) 16%, transparent);
color: var(--aksrt-ticket-color, #6b7280);
background: color-mix(in srgb, var(--jio-ticket-color, #6b7280) 16%, transparent);
color: var(--jio-ticket-color, #6b7280);
font-size: 12px;
font-weight: 600;
line-height: 1.6;
@@ -387,24 +387,24 @@ defineExpose({ reload: load })
<style scoped>
/* —— 弹窗:顶部实时预览 —— */
.aksrt-status-preview {
.jio-status-preview {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 18px;
padding: 14px 16px;
border: 1px dashed var(--aksrt-color-border);
border: 1px dashed var(--jio-color-border);
border-radius: 8px;
background: rgba(0, 0, 0, 0.02);
}
.aksrt-status-preview__label {
color: var(--aksrt-color-muted);
.jio-status-preview__label {
color: var(--jio-color-muted);
font-size: 12px;
}
.aksrt-status-preview .aksrt-ticket-badge {
.jio-status-preview .jio-ticket-badge {
max-width: 70%;
overflow: hidden;
text-overflow: ellipsis;
@@ -412,20 +412,20 @@ defineExpose({ reload: load })
}
/* —— 弹窗:颜色选择(预设色板 + 自定义 + 十六进制输入) —— */
.aksrt-status-color {
.jio-status-color {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
.aksrt-status-color__swatches {
.jio-status-color__swatches {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.aksrt-status-color__swatch {
.jio-status-color__swatch {
position: relative;
width: 28px;
height: 28px;
@@ -438,21 +438,21 @@ defineExpose({ reload: load })
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.aksrt-status-color__swatch:hover {
.jio-status-color__swatch:hover {
transform: scale(1.1);
}
.aksrt-status-color__swatch:focus-visible {
outline: 2px solid var(--aksrt-color-primary, #2271b1);
.jio-status-color__swatch:focus-visible {
outline: 2px solid var(--jio-color-primary, #2271b1);
outline-offset: 2px;
}
/* 选中态:白色内圈 + 同色外环,深浅主题都清晰。 */
.aksrt-status-color__swatch.is-active {
.jio-status-color__swatch.is-active {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--swatch);
}
.aksrt-status-color__swatch.is-active::after {
.jio-status-color__swatch.is-active::after {
content: '';
position: absolute;
left: 50%;
@@ -465,11 +465,11 @@ defineExpose({ reload: load })
}
/* 自定义:未选中时显示色环,提示「更多颜色」。 */
.aksrt-status-color__swatch--custom:not(.is-active) {
.jio-status-color__swatch--custom:not(.is-active) {
background: conic-gradient(#ef4444, #f59e0b, #eab308, #22c55e, #06b6d4, #3b82f6, #8b5cf6, #ec4899, #ef4444);
}
.aksrt-status-color__swatch--custom:not(.is-active)::after {
.jio-status-color__swatch--custom:not(.is-active)::after {
content: '+';
position: absolute;
inset: 5px;
@@ -484,12 +484,12 @@ defineExpose({ reload: load })
line-height: 1;
}
.aksrt-status-color__hex {
.jio-status-color__hex {
width: 160px;
font-family: var(--n-font-family-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
}
.aksrt-status-color__chip {
.jio-status-color__chip {
display: inline-block;
width: 14px;
height: 14px;
@@ -498,26 +498,26 @@ defineExpose({ reload: load })
}
/* —— 弹窗:标识 —— */
.aksrt-status-key {
.jio-status-key {
display: flex;
flex-direction: column;
gap: 6px;
width: 100%;
}
.aksrt-status-key__input {
.jio-status-key__input {
font-family: var(--n-font-family-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
}
/* —— 弹窗:完结开关 —— */
.aksrt-status-closed {
.jio-status-closed {
display: flex;
align-items: center;
gap: 10px;
}
.aksrt-ticket-status__hint {
color: var(--aksrt-color-muted);
.jio-ticket-status__hint {
color: var(--jio-color-muted);
font-size: 12px;
}
</style>
@@ -1,6 +1,6 @@
<template>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<n-card class="jio-section-card">
<div class="jio-section-header">
<h3>工单部门</h3>
<n-button size="small" type="primary" :disabled="saving" @click="openCreate">新增部门</n-button>
</div>
+4 -4
View File
@@ -12,7 +12,7 @@ function resolveComponent(page) {
// Log missing component for debugging — avoids silent fallback to DeferredPage
if (page.component && page.component !== 'DeferredPage') {
console.warn(
`[AKSRT Route] Component "${page.component}" not found in componentLoaders for route "${page.route}". Falling back to DeferredPage.`
`[Jio Route] Component "${page.component}" not found in componentLoaders for route "${page.route}". Falling back to DeferredPage.`
)
}
@@ -77,9 +77,9 @@ function createSectionRedirectRoutes() {
* (封禁/举报 as a sidebar entry lives in constants/sections/users.js → /users/ban.)
*/
const NATIVE_OPS_REDIRECTS = [
{ path: '/users/messages', title: '消息中心', nativeAdminPath: 'users.php?page=aksrt_messages' },
{ path: '/users/report', title: '禁封/举报', nativeAdminPath: 'users.php?page=aksrt_user_ban' },
{ path: '/users/tickets', title: '工单管理', nativeAdminPath: 'users.php?page=aksrt_tickets' },
{ path: '/users/messages', title: '消息中心', nativeAdminPath: 'users.php?page=jio_messages' },
{ path: '/users/report', title: '禁封/举报', nativeAdminPath: 'users.php?page=jio_user_ban' },
{ path: '/users/tickets', title: '工单管理', nativeAdminPath: 'users.php?page=jio_tickets' },
]
const routes = [
+1 -1
View File
@@ -2,7 +2,7 @@
* Schema Registry — central index for all admin section schemas.
*
* Each section's schema defines the form fields, groups, API endpoints,
* and default values for a settings page. The AksFormRenderer component
* and default values for a settings page. The JioFormRenderer component
* consumes these schemas to render settings pages automatically.
*
* Schema shape per section:
@@ -1,68 +0,0 @@
export const codeHighlightSchema = {
key: 'global.code-highlight',
title: '代码高亮',
icon: 'fa fas fa-code',
description: '代码块语法高亮主题、行号、最大高度等配置。',
defaults: {
highlight_kg: true,
highlight_hh: false,
highlight_btn: true,
highlight_maxheight: 400,
highlight_zt: 'enlighter',
highlight_dark_zt: 'monokai',
},
groups: [
{
key: 'basic',
title: '代码高亮设置',
icon: 'fa fas fa-pen',
fields: [
{ key: 'highlight_kg', label: '代码高亮开关', type: 'switch', default: true },
{ key: 'highlight_hh', label: '显示行号', type: 'switch', default: false },
{ key: 'highlight_btn', label: '显示扩展按钮', type: 'switch', default: true, help: '控制原始代码 / 信息 / 新窗口按钮的显示' },
{ key: 'highlight_maxheight', label: '最大高度(px,0=不限)', type: 'number', default: 400, min: 0, max: 2000, help: '代码块最大高度,超出滚动显示。0 表示不限制' },
],
},
{
key: 'theme',
title: '主题设置',
icon: 'fa fas fa-paint-brush',
fields: [
{
key: 'highlight_zt',
label: '日间默认主题',
type: 'select',
default: 'enlighter',
options: [
{ label: 'Enlighter', value: 'enlighter' },
{ label: 'Bootstrap 4', value: 'bootstrap4' },
{ label: 'Classic', value: 'classic' },
{ label: 'Beyond', value: 'beyond' },
{ label: 'Mowtwo', value: 'mowtwo' },
{ label: 'Eclipse', value: 'eclipse' },
{ label: 'Droide', value: 'droide' },
{ label: 'Minimal', value: 'minimal' },
{ label: 'Rowhammer', value: 'rowhammer' },
{ label: 'Godzilla', value: 'godzilla' },
{ label: 'Dracula', value: 'dracula' },
{ label: 'Atomic', value: 'atomic' },
{ label: 'Monokai', value: 'monokai' },
],
},
{
key: 'highlight_dark_zt',
label: '暗色默认主题',
type: 'select',
default: 'monokai',
options: [
{ label: 'Dracula', value: 'dracula' },
{ label: 'Atomic', value: 'atomic' },
{ label: 'Monokai', value: 'monokai' },
{ label: 'Enlighter', value: 'enlighter' },
{ label: 'Bootstrap 4', value: 'bootstrap4' },
],
},
],
},
],
}
@@ -1,13 +1,23 @@
const themeRoot = typeof window !== 'undefined' && window.aksrtAdminData?.themeUri
? String(window.aksrtAdminData.themeUri).replace(/\/$/, '')
const themeRoot = typeof window !== 'undefined' && window.jioAdminData?.themeUri
? String(window.jioAdminData.themeUri).replace(/\/$/, '')
: ''
const defaultThumbnail = themeRoot ? `${themeRoot}/assets/img/thumbnail.svg` : ''
const noticeColorOptions = [
{ label: '主题色', value: 'primary' },
{ label: '成功绿', value: 'success' },
{ label: '警告橙', value: 'warning' },
{ label: '危险红', value: 'danger' },
{ label: '信息蓝', value: 'info' },
{ label: '深紫', value: 'purple' },
{ label: '深灰', value: 'dark' },
]
export const commonSchema = {
key: 'global.common',
title: '常用功能',
icon: 'fa fas fa-cog',
description: '管理站点图像懒加载等常用基础配置。',
description: '图像懒加载、代码高亮与弹窗公告等全站常用配置。',
defaults: {
lazy_posts_thumb: true,
lazy_avatar: true,
@@ -18,6 +28,35 @@ export const commonSchema = {
lazy_other: true,
lazy_animation: 'blur',
thumbnail: defaultThumbnail,
highlight_kg: true,
highlight_hh: false,
highlight_btn: true,
highlight_maxheight: 400,
highlight_zt: 'enlighter',
highlight_dark_zt: 'monokai',
system_notice_enabled: true,
system_notice_policy: '',
system_notice_title: '',
system_notice_content: '',
system_notice_btn1_text: '',
system_notice_btn1_link: '',
system_notice_btn1_color: 'primary',
system_notice_btn2_text: '',
system_notice_btn2_link: '',
system_notice_btn2_color: 'primary',
system_notice_btn3_text: '',
system_notice_btn3_link: '',
system_notice_btn3_color: 'primary',
system_notice_btn4_text: '',
system_notice_btn4_link: '',
system_notice_btn4_color: 'primary',
system_notice_size: '',
system_notice_radius: false,
system_notice_class: '',
system_notice_title_icon: '',
system_notice_title_class: '',
system_notice_title_style: '',
system_notice_expires: 24,
},
groups: [
{
@@ -53,5 +92,123 @@ export const commonSchema = {
{ key: 'lazy_other', label: '其它图片懒加载', type: 'switch', default: true },
],
},
{
key: 'code-highlight',
title: '代码高亮',
icon: 'fa fas fa-code',
fields: [
{ key: 'highlight_kg', label: '代码高亮开关', type: 'switch', default: true },
{ key: 'highlight_hh', label: '显示行号', type: 'switch', default: false, dependsOn: { key: 'highlight_kg', value: true } },
{ key: 'highlight_btn', label: '显示扩展按钮', type: 'switch', default: true, dependsOn: { key: 'highlight_kg', value: true }, help: '控制原始代码 / 信息 / 新窗口按钮的显示' },
{ key: 'highlight_maxheight', label: '最大高度(px,0=不限)', type: 'number', default: 400, min: 0, max: 2000, dependsOn: { key: 'highlight_kg', value: true }, help: '代码块最大高度,超出滚动显示。0 表示不限制' },
{
key: 'highlight_zt',
label: '日间默认主题',
type: 'select',
default: 'enlighter',
dependsOn: { key: 'highlight_kg', value: true },
options: [
{ label: 'Enlighter', value: 'enlighter' },
{ label: 'Bootstrap 4', value: 'bootstrap4' },
{ label: 'Classic', value: 'classic' },
{ label: 'Beyond', value: 'beyond' },
{ label: 'Mowtwo', value: 'mowtwo' },
{ label: 'Eclipse', value: 'eclipse' },
{ label: 'Droide', value: 'droide' },
{ label: 'Minimal', value: 'minimal' },
{ label: 'Rowhammer', value: 'rowhammer' },
{ label: 'Godzilla', value: 'godzilla' },
{ label: 'Dracula', value: 'dracula' },
{ label: 'Atomic', value: 'atomic' },
{ label: 'Monokai', value: 'monokai' },
],
},
{
key: 'highlight_dark_zt',
label: '暗色默认主题',
type: 'select',
default: 'monokai',
dependsOn: { key: 'highlight_kg', value: true },
options: [
{ label: 'Dracula', value: 'dracula' },
{ label: 'Atomic', value: 'atomic' },
{ label: 'Monokai', value: 'monokai' },
{ label: 'Enlighter', value: 'enlighter' },
{ label: 'Bootstrap 4', value: 'bootstrap4' },
],
},
],
},
{
key: 'system-notice',
title: '弹窗通知',
icon: 'fa fas fa-bullhorn',
fields: [
{ key: 'system_notice_enabled', label: '启用系统公告', type: 'switch', default: true },
{
key: 'system_notice_policy',
label: '显示对象',
type: 'select',
default: '',
options: [
{ label: '所有访客', value: '' },
{ label: '仅登录用户', value: 'signin' },
{ label: '仅 VIP 会员', value: 'vip' },
{ label: '仅非 VIP', value: 'free' },
{ label: '仅认证用户', value: 'auth' },
],
dependsOn: { key: 'system_notice_enabled', value: true },
},
{ key: 'system_notice_title', label: '公告标题', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_content', label: '公告内容', type: 'textarea', default: '', rows: 5, dependsOn: { key: 'system_notice_enabled', value: true }, help: '支持 HTML 标签' },
{ key: 'system_notice_btn1_text', label: '按钮 1 文案', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn1_link', label: '按钮 1 链接', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true }, help: '可选,填写后按钮变为链接' },
{ key: 'system_notice_btn1_color', label: '按钮 1 颜色', type: 'select', default: 'primary', options: noticeColorOptions, dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn2_text', label: '按钮 2 文案', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn2_link', label: '按钮 2 链接', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn2_color', label: '按钮 2 颜色', type: 'select', default: 'primary', options: noticeColorOptions, dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn3_text', label: '按钮 3 文案', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn3_link', label: '按钮 3 链接', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn3_color', label: '按钮 3 颜色', type: 'select', default: 'primary', options: noticeColorOptions, dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn4_text', label: '按钮 4 文案', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn4_link', label: '按钮 4 链接', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn4_color', label: '按钮 4 颜色', type: 'select', default: 'primary', options: noticeColorOptions, dependsOn: { key: 'system_notice_enabled', value: true } },
{
key: 'system_notice_size',
label: '弹窗尺寸',
type: 'select',
default: '',
options: [
{ label: '默认', value: '' },
{ label: '迷你', value: 'modal-mini' },
{ label: '小号', value: 'modal-sm' },
{ label: '大号', value: 'modal-lg' },
],
dependsOn: { key: 'system_notice_enabled', value: true },
},
{ key: 'system_notice_radius', label: '按钮圆角', type: 'switch', default: false, dependsOn: { key: 'system_notice_enabled', value: true }, help: '开启后弹窗按钮使用圆角样式' },
{ key: 'system_notice_class', label: '公告样式类', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_title_icon', label: '标题图标', type: 'icon', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{
key: 'system_notice_title_class',
label: '标题背景配色',
type: 'select',
default: '',
dependsOn: { key: 'system_notice_enabled', value: true },
options: [
{ label: '无', value: '' },
{ label: '炫彩黄', value: 'jb-yellow' },
{ label: '炫彩蓝', value: 'jb-blue' },
{ label: '炫彩绿', value: 'jb-green' },
{ label: '炫彩紫', value: 'jb-purple' },
{ label: '炫彩红', value: 'jb-red' },
{ label: '炫彩橙', value: 'jb-orange' },
],
help: '弹窗标题区域的背景配色方案',
},
{ key: 'system_notice_title_style', label: '标题样式', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_expires', label: '弹窗周期(小时)', type: 'number', default: 24, min: 0, max: 720, dependsOn: { key: 'system_notice_enabled', value: true }, help: '0 表示永不过期' },
],
},
],
}
@@ -37,7 +37,7 @@ export const i18nSchema = {
],
dependsOn: { key: 'i18n_enabled', value: true },
},
{ key: 'i18n_auto_detect', label: '自动检测浏览器语言', type: 'switch', default: true, dependsOn: { key: 'i18n_enabled', value: true }, help: '基于浏览器 Accept-Language 自动切换(AKSRT 方案,区别于 legacy IP 定位方案)' },
{ key: 'i18n_auto_detect', label: '自动检测浏览器语言', type: 'switch', default: true, dependsOn: { key: 'i18n_enabled', value: true }, help: '基于浏览器 Accept-Language 自动切换(Jio 方案,区别于 legacy IP 定位方案)' },
],
},
{
@@ -12,8 +12,6 @@ import { tabbarSchema } from './tabbar.js'
import { i18nSchema } from './i18n.js'
import { customCodeSchema } from './custom-code.js'
import { seoSchema } from './seo.js'
import { codeHighlightSchema } from './code-highlight.js'
import { systemNoticeSchema } from './system-notice.js'
import { shareSchema } from './share.js'
export const globalSchemas = {
@@ -26,7 +24,5 @@ export const globalSchemas = {
i18n: i18nSchema,
'custom-code': customCodeSchema,
seo: seoSchema,
'code-highlight': codeHighlightSchema,
'system-notice': systemNoticeSchema,
share: shareSchema,
}
@@ -61,7 +61,7 @@ export const layoutSchema = {
theme_skin: 'f04494',
dark_theme_skin_custom: '',
close_backdrop: false,
theme_main_radius: 12,
theme_main_radius: 8,
container_max_width: 1200,
sidebar_enabled: true,
sidebar_position: 'right',
@@ -147,7 +147,7 @@ export const layoutSchema = {
},
{ key: 'dark_theme_skin_custom', label: '夜间模式主题色', type: 'color', default: '', help: '在此可单独设置夜间模式的高亮色,清空此处则使用上方默认高亮色' },
{ key: 'close_backdrop', label: '全局关闭背景高斯模糊', type: 'switch', default: false, help: '在支持的浏览器上部分UI(例如导航栏)会显示背景高斯模糊的特效,但此功能比较消耗浏览器性能。同时系统会自动检测用户浏览器性能情况,如果性能较差,将自动关闭此特效,以提供更流畅的体验' },
{ key: 'theme_main_radius', label: '卡片圆角', type: 'spinner', default: 12, min: 0, max: 30, step: 1, unit: 'PX', description: '页面卡片的圆角尺寸' },
{ key: 'theme_main_radius', label: '卡片圆角', type: 'spinner', default: 8, min: 0, max: 30, step: 1, unit: 'PX', description: '页面卡片的圆角尺寸' },
],
},
{
+14 -10
View File
@@ -1,14 +1,19 @@
const themeRoot = typeof window !== 'undefined' && window.jioAdminData?.themeUri
? String(window.jioAdminData.themeUri).replace(/\/$/, '')
: ''
const defaultLogo = themeRoot ? `${themeRoot}/assets/img/logo.png` : ''
const defaultFavicon = themeRoot ? `${themeRoot}/assets/img/favicon.png` : ''
export const logoSchema = {
key: 'global.logo',
title: 'LOGO & 品牌',
icon: 'fa fas fa-image',
description: '管理站点 Logo、站点图标和品牌标识。',
defaults: {
site_logo: '',
site_logo_dark: '',
site_favicon: '',
site_icon: '',
logo_width: 150,
site_logo: defaultLogo,
site_logo_dark: defaultLogo,
site_favicon: defaultFavicon,
site_icon: defaultFavicon,
logo_text: '',
},
groups: [
@@ -17,9 +22,8 @@ export const logoSchema = {
title: 'Logo 设置',
icon: 'fa fas fa-image',
fields: [
{ key: 'site_logo', label: '站点 Logo', type: 'media', help: '建议尺寸 300x80,支持 PNG/SVG 透明背景' },
{ key: 'site_logo_dark', label: '暗色模式 Logo', type: 'media', help: '暗色模式下显示的 Logo,留空则使用默认 Logo' },
{ key: 'logo_width', label: 'Logo 宽度 (px)', type: 'number', min: 50, max: 400, step: 10, help: '导航栏中 Logo 显示宽度' },
{ key: 'site_logo', label: '站点 Logo', type: 'media', default: defaultLogo, help: '建议尺寸 300x80,支持 PNG/SVG 透明背景' },
{ key: 'site_logo_dark', label: '暗色模式 Logo', type: 'media', default: defaultLogo, help: '暗色模式下显示的 Logo,留空则使用默认 Logo' },
{ key: 'logo_text', label: 'Logo 文字', type: 'input', placeholder: '当无 Logo 图片时显示的文字', help: '同时作为首页标题和 alt 文本使用' },
],
},
@@ -28,8 +32,8 @@ export const logoSchema = {
title: '站点图标',
icon: 'fa fas fa-id-badge',
fields: [
{ key: 'site_favicon', label: '浏览器图标 (Favicon)', type: 'media', help: '建议 32x32 或 16x16 的 ICO/PNG 文件' },
{ key: 'site_icon', label: '移动端图标', type: 'media', help: '建议 192x192 的 PNG,用于移动端添加到桌面' },
{ key: 'site_favicon', label: '浏览器图标 (Favicon)', type: 'media', default: defaultFavicon, help: '建议 32x32 或 16x16 的 ICO/PNG 文件' },
{ key: 'site_icon', label: '移动端图标', type: 'media', default: defaultFavicon, help: '建议 192x192 的 PNG,用于移动端添加到桌面' },
],
},
],
@@ -20,7 +20,7 @@ export const searchSchema = {
search_custom_key: ['title', 'content', 'excerpt', 'category', 'post_tag'],
search_facets: {},
search_ms_s: false,
search_ms_opt: { api_url: 'http://127.0.0.1:7700', api_key: '', index_prefix: 'aksrt', synonyms: [{ key: 'wordpress', val: 'wp,wordp,wordpre' }] },
search_ms_opt: { api_url: 'http://127.0.0.1:7700', api_key: '', index_prefix: 'jio', synonyms: [{ key: 'wordpress', val: 'wp,wordp,wordpre' }] },
},
groups: [
{
@@ -98,10 +98,10 @@ export const searchSchema = {
icon: 'fa fas fa-bolt',
fields: [
{ key: 'search_ms_s', label: '启用 Meilisearch', type: 'switch', default: false },
{ key: 'search_ms_opt', label: 'Meilisearch 参数', type: 'fieldset', sanitizeType: 'array', dependsOn: { key: 'search_ms_s', value: true }, default: { api_url: 'http://127.0.0.1:7700', api_key: '', index_prefix: 'aksrt', synonyms: [{ key: 'wordpress', val: 'wp,wordp,wordpre' }] }, fields: [
{ key: 'search_ms_opt', label: 'Meilisearch 参数', type: 'fieldset', sanitizeType: 'array', dependsOn: { key: 'search_ms_s', value: true }, default: { api_url: 'http://127.0.0.1:7700', api_key: '', index_prefix: 'jio', synonyms: [{ key: 'wordpress', val: 'wp,wordp,wordpre' }] }, fields: [
{ key: 'api_url', label: 'API 地址', type: 'input', default: 'http://127.0.0.1:7700' },
{ key: 'api_key', label: 'API 密钥', type: 'input', default: '' },
{ key: 'index_prefix', label: '索引前缀', type: 'input', default: 'aksrt' },
{ key: 'index_prefix', label: '索引前缀', type: 'input', default: 'jio' },
{ key: 'synonyms', label: '同义词', type: 'repeater', default: [{ key: 'wordpress', val: 'wp,wordp,wordpre' }], addText: '添加同义词', fields: [
{ key: 'key', label: '关键词', type: 'input', default: '' },
{ key: 'val', label: '同义词', type: 'textarea', default: '' },
@@ -1,5 +1,5 @@
const themeRoot = typeof window !== 'undefined' && window.aksrtAdminData?.themeUri
? String(window.aksrtAdminData.themeUri).replace(/\/$/, '')
const themeRoot = typeof window !== 'undefined' && window.jioAdminData?.themeUri
? String(window.jioAdminData.themeUri).replace(/\/$/, '')
: ''
const shareOptions = [
@@ -1,149 +0,0 @@
export const systemNoticeSchema = {
key: 'global.system-notice',
title: '弹窗通知',
icon: 'fa fas fa-bullhorn',
description: '全站弹窗公告配置,支持按用户身份定向、多按钮组、弹窗尺寸与过期周期。',
defaults: {
system_notice_enabled: true,
system_notice_policy: '',
system_notice_title: '',
system_notice_content: '',
system_notice_btn1_text: '',
system_notice_btn1_link: '',
system_notice_btn1_color: 'primary',
system_notice_btn2_text: '',
system_notice_btn2_link: '',
system_notice_btn2_color: 'primary',
system_notice_btn3_text: '',
system_notice_btn3_link: '',
system_notice_btn3_color: 'primary',
system_notice_btn4_text: '',
system_notice_btn4_link: '',
system_notice_btn4_color: 'primary',
system_notice_size: '',
system_notice_radius: false,
system_notice_class: '',
system_notice_title_icon: '',
system_notice_title_class: '',
system_notice_title_style: '',
system_notice_expires: 24,
},
groups: [
{
key: 'basic',
title: '公告设置',
icon: 'fa fas fa-bullhorn',
fields: [
{ key: 'system_notice_enabled', label: '启用系统公告', type: 'switch', default: true },
{
key: 'system_notice_policy',
label: '显示对象',
type: 'select',
default: '',
options: [
{ label: '所有访客', value: '' },
{ label: '仅登录用户', value: 'signin' },
{ label: '仅 VIP 会员', value: 'vip' },
{ label: '仅非 VIP', value: 'free' },
{ label: '仅认证用户', value: 'auth' },
],
dependsOn: { key: 'system_notice_enabled', value: true },
},
{ key: 'system_notice_title', label: '公告标题', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_content', label: '公告内容', type: 'textarea', default: '', rows: 5, dependsOn: { key: 'system_notice_enabled', value: true }, help: '支持 HTML 标签' },
],
},
{
key: 'buttons',
title: '按钮组(留空则不显示)',
icon: 'fa fas fa-th-large',
fields: [
{ key: 'system_notice_btn1_text', label: '按钮 1 文案', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn1_link', label: '按钮 1 链接', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true }, help: '可选,填写后按钮变为链接' },
{ key: 'system_notice_btn1_color', label: '按钮 1 颜色', type: 'select', default: 'primary', options: [
{ label: '主题色', value: 'primary' },
{ label: '成功绿', value: 'success' },
{ label: '警告橙', value: 'warning' },
{ label: '危险红', value: 'danger' },
{ label: '信息蓝', value: 'info' },
{ label: '深紫', value: 'purple' },
{ label: '深灰', value: 'dark' },
], dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn2_text', label: '按钮 2 文案', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn2_link', label: '按钮 2 链接', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn2_color', label: '按钮 2 颜色', type: 'select', default: 'primary', options: [
{ label: '主题色', value: 'primary' },
{ label: '成功绿', value: 'success' },
{ label: '警告橙', value: 'warning' },
{ label: '危险红', value: 'danger' },
{ label: '信息蓝', value: 'info' },
{ label: '深紫', value: 'purple' },
{ label: '深灰', value: 'dark' },
], dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn3_text', label: '按钮 3 文案', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn3_link', label: '按钮 3 链接', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn3_color', label: '按钮 3 颜色', type: 'select', default: 'primary', options: [
{ label: '主题色', value: 'primary' },
{ label: '成功绿', value: 'success' },
{ label: '警告橙', value: 'warning' },
{ label: '危险红', value: 'danger' },
{ label: '信息蓝', value: 'info' },
{ label: '深紫', value: 'purple' },
{ label: '深灰', value: 'dark' },
], dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn4_text', label: '按钮 4 文案', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn4_link', label: '按钮 4 链接', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_btn4_color', label: '按钮 4 颜色', type: 'select', default: 'primary', options: [
{ label: '主题色', value: 'primary' },
{ label: '成功绿', value: 'success' },
{ label: '警告橙', value: 'warning' },
{ label: '危险红', value: 'danger' },
{ label: '信息蓝', value: 'info' },
{ label: '深紫', value: 'purple' },
{ label: '深灰', value: 'dark' },
], dependsOn: { key: 'system_notice_enabled', value: true } },
],
},
{
key: 'style',
title: '样式设置',
icon: 'fa fas fa-paint-brush',
fields: [
{
key: 'system_notice_size',
label: '弹窗尺寸',
type: 'select',
default: '',
options: [
{ label: '默认', value: '' },
{ label: '迷你', value: 'modal-mini' },
{ label: '小号', value: 'modal-sm' },
{ label: '大号', value: 'modal-lg' },
],
dependsOn: { key: 'system_notice_enabled', value: true },
},
{ key: 'system_notice_radius', label: '按钮圆角', type: 'switch', default: false, dependsOn: { key: 'system_notice_enabled', value: true }, help: '开启后弹窗按钮使用圆角样式' },
{ key: 'system_notice_class', label: '公告样式类', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_title_icon', label: '标题图标', type: 'icon', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
{ key: 'system_notice_title_class', label: '标题背景配色', type: 'select', default: '', dependsOn: { key: 'system_notice_enabled', value: true }, options: [
{ label: '无', value: '' },
{ label: '炫彩黄', value: 'jb-yellow' },
{ label: '炫彩蓝', value: 'jb-blue' },
{ label: '炫彩绿', value: 'jb-green' },
{ label: '炫彩紫', value: 'jb-purple' },
{ label: '炫彩红', value: 'jb-red' },
{ label: '炫彩橙', value: 'jb-orange' },
], help: '弹窗标题区域的背景配色方案' },
{ key: 'system_notice_title_style', label: '标题样式', type: 'input', default: '', dependsOn: { key: 'system_notice_enabled', value: true } },
],
},
{
key: 'expire',
title: '过期设置',
icon: 'fa fas fa-clock',
fields: [
{ key: 'system_notice_expires', label: '弹窗周期(小时)', type: 'number', default: 24, min: 0, max: 720, dependsOn: { key: 'system_notice_enabled', value: true }, help: '0 表示永不过期' },
],
},
],
}
@@ -93,7 +93,7 @@ export const gatewaySchema = {
label: '',
type: 'content',
ui_only: true,
content: '<ul><li>以下收款接口,主题仅提供API接入服务,收款平台的可靠性请自行斟酌!</li><li>涉及到资金及信息安全,有相关执照的商家推荐使用官方接口</li><li>微信 / 支付宝接口可在「官方」与「易支付」之间选择;PayPal 与 Stripe 独立开关</li><li>异步回调由各网关验签,无需额外口令;回调地址固定为 /wp-json/aksrt/v1/payment/{gateway}/{notify|return}</li></ul>',
content: '<ul><li>以下收款接口,主题仅提供API接入服务,收款平台的可靠性请自行斟酌!</li><li>涉及到资金及信息安全,有相关执照的商家推荐使用官方接口</li><li>微信 / 支付宝接口可在「官方」与「易支付」之间选择;PayPal 与 Stripe 独立开关</li><li>异步回调由各网关验签,无需额外口令;回调地址固定为 /wp-json/jio/v1/payment/{gateway}/{notify|return}</li></ul>',
},
],
},
@@ -159,7 +159,7 @@ export const gatewaySchema = {
],
},
{ key: 'pay_sdk_custom_title_s', label: '自定义收款标题', type: 'switch', default: false, help: '默认为付款类型加网站名,如需自定义请开启' },
{ key: 'pay_sdk_custom_title', label: '自定义收款标题', type: 'input', default: '', dependsOn: { key: 'pay_sdk_custom_title_s', value: true }, help: '可使用变量:订单类型 %order_type%,例如:%order_type%-AKSRT;注意控制总长度,太长会导致部分接口报错' },
{ key: 'pay_sdk_custom_title', label: '自定义收款标题', type: 'input', default: '', dependsOn: { key: 'pay_sdk_custom_title_s', value: true }, help: '可使用变量:订单类型 %order_type%,例如:%order_type%-Jio;注意控制总长度,太长会导致部分接口报错' },
],
},
{
@@ -208,7 +208,7 @@ export const gatewaySchema = {
label: '',
type: 'content',
ui_only: true,
content: '<ul><li>支付宝公钥为必填项目(公钥错误可以成功付款,但会回调失败)</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/alipay/{notify|return}</li><li>电脑端按「电脑端收款」选择站内二维码或跳转;手机端开启 H5 时跳转,否则显示二维码</li></ul>',
content: '<ul><li>支付宝公钥为必填项目(公钥错误可以成功付款,但会回调失败)</li><li>回调地址固定为 /wp-json/jio/v1/payment/alipay/{notify|return}</li><li>电脑端按「电脑端收款」选择站内二维码或跳转;手机端开启 H5 时跳转,否则显示二维码</li></ul>',
},
{ key: 'alipay_appid', label: '当面付:APPID', type: 'input', default: '' },
{ key: 'alipay_privatekey', label: '当面付:应用私钥', type: 'textarea', sanitize_type: 'code', default: '', rows: 4, secret: true },
@@ -243,7 +243,7 @@ export const gatewaySchema = {
label: '',
type: 'content',
ui_only: true,
content: '<ul><li>微信企业支付:官方接口,商家可申请,有年费,申请较为复杂</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/wechatpay/{notify|return},请配置 Native 支付回调链接</li><li>电脑端显示站内二维码;手机端按下方开关选择 H5 跳转或微信内 JSAPI,未开启时同样显示二维码</li></ul>',
content: '<ul><li>微信企业支付:官方接口,商家可申请,有年费,申请较为复杂</li><li>回调地址固定为 /wp-json/jio/v1/payment/wechatpay/{notify|return},请配置 Native 支付回调链接</li><li>电脑端显示站内二维码;手机端按下方开关选择 H5 跳转或微信内 JSAPI,未开启时同样显示二维码</li></ul>',
},
{ key: 'wechat_merchantid', label: '商户号 PartnerID', type: 'input', default: '' },
{ key: 'wechat_appid', label: '授权绑定的AppID', type: 'input', default: '' },
@@ -1,6 +1,6 @@
/**
* 知识付费 — 1:1 复刻参考主题对应页面(免登陆购买 / 模块位置 / 优惠码 /
* 客户服务 / 付费下载 / 商品预置参数),并保留 AKSRT 订单展示与状态文案。
* 客户服务 / 付费下载 / 商品预置参数),并保留 Jio 订单展示与状态文案。
*/
export const knowledgeSchema = {
key: 'payment.knowledge',
@@ -1,5 +1,5 @@
/**
* VIP会员 — 结构 1:1 对照参考主题 Zibll:会员等级(开关)、会员权益
* VIP会员 — 结构 1:1 对照既有实现:会员等级(开关)、会员权益
* (普通/一级/二级 tab)、会员购买选项(一级/二级/续费/升级 tab)、
* 积分兑换会员与界面文案。
*
@@ -10,6 +10,12 @@
const VIP_EQUITY_DEFAULT = '<li>全站资源折扣购买</li><li>部分内容免费阅读</li><li>一对一技术指导</li><li>VIP用户专属QQ群</li>';
// 主题自带会员图标(assets/img/user/vip/vip-N.svg)。
const themeRoot = typeof window !== 'undefined' && window.jioAdminData?.themeUri
? String(window.jioAdminData.themeUri).replace(/\/$/, '')
: ''
const vipIconDefault = (level) => `${themeRoot}/assets/img/user/vip/vip-${level}.svg`
const vipTagPalette = [
{ label: '#f74b3d', value: 'b-red' },
{ label: '#f3920a', value: 'b-yellow' },
@@ -34,7 +40,7 @@ const vipUnitOptions = [
{ label: '月', value: 'month' },
];
// 会员商品/续费商品:标题行显示价格与时长摘要(行默认收起由 AksField 统一处理)。
// 会员商品/续费商品:标题行显示价格与时长摘要(行默认收起由 JioField 统一处理)。
const vipProductGroupOptions = {
listPreview: { prefix: '价格:', titleKey: 'price', descKey: 'time', descUnitKey: 'unit' },
};
@@ -84,7 +90,7 @@ function vipLevelTabFields(level) {
help: '会员购买价格配置:执行价、原价、促销标签、标签颜色与会员有效时间',
fields: vipProductFields(),
},
{ key: `vip_${level}_img_icon`, label: '会员图标', type: 'upload', default: '', dependsOn: dep, help: `留空使用内置 vip-${level}.svg 图标;建议尺寸300x300` },
{ key: `vip_${level}_img_icon`, label: '会员图标', type: 'upload', default: vipIconDefault(level), dependsOn: dep, help: '默认使用主题自带会员图标;建议尺寸300x300' },
];
}
@@ -105,7 +111,7 @@ export const vipSchema = {
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
],
vip_1_img_icon: '',
vip_1_img_icon: vipIconDefault(1),
pay_user_vip_2_s: true,
pay_user_vip_2_name: '钻石会员',
pay_user_vip_2_desc: '尊享专属特权',
@@ -115,7 +121,7 @@ export const vipSchema = {
{ price: 99, show_price: 199, tag: '<i class="fa fas fa-fw fa-bolt"></i> 限时特惠', time: 3, unit: 'month' },
{ price: 199, show_price: 299, tag: '<i class="fa fas fa-fw fa-bolt"></i> 站长推荐', time: 6, unit: 'month' },
],
vip_2_img_icon: '',
vip_2_img_icon: vipIconDefault(2),
pay_user_vip_desc: '开通VIP会员,享受会员专属折扣以及多项特权',
pay_user_vip_more: '<div>购买后不支持退款</div><div>VIP权益仅适用于本站</div><div>欢迎与站长联系</div>',
pay_download_limit: 0,
@@ -1,6 +1,14 @@
/**
* 提现策略 — 提现门槛、每日限制、收款渠道与提现协议。
*/
/** 默认收款渠道:支付宝与微信,与参考主题一致。 */
export function defaultPayoutChannels() {
return [
{ key: 'alipay', label: '支付宝收款码', account_hint: '姓名 + 支付宝账号', note: '', enabled: true },
{ key: 'wechat', label: '微信收款码', account_hint: '姓名 + 微信号', note: '', enabled: true },
]
}
export const withdrawalSchema = {
key: 'payment.withdrawal',
title: '提现策略',
@@ -12,7 +20,7 @@ export const withdrawalSchema = {
affiliate_daily_withdrawal_count: 3,
affiliate_daily_withdrawal_limit: '500.00',
affiliate_withdrawal_reset_time: '00:00',
affiliate_payout_channels: ['alipay', 'wechat'],
affiliate_payout_channels: defaultPayoutChannels(),
pay_rebate_withdraw_text_details: '<div>可提现金额达到50元后即可发起提现</div><div>申请提现需后台人工处理,一般2-3小时,请耐心等待</div><div>如有其它疑问,请与客服联系</div>',
},
groups: [
@@ -40,7 +48,21 @@ export const withdrawalSchema = {
title: '收款渠道',
icon: 'fa fas fa-qrcode',
fields: [
{ key: 'affiliate_payout_channels', label: '可用收款渠道', type: 'checkbox', default: ['alipay', 'wechat'], sanitize_type: 'array', options: [{ label: '支付宝收款码', value: 'alipay' }, { label: '微信收款码', value: 'wechat' }] },
{
key: 'affiliate_payout_channels',
label: '收款渠道',
type: 'repeater',
addText: '添加收款渠道',
default: defaultPayoutChannels(),
description: '用户提现时可选择的收款方式,可拖拽排序、自定义名称。停用的渠道不会在前台展示,但历史提现记录仍按原名称显示。',
fields: [
{ key: 'key', label: '渠道标识', type: 'input', default: '', help: '英文小写,用于区分渠道(如 alipay、wechat、bank)。保存后请勿随意修改,历史提现记录依赖它。' },
{ key: 'label', label: '渠道名称', type: 'input', default: '', help: '前台下拉里显示的名称,可自定义。' },
{ key: 'account_hint', label: '收款信息提示', type: 'input', default: '', help: '用户填写收款账户时的占位提示,如「姓名 + 支付宝账号」。' },
{ key: 'note', label: '渠道说明', type: 'textarea', default: '', rows: 2, help: '选中该渠道时在收款信息下方展示的补充说明,可留空。' },
{ key: 'enabled', label: '启用', type: 'switch', default: true },
],
},
],
},
{
@@ -5,7 +5,7 @@
/**
* User type options shared by the upload limit repeaters.
* Mirrors AKSRT\Attachment\UploadLimits::user_type_options() (levels 2-10, VIP 1-2).
* Mirrors Jio\Attachment\UploadLimits::user_type_options() (levels 2-10, VIP 1-2).
*/
const uploadUserTypeOptions = [
{ label: '最小默认值(所有人)', value: 'default' },
@@ -87,6 +87,7 @@ export const permissionsSchemas = {
{ label: '打赏用户', value: 'tip_user' },
{ label: '提交工单', value: 'ticket_create' },
{ label: '回复工单', value: 'ticket_reply' },
{ label: '友情链接申请', value: 'friend_link' },
],
},
],
@@ -130,10 +131,6 @@ export const permissionsSchemas = {
description: '管理评论审核、反垃圾、限流与拦截配置。',
defaults: {
enabled: false,
akismet_enabled: false,
akismet_site_url: '',
akismet_blog_lang: 'zh-CN',
akismet_api_key: '',
ai_enabled: false,
ai_moderate_comments: true,
ai_moderate_background: true,
@@ -157,17 +154,6 @@ export const permissionsSchemas = {
{ key: 'enabled', label: '启用审核系统', type: 'switch', help: '关闭后评论将不经过主题审核链路。' },
],
},
{
key: 'akismet',
title: 'Akismet',
icon: 'fa fas fa-filter',
fields: [
{ key: 'akismet_enabled', label: '启用 Akismet', type: 'switch', dependsOn: { key: 'enabled', value: true } },
{ key: 'akismet_site_url', label: 'Site URL', type: 'input', placeholder: 'https://your-site.com', dependsOn: { key: 'akismet_enabled', value: true } },
{ key: 'akismet_blog_lang', label: '博客语言', type: 'input', placeholder: 'zh-CN', dependsOn: { key: 'akismet_enabled', value: true } },
{ key: 'akismet_api_key', label: 'API Key', type: 'input', inputType: 'password', placeholder: 'Akismet API Key', dependsOn: { key: 'akismet_enabled', value: true } },
],
},
{
key: 'ai',
title: 'AI 审核',
@@ -33,7 +33,7 @@ export const commentSchema = {
type: 'content',
ui_only: true,
dependsOn: { key: 'close_comments', value: false },
content: '<p class="aksrt-hint"><i class="fa fas fa-fw fa-info-circle"></i> WordPress默认关闭评论翻页功能,如需启用请按此进行设置:<br/>1. 进入WP讨论设置,勾选 <code>分页显示评论</code><br/>2. 设置默认显示 <code>最前</code>一页<br/>3. 设置在每个页面顶部显示 <code>旧的</code>评论<br/>4. 根据需要设置每一页显示数量<br/>5. 根据需要设置评论嵌套(推荐开启并设置为3层)<br/>6. wp默认开启免登陆评论,如需关闭则勾选<code>需要登录后才能发表评论</code><br/><a href="options-discussion.php">【去设置】</a></p>',
content: '<p class="jio-hint"><i class="fa fas fa-fw fa-info-circle"></i> WordPress默认关闭评论翻页功能,如需启用请按此进行设置:<br/>1. 进入WP讨论设置,勾选 <code>分页显示评论</code><br/>2. 设置默认显示 <code>最前</code>一页<br/>3. 设置在每个页面顶部显示 <code>旧的</code>评论<br/>4. 根据需要设置每一页显示数量<br/>5. 根据需要设置评论嵌套(推荐开启并设置为3层)<br/>6. wp默认开启免登陆评论,如需关闭则勾选<code>需要登录后才能发表评论</code><br/><a href="options-discussion.php">【去设置】</a></p>',
},
{ key: 'close_comments', label: '关闭文章评论功能', type: 'switch', default: false, description: '部分网站无需交互,或需备案审核,可在此关闭所有文章的评论功能。同时每一篇文章可单独关闭评论功能' },
{
@@ -22,24 +22,8 @@ export const featureSchema = {
title: '',
fields: [
{ key: 'article_nav', label: '文章目录树', type: 'switch', default: true, description: '默认开关,每篇文章可单独设置。开启后请自行添加文章目录树模块到侧边栏(文章内标题超过3个才会显示)' },
{
key: 'article_nav_tutorial',
label: '',
type: 'content',
ui_only: true,
dependsOn: { key: 'article_nav', value: true },
content: '<p class="aksrt-hint"><a href="https://www.zibll.com/1717.html" target="_blank">【查看官网教程】</a></p>',
},
{ key: 'article_nav_mobile_nav_s', label: '在移动端弹出菜单内显示', type: 'switch', default: true, dependsOn: { key: 'article_nav', value: true }, description: '如需显示在侧边栏等其它位置,请使用小工具模块添加' },
{ key: 'imagelightbox', label: '图片灯箱', type: 'switch', default: true, description: '点击图片查看原图功能,共两种模式' },
{
key: 'imagelightbox_tutorial',
label: '',
type: 'content',
ui_only: true,
dependsOn: { key: 'imagelightbox', value: true },
content: '<p class="aksrt-hint"><a href="https://www.zibll.com/683.html" target="_blank">【查看官网教程】</a></p>',
},
{
key: 'imagelightbox_type',
label: '图片灯箱模式',
@@ -18,7 +18,7 @@ export const filterSchema = {
key: 'filter_intro',
type: 'content',
ui_only: true,
content: '<p class="aksrt-hint"><b>文章高级筛选功能:</b>通过此功能可实现更加复杂、精细化的内容筛选<br/>添加筛选类型和内容值之后,编写文章时候,可以进行选择,且在前台也可以实现精准筛选<br/>例如影视网站:[类型]有[剧情/喜剧/悬疑/惊悚/犯罪],[地区]有[大陆/美国/日韩/港台/印度],[年份]有[2022/2021/2020/10年代/00年代]等<br/><span class="aksrt-warn">注意事项:添加类型key时候,只能使用英文加下划线,不能有空格,且尽量复杂一点(不能为tag,category等系统已存在的类型),避免与其他mate_key重复!类型名称不能为分类、标签等系统已存在的类型</span><br/><a href="https://www.zibll.com/15886.html" target="_blank">查看官网教程</a></p>',
content: '<p class="jio-hint"><b>文章高级筛选功能:</b>通过此功能可实现更加复杂、精细化的内容筛选<br/>添加筛选类型和内容值之后,编写文章时候,可以进行选择,且在前台也可以实现精准筛选<br/>例如影视网站:[类型]有[剧情/喜剧/悬疑/惊悚/犯罪],[地区]有[大陆/美国/日韩/港台/印度],[年份]有[2022/2021/2020/10年代/00年代]等<br/><span class="jio-warn">注意事项:添加类型key时候,只能使用英文加下划线,不能有空格,且尽量复杂一点(不能为tag,category等系统已存在的类型),避免与其他mate_key重复!类型名称不能为分类、标签等系统已存在的类型</span></p>',
},
{
key: 'custom_filter_show',
@@ -7,7 +7,6 @@ import { thumbSchema } from './thumb.js'
import { pageSchema } from './page.js'
import { filterSchema } from './filter.js'
import { featureSchema } from './feature.js'
import { paySchema } from './pay.js'
import { commentSchema } from './comment.js'
import { submitSchema } from './submit.js'
@@ -17,7 +16,6 @@ export const postListSchemas = {
page: pageSchema,
filter: filterSchema,
feature: featureSchema,
pay: paySchema,
comment: commentSchema,
submit: submitSchema,
}
@@ -129,7 +129,7 @@ export const pageSchema = {
type: 'fieldset',
default: { scale: 0 },
dependsOn: { key: 'article_video_cover', value: true },
description: '视频封面配置 <a href="https://www.zibll.com/1900.html" target="_blank">【查看官方封面配置教程】</a>',
description: '视频封面配置',
fields: [
{ key: 'scale', label: '固定长宽比例', type: 'number', default: 0, min: 0, max: 200, step: 5, unit: '%', description: '为0则不固定长宽比例' },
],
@@ -146,7 +146,7 @@ export const pageSchema = {
key: 'yiyan_intro',
type: 'content',
ui_only: true,
content: '<p class="aksrt-hint"><b>精彩一言功能:</b>随机显示中英文文案,可插入文章页或添加“一言”小工具。默认句库位于 <code>themes/aksrt/yiyan/qv-yiyan.txt</code>。您也可以将自定义句库保存为 <code>wp-content/yiyan.txt</code>,更新主题时即可保留自定义内容。</p>',
content: '<p class="jio-hint"><b>精彩一言功能:</b>随机显示中英文文案,可插入文章页或添加“一言”小工具。默认句库位于 <code>themes/jio/yiyan/qv-yiyan.txt</code>。您也可以将自定义句库保存为 <code>wp-content/yiyan.txt</code>,更新主题时即可保留自定义内容。</p>',
},
{ key: 'yiyan_single_content_header', label: '文章内容头部显示一言', type: 'switch', default: false },
{ key: 'yiyan_single_content_footer', label: '文章内容尾部显示一言', type: 'switch', default: false },
@@ -1,46 +0,0 @@
export const paySchema = {
key: 'post_list.pay',
title: '付费内容',
icon: 'fa fas fa-yen-sign',
description: '配置文章付费阅读、下载功能和购买用户显示。',
defaults: {
post_pay_enable: false,
post_pay_price_default: 0,
post_pay_free_logged_show: false,
post_pay_box_position: 'content',
post_pay_mark: '付费',
post_pay_show_user: false,
post_download_enable: false,
post_download_remote_s: false,
post_download_verify_nonce: true,
post_download_user_log: true,
post_download_post_log: true,
},
groups: [
{
key: 'basic',
title: '付费设置',
icon: 'fa fas fa-database',
fields: [
{ key: 'post_pay_enable', label: '启用付费内容', type: 'switch', default: false },
{ key: 'post_pay_price_default', label: '默认价格', type: 'number', default: 0, min: 0, max: 9999, dependsOn: { key: 'post_pay_enable', value: true } },
{ key: 'post_pay_free_logged_show', label: '登录用户显示免费', type: 'switch', default: false, dependsOn: { key: 'post_pay_enable', value: true } },
{ key: 'post_pay_box_position', label: '付费框位置', type: 'radio', default: 'content', options: [{ label: '内容前', value: 'content' }, { label: '全文遮罩', value: 'mask' }], dependsOn: { key: 'post_pay_enable', value: true } },
{ key: 'post_pay_mark', label: '付费标记', type: 'input', default: '付费', dependsOn: { key: 'post_pay_enable', value: true } },
{ key: 'post_pay_show_user', label: '显示购买用户', type: 'switch', default: false, dependsOn: { key: 'post_pay_enable', value: true } },
],
},
{
key: 'download',
title: '下载设置',
icon: 'fa fas fa-download',
fields: [
{ key: 'post_download_enable', label: '启用下载功能', type: 'switch', default: false },
{ key: 'post_download_remote_s', label: '远程文件下载', type: 'switch', default: false, dependsOn: { key: 'post_download_enable', value: true } },
{ key: 'post_download_verify_nonce', label: '下载鉴权', type: 'switch', default: true, dependsOn: { key: 'post_download_enable', value: true } },
{ key: 'post_download_user_log', label: '用户下载日志', type: 'switch', default: true, dependsOn: { key: 'post_download_enable', value: true } },
{ key: 'post_download_post_log', label: '文章下载日志', type: 'switch', default: true, dependsOn: { key: 'post_download_enable', value: true } },
],
},
],
}
Loaded 100 of 1107 files, more files were not shown because too many files have changed in this diff. Show more