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:
1 parent
5b16bc6e15
commit
35bb96e82f
997 files changed
+28216
-28188
No files matched your search
@@ -0,0 +1,391 @@
|
||||
<template>
|
||||
<div class="jio-form-renderer">
|
||||
<!-- Page header with title and description -->
|
||||
<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="jio-form-renderer__title">{{ title }}</h1>
|
||||
<p v-if="description" class="jio-form-renderer__desc">{{ description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showSearch" class="jio-form-renderer__search">
|
||||
<n-input
|
||||
v-model:value="searchQuery"
|
||||
placeholder="搜索设置项..."
|
||||
size="small"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<FaIcon icon="fa fas fa-search" :size="12" />
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="jio-form-renderer__loading">
|
||||
<n-spin size="medium" />
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<n-alert
|
||||
v-else-if="loadError"
|
||||
type="error"
|
||||
:title="'加载失败'"
|
||||
:style="{ marginBottom: '16px' }"
|
||||
>
|
||||
{{ loadError }}
|
||||
<template #footer>
|
||||
<n-button size="small" @click="$emit('reload')">重试</n-button>
|
||||
</template>
|
||||
</n-alert>
|
||||
|
||||
<!-- Form content -->
|
||||
<template v-else-if="schema && schema.length">
|
||||
<template v-for="group in filteredSchema" :key="groupKey(group)">
|
||||
<JioFieldGroup
|
||||
v-if="isGroupVisible(group)"
|
||||
:title="group.title"
|
||||
:description="group.description"
|
||||
:icon="group.icon"
|
||||
:collapsible="!!group.collapsible"
|
||||
:default-collapsed="resolveGroupCollapsed(group)"
|
||||
>
|
||||
<template v-if="group.fields && !hasGroupSlot(group)">
|
||||
<template v-for="field in group.fields" :key="field.key">
|
||||
<template v-if="isFieldVisible(field)">
|
||||
<!-- Tabbed: renders tabs whose sub-fields are flat top-level keys -->
|
||||
<div
|
||||
v-if="field.type === 'tabbed' && (field.tabs || []).length"
|
||||
class="jio-form-renderer__tabbed"
|
||||
>
|
||||
<n-tabs type="line" animated>
|
||||
<n-tab-pane
|
||||
v-for="tab in field.tabs"
|
||||
:key="tab.key || tab.title"
|
||||
:name="tabName(tab)"
|
||||
:tab="tabName(tab)"
|
||||
>
|
||||
<template v-for="sub in (tab.fields || [])" :key="sub.key">
|
||||
<JioField
|
||||
v-if="isFieldVisible(sub)"
|
||||
:schema="sub"
|
||||
:model-value="getFieldValue(sub.key)"
|
||||
:form-data="formData"
|
||||
:error-message="errors?.[sub.key]"
|
||||
@update:model-value="setFieldValue(sub.key, $event)"
|
||||
@action="emit('field-action', $event)"
|
||||
/>
|
||||
</template>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
<JioSorterField
|
||||
v-else-if="field.type === 'custom' && field.renderer === 'sorter'"
|
||||
:model-value="getFieldValue(field.key) || {}"
|
||||
:options="field.options || []"
|
||||
@update:model-value="setFieldValue(field.key, $event)"
|
||||
/>
|
||||
<slot
|
||||
v-else-if="field.type === 'custom'"
|
||||
:name="`field-${field.key}`"
|
||||
v-bind="{ field, value: getFieldValue(field.key), setValue: (value) => setFieldValue(field.key, value), formData }"
|
||||
>
|
||||
<JioField
|
||||
:schema="field"
|
||||
:model-value="getFieldValue(field.key)"
|
||||
:form-data="formData"
|
||||
:error-message="errors?.[field.key]"
|
||||
@update:model-value="setFieldValue(field.key, $event)"
|
||||
@action="emit('field-action', $event)"
|
||||
/>
|
||||
</slot>
|
||||
<JioField
|
||||
v-else
|
||||
:schema="field"
|
||||
:model-value="getFieldValue(field.key)"
|
||||
:form-data="formData"
|
||||
:error-message="errors?.[field.key]"
|
||||
@update:model-value="setFieldValue(field.key, $event)"
|
||||
@action="emit('field-action', $event)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Custom group content slot -->
|
||||
<slot :name="`group-${group.key}`" v-bind="{ group, formData }" />
|
||||
</JioFieldGroup>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-else class="jio-form-renderer__empty">
|
||||
<FaIcon icon="fa fas fa-cog" :size="32" />
|
||||
<p>暂无配置项</p>
|
||||
</div>
|
||||
|
||||
<!-- Sticky save bar -->
|
||||
<JioSaveBar :dirty="dirty" :saving="saving" @save="$emit('save')" @reload="$emit('reload')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 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:
|
||||
* - Grouped fields in card-style containers
|
||||
* - Inline search/filter
|
||||
* - Dirty state tracking with sticky save bar
|
||||
* - Dependency-based field visibility
|
||||
* - Loading and error states
|
||||
*/
|
||||
import { ref, computed, useSlots } from 'vue'
|
||||
import FaIcon from './FaIcon.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 */
|
||||
title: { type: String, default: '' },
|
||||
/** Page description */
|
||||
description: { type: String, default: '' },
|
||||
/** Title icon (FA5 class) */
|
||||
icon: { type: String, default: '' },
|
||||
/**
|
||||
* Schema definition: array of field groups.
|
||||
* Each group: { key, title, description?, icon?, collapsible?, fields: FieldDef[] }
|
||||
* Each field: { key, label, type, default, options?, dependsOn?, help?, ... }
|
||||
*/
|
||||
schema: { type: Array, default: () => [] },
|
||||
/** Reactive form data object */
|
||||
formData: { type: Object, default: () => ({}) },
|
||||
/** Whether the form has unsaved changes */
|
||||
dirty: { type: Boolean, default: false },
|
||||
/** Whether data is currently loading */
|
||||
loading: { type: Boolean, default: false },
|
||||
/** Whether a save is in progress */
|
||||
saving: { type: Boolean, default: false },
|
||||
/** Load error message */
|
||||
loadError: { type: String, default: '' },
|
||||
/** Whether to show the search bar */
|
||||
showSearch: { type: Boolean, default: true },
|
||||
/** Validation errors keyed by field key */
|
||||
errors: { type: Object, default: () => ({}) },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:formData', 'save', 'reload', 'field-action'])
|
||||
const slots = useSlots()
|
||||
|
||||
const searchQuery = ref('')
|
||||
|
||||
/** Evaluate dependsOn rule against current form data */
|
||||
function matchDependsOn(dependsOn) {
|
||||
if (!dependsOn) return true
|
||||
if (typeof dependsOn === 'string') {
|
||||
return !!props.formData[dependsOn]
|
||||
}
|
||||
if (typeof dependsOn === 'object') {
|
||||
if (Array.isArray(dependsOn.all)) return dependsOn.all.every(matchDependsOn)
|
||||
if (Array.isArray(dependsOn.any)) return dependsOn.any.some(matchDependsOn)
|
||||
const { key, value, values, not, operator } = dependsOn
|
||||
const currentVal = props.formData[key]
|
||||
if (operator === 'truthy') return !!currentVal
|
||||
if (operator === 'falsy') return !currentVal
|
||||
if (operator === 'not_empty') return Array.isArray(currentVal) ? currentVal.length > 0 : currentVal !== '' && currentVal != null
|
||||
if (operator === 'empty') return Array.isArray(currentVal) ? currentVal.length === 0 : currentVal === '' || currentVal == null
|
||||
if (operator === 'eq') return currentVal === value
|
||||
if (operator === 'neq') return currentVal !== value
|
||||
if (not) {
|
||||
if (Array.isArray(values)) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return !values.some((v) => currentVal == v)
|
||||
}
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return currentVal !== value && currentVal != value
|
||||
}
|
||||
// values: match any of a list (e.g. provider ∈ [qq, polling])
|
||||
if (Array.isArray(values)) {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return values.some((v) => currentVal == v)
|
||||
}
|
||||
// Loose equality so true matches 1 / '1' from some option stores.
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return currentVal == value || (!!value === true && !!currentVal)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function hasGroupSlot(group) {
|
||||
return !!slots[`group-${group.key}`]
|
||||
}
|
||||
|
||||
/** Check if a field should be visible based on its dependsOn rule */
|
||||
function isFieldVisible(field) {
|
||||
return matchDependsOn(field.dependsOn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab label. `titleKey` binds the label to a live form value (e.g. the VIP level
|
||||
* display name) so renaming a level renames its tab; `titleFallback` is used
|
||||
* while that value is still empty.
|
||||
*/
|
||||
function tabName(tab) {
|
||||
if (!tab) return ''
|
||||
const dynamic = tab.titleKey ? props.formData?.[tab.titleKey] : ''
|
||||
const label = typeof dynamic === 'string' ? dynamic.trim() : ''
|
||||
return label || tab.titleFallback || tab.title || tab.key || ''
|
||||
}
|
||||
|
||||
/** Whole group can be hidden (e.g. optional advanced blocks). */
|
||||
function isGroupVisible(group) {
|
||||
return matchDependsOn(group.dependsOn)
|
||||
}
|
||||
|
||||
/** 可折叠分组的初始状态:defaultCollapsed 为真时默认收起。 */
|
||||
function resolveGroupCollapsed(group) {
|
||||
return !!group.defaultCollapsed
|
||||
}
|
||||
|
||||
function groupKey(group) {
|
||||
return group.key || group.title || 'group'
|
||||
}
|
||||
|
||||
/** Get field value from formData */
|
||||
function getFieldValue(key) {
|
||||
return props.formData?.[key]
|
||||
}
|
||||
|
||||
/** Set field value, emitting update for parent */
|
||||
function setFieldValue(key, value) {
|
||||
const newData = { ...props.formData, [key]: value }
|
||||
emit('update:formData', newData)
|
||||
}
|
||||
|
||||
/** Filter schema groups/fields by search query */
|
||||
const filteredSchema = computed(() => {
|
||||
const q = searchQuery.value?.toLowerCase().trim()
|
||||
if (!q) return props.schema
|
||||
|
||||
return props.schema
|
||||
.map(group => {
|
||||
if (!group.fields) return null
|
||||
|
||||
// Check if group title matches
|
||||
const groupMatch = (group.title || '').toLowerCase().includes(q)
|
||||
|| (group.description || '').toLowerCase().includes(q)
|
||||
|
||||
// Filter fields by label, key, or help text matching query
|
||||
const matchingFields = group.fields.filter(field => {
|
||||
if (groupMatch) return true
|
||||
if (field.type === 'tabbed' && Array.isArray(field.tabs)) {
|
||||
return (field.label || '').toLowerCase().includes(q)
|
||||
|| field.tabs.some(tab => (tab.fields || []).some(sub =>
|
||||
(sub.label || '').toLowerCase().includes(q)
|
||||
|| (sub.key || '').toLowerCase().includes(q)
|
||||
|| (sub.help || '').toLowerCase().includes(q)
|
||||
|| (sub.description || '').toLowerCase().includes(q)
|
||||
))
|
||||
}
|
||||
return (field.label || '').toLowerCase().includes(q)
|
||||
|| (field.key || '').toLowerCase().includes(q)
|
||||
|| (field.help || '').toLowerCase().includes(q)
|
||||
|| (field.description || '').toLowerCase().includes(q)
|
||||
})
|
||||
|
||||
if (matchingFields.length === 0) return null
|
||||
|
||||
return { ...group, fields: groupMatch ? group.fields : matchingFields }
|
||||
})
|
||||
.filter(Boolean)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.jio-form-renderer__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.jio-form-renderer__header-left {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.jio-form-renderer__header-icon {
|
||||
color: var(--jio-color-primary);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.jio-form-renderer__title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--jio-color-text);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.jio-form-renderer__desc {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
color: var(--jio-color-muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.jio-form-renderer__tabbed {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.jio-form-renderer__search {
|
||||
min-width: 200px;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.jio-form-renderer__loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 60px 0;
|
||||
color: var(--jio-color-muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.jio-form-renderer__empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 60px 0;
|
||||
color: var(--jio-color-border-strong);
|
||||
}
|
||||
|
||||
.jio-form-renderer__empty p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 768px) {
|
||||
.jio-form-renderer__header {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.jio-form-renderer__search {
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user