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>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-11-01 20:05:04 +08:00
1 parent 5b16bc6e15
commit 35bb96e82f
997 files changed
+28216 -28188

No files matched your search

+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%;
}
}
@@ -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,7 +16,7 @@ export const FLOAT_BTN_META = {
more: { title: '更多按钮', panel: '添加更多自定义按钮' },
}
const themeUri = (window.aksrtAdminData?.themeUri || '').replace(/\/$/, '')
const themeUri = (window.jioAdminData?.themeUri || '').replace(/\/$/, '')
/** 「更多按钮」出厂默认项(对齐参考主题 float_btn.more 默认值)。 */
export function giftMoreButton() {
@@ -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"
class="jio-icon-picker__preview-svg"
v-html="zibsvgMarkup(`zibsvg-${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'
@@ -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>
@@ -4,13 +4,13 @@
<!-- Built-in sprite value -->
<span
v-else-if="isZibsvgValue(value)"
class="aksrt-icon-value__svg"
class="jio-icon-value__svg"
v-html="zibsvgMarkup(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>
@@ -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;
@@ -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,7 +25,7 @@
/>
<!-- 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 || ''"
@@ -36,11 +36,11 @@
<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 +49,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 +64,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 +78,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 +87,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 +100,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 +199,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 +208,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 +230,7 @@
/>
<!-- Media upload -->
<AksMediaField
<JioMediaField
v-else-if="schema.type === 'media' || schema.type === 'upload'"
:model-value="modelValue"
:accept="schema.accept"
@@ -239,7 +239,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 +259,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 +281,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 +316,7 @@
/>
<!-- Object composite: between_number (min/max pair) -->
<AksCompositeField
<JioCompositeField
v-else-if="schema.type === 'between_number'"
:schema="schema"
:model-value="modelValue"
@@ -324,7 +324,7 @@
/>
<!-- Image select (radio with image preview) -->
<AksImageSelectField
<JioImageSelectField
v-else-if="schema.type === 'image_select'"
:model-value="modelValue"
:options="normalizedOptions"
@@ -334,24 +334,24 @@
/>
<!-- 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"
@@ -363,7 +363,7 @@
</div>
</template>
<template v-if="!fieldsetSections.length">
<AksField
<JioField
v-for="field in visibleNestedFields"
:key="field.key"
:schema="field"
@@ -374,7 +374,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)" />
@@ -386,54 +386,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"
@@ -448,12 +448,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]"
@@ -466,7 +466,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>
@@ -483,7 +483,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"
@@ -503,35 +503,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'
@@ -567,12 +567,12 @@ 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__')
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 === '***')
@@ -580,7 +580,7 @@ const secretClearVisible = computed(() => secretConfigured.value || secretMarked
const secretTextareaValue = computed(() => secretConfigured.value ? '' : secretDisplayValue.value)
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 */
@@ -682,7 +682,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))
@@ -754,165 +754,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;
@@ -920,58 +920,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,15 +1,15 @@
<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="搜索设置项..."
@@ -24,7 +24,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 +45,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 +59,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 +69,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 +82,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 +93,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 +102,7 @@
@action="emit('field-action', $event)"
/>
</slot>
<AksField
<JioField
v-else
:schema="field"
:model-value="getFieldValue(field.key)"
@@ -117,24 +117,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 +146,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 */
@@ -308,7 +308,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 +317,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;
@@ -1,21 +1,21 @@
<template>
<div class="aksrt-image-select" role="radiogroup">
<div class="jio-image-select" role="radiogroup">
<button
v-for="opt in options"
:key="opt.value"
type="button"
role="radio"
class="aksrt-image-select__item"
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="aksrt-image-select__img" :style="imgStyle(opt)" />
<span v-else class="aksrt-image-select__placeholder" :style="imgStyle(opt)">
<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="aksrt-image-select__label">{{ opt.label }}</span>
<span class="jio-image-select__label">{{ opt.label }}</span>
</button>
</div>
</template>
@@ -37,78 +37,78 @@ 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 { '--aksrt-image-select-max': typeof width === 'number' ? `${width}px` : width }
return { '--jio-image-select-max': typeof width === 'number' ? `${width}px` : width }
}
</script>
<style scoped>
.aksrt-image-select {
.jio-image-select {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.aksrt-image-select__item {
.jio-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: 2px solid var(--jio-color-border);
border-radius: 8px;
background: var(--aksrt-color-surface);
background: var(--jio-color-surface);
cursor: pointer;
transition: border-color .15s ease, background-color .15s ease;
}
.aksrt-image-select__item:hover,
.aksrt-image-select__item:focus-visible {
.jio-image-select__item:hover,
.jio-image-select__item:focus-visible {
outline: none;
border-color: var(--aksrt-color-primary);
border-color: var(--jio-color-primary);
}
.aksrt-image-select__item.is-active {
border-color: var(--aksrt-color-primary);
background: var(--aksrt-color-primary-soft);
.jio-image-select__item.is-active {
border-color: var(--jio-color-primary);
background: var(--jio-color-primary-soft);
}
.aksrt-image-select__item:disabled {
.jio-image-select__item:disabled {
cursor: not-allowed;
}
.aksrt-image-select__img {
.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(--aksrt-image-select-max, 140px);
max-width: var(--jio-image-select-max, 140px);
height: auto;
border-radius: 4px;
background: var(--aksrt-color-canvas);
background: var(--jio-color-canvas);
}
.aksrt-image-select__placeholder {
.jio-image-select__placeholder {
display: flex;
align-items: center;
justify-content: center;
width: var(--aksrt-image-select-max, 140px);
width: var(--jio-image-select-max, 140px);
height: 60px;
border-radius: 4px;
background: var(--aksrt-color-canvas);
color: var(--aksrt-color-border-strong);
background: var(--jio-color-canvas);
color: var(--jio-color-border-strong);
}
.aksrt-image-select__label {
color: var(--aksrt-color-text-secondary);
.jio-image-select__label {
color: var(--jio-color-text-secondary);
font-size: 11px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.aksrt-image-select__item {
.jio-image-select__item {
transition: none;
}
}
@@ -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