refactor(admin): unify sortable/draggable/repeater list UI

- Add shared sortable-list.css, SORTABLE_LIST_ATTRS and AksSortHandle
- Migrate schema sortable, selected order, inline/modal repeater, sorter,
  floating buttons, footer tabbar and ticket prereply lists to the shared design
- Split AksField.vue (1885 -> 969 lines) into focused components/helpers
- Remove dead AksArrayTable/AksRepeaterField and move-up/down helpers
- Replace hardcoded colors and --n-* vars with --aksrt-color-* tokens

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-28 13:41:26 +08:00
1 parent 135fb4eaa4
commit 8843ae4261
20 files changed
+1442 -1796

No files matched your search

@@ -51,11 +51,9 @@
<div class="float-settings__desc">设置并排序需要显示的按钮</div>
<draggable
v-model="form.items"
v-bind="SORTABLE_LIST_ATTRS"
item-key="type"
handle=".float-btn-panel__handle"
ghost-class="float-settings__ghost"
animation="180"
class="float-settings__list"
class="aksrt-sortable"
>
<template #item="{ element, index }">
<FloatBtnItemPanel :model-value="element" @update:model-value="updateItem(index, $event)" />
@@ -76,6 +74,7 @@ import draggable from 'vuedraggable'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
import AksSaveBar from '@/components/form/AksSaveBar.vue'
import { SORTABLE_LIST_ATTRS } from '@/components/form/sortable-options.js'
import FloatBtnItemPanel from '@/components/floating/FloatBtnItemPanel.vue'
import { defaultSettings, normalizeItems } from '@/components/floating/floating-defaults.js'
@@ -163,13 +162,13 @@ onMounted(loadSettings)
.float-settings__desc {
font-size: 12px;
color: var(--n-text-color-3, #999);
color: var(--aksrt-color-muted, #6F7A72);
}
.float-settings__label {
margin-left: 12px;
font-size: 13px;
color: var(--n-text-color-3, #999);
color: var(--aksrt-color-muted, #6F7A72);
}
.float-settings__section {
@@ -183,14 +182,4 @@ onMounted(loadSettings)
font-weight: 600;
font-size: 14px;
}
.float-settings__list {
display: flex;
flex-direction: column;
gap: 8px;
}
.float-settings__ghost {
opacity: 0.45;
}
</style>
@@ -61,42 +61,38 @@
<draggable
v-if="form.items.length"
v-model="form.items"
v-bind="SORTABLE_LIST_ATTRS"
item-key="id"
handle=".tabbar-sort__handle"
ghost-class="tabbar-sort__ghost"
animation="180"
class="tabbar-sort"
class="aksrt-sortable"
@end="resetSortOrder"
>
<template #item="{ element, index }">
<div class="tabbar-sort__item">
<div class="tabbar-sort__main">
<button type="button" class="tabbar-sort__handle" aria-label="拖拽排序">
<i class="fa fas fa-bars"></i>
</button>
<span class="tabbar-sort__icon">
<div class="aksrt-sortable__item">
<div class="aksrt-sortable__head">
<AksSortHandle />
<span class="aksrt-sortable__icon">
<IconValue v-if="element.icon" :value="element.icon" />
</span>
<div class="tabbar-sort__meta">
<div class="tabbar-sort__title-row">
<strong>{{ element.text || '按钮' }}</strong>
<div class="aksrt-sortable__main">
<div class="aksrt-sortable__title-row">
<span class="aksrt-sortable__title">{{ element.text || '按钮' }}</span>
<n-tag size="small" :bordered="false">{{ typeLabelMap[element.type] || element.type }}</n-tag>
<n-tag v-if="element.badge" size="small" type="error" :bordered="false">徽标 {{ element.badge }}</n-tag>
<n-tag v-if="element.type === 'home' && element.ontop" size="small" type="success" :bordered="false">返回顶部</n-tag>
<n-tag size="small" :type="element.enabled ? 'success' : 'default'" :bordered="false">{{ element.enabled ? '启用' : '禁用' }}</n-tag>
</div>
<div class="tabbar-sort__desc">{{ getItemSummary(element) }}</div>
<div class="aksrt-sortable__desc">{{ getItemSummary(element) }}</div>
</div>
<div class="aksrt-sortable__actions">
<n-button size="tiny" quaternary type="primary" @click="editItem(index)">
<template #icon><i class="fa fas fa-edit" style="font-size:12px;" /></template>
编辑
</n-button>
<n-button size="tiny" quaternary type="error" @click="deleteItem(index)">
<template #icon><i class="fa fas fa-trash-alt" style="font-size:12px;" /></template>
删除
</n-button>
</div>
</div>
<div class="tabbar-sort__actions">
<n-button size="tiny" quaternary type="primary" @click="editItem(index)">
<template #icon><i class="fa fas fa-edit" style="font-size:12px;" /></template>
编辑
</n-button>
<n-button size="tiny" quaternary type="error" @click="deleteItem(index)">
<template #icon><i class="fa fas fa-trash-alt" style="font-size:12px;" /></template>
删除
</n-button>
</div>
</div>
</template>
@@ -216,22 +212,21 @@
<div v-if="editingItem.add_items.length" class="add-items-editor__list">
<draggable
v-model="editingItem.add_items"
v-bind="SORTABLE_LIST_ATTRS"
item-key="id"
handle=".add-items-editor__drag"
ghost-class="add-items-editor__ghost"
animation="180"
class="aksrt-sortable aksrt-sortable--compact"
>
<template #item="{ element, index: subIndex }">
<div class="add-items-editor__row">
<button type="button" class="add-items-editor__drag" aria-label="拖拽排序">
<i class="fa fas fa-bars"></i>
</button>
<n-input v-model:value="element.label" placeholder="名称,例如:写文章" />
<FaIconPicker v-model="element.icon" placeholder="选择图标" />
<n-input v-model:value="element.url" placeholder="链接,例如:/wp-admin/post-new.php" />
<n-button quaternary type="error" @click="removeAddItem(subIndex)">
<template #icon><i class="fa fas fa-trash-alt" /></template>
</n-button>
<div class="aksrt-sortable__item">
<div class="aksrt-sortable__head add-items-editor__row">
<AksSortHandle />
<n-input v-model:value="element.label" placeholder="名称,例如:写文章" />
<FaIconPicker v-model="element.icon" placeholder="选择图标" />
<n-input v-model:value="element.url" placeholder="链接,例如:/wp-admin/post-new.php" />
<n-button quaternary type="error" aria-label="删除子项" @click="removeAddItem(subIndex)">
<template #icon><i class="fa fas fa-trash-alt" /></template>
</n-button>
</div>
</div>
</template>
</draggable>
@@ -267,6 +262,8 @@ import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
import FaIconPicker from '@/components/form/FaIconPicker.vue'
import IconValue from '@/components/form/IconValue.vue'
import AksSortHandle from '@/components/form/AksSortHandle.vue'
import { SORTABLE_LIST_ATTRS } from '@/components/form/sortable-options.js'
const message = useMessage()
const dialog = useDialog()
@@ -609,7 +606,7 @@ onMounted(loadSettings)
}
.icon-preview-row a {
color: var(--n-primary-color);
color: var(--aksrt-color-primary, #18A058);
text-decoration: none;
}
@@ -618,18 +615,12 @@ onMounted(loadSettings)
}
.muted-text {
color: #999;
color: var(--aksrt-color-muted, #6F7A72);
font-size: 12px;
}
.aksrt-modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.section-desc {
color: var(--n-text-color-3);
color: var(--aksrt-color-muted, #6F7A72);
font-size: 13px;
margin: 0 0 16px;
}
@@ -659,127 +650,19 @@ onMounted(loadSettings)
.add-items-editor__presets-label {
font-size: 12px;
color: var(--n-text-color-3, #999);
}
.add-items-editor__list {
display: flex;
flex-direction: column;
gap: 10px;
color: var(--aksrt-color-muted, #6F7A72);
}
.add-items-editor__row {
display: grid;
grid-template-columns: 32px 1fr 1fr 1.2fr 40px;
grid-template-columns: 28px 1fr 1fr 1.2fr 34px;
gap: 8px;
align-items: center;
}
.add-items-editor__drag {
width: 32px;
height: 32px;
border: 0;
border-radius: 10px;
background: #f3f5f7;
color: #6b7280;
cursor: grab;
}
.add-items-editor__ghost {
opacity: 0.5;
}
.tabbar-sort {
display: flex;
flex-direction: column;
gap: 12px;
}
.tabbar-sort__item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
border: 1px solid var(--n-border-color);
border-radius: 14px;
background: #fff;
}
.tabbar-sort__main {
min-width: 0;
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.tabbar-sort__handle {
width: 32px;
height: 32px;
border: 0;
border-radius: 10px;
background: #f3f5f7;
color: #6b7280;
cursor: grab;
}
.tabbar-sort__icon {
width: 38px;
height: 38px;
border-radius: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(24, 160, 88, 0.12);
color: var(--aksrt-color-primary);
font-size: 18px;
flex: 0 0 auto;
}
.tabbar-sort__meta {
min-width: 0;
flex: 1;
}
.tabbar-sort__title-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 4px;
}
.tabbar-sort__desc {
color: var(--n-text-color-3);
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tabbar-sort__actions {
display: flex;
gap: 6px;
flex: 0 0 auto;
}
.tabbar-sort__ghost {
opacity: 0.5;
}
@media (max-width: 1023px) {
.tabbar-sort__item {
flex-direction: column;
align-items: stretch;
}
.tabbar-sort__actions {
justify-content: flex-end;
}
.add-items-editor__row {
grid-template-columns: 32px 1fr;
grid-template-columns: 28px 1fr;
}
}
</style>
@@ -19,20 +19,23 @@
<draggable
v-else
v-model="rows"
v-bind="SORTABLE_LIST_ATTRS"
item-key="id"
handle=".aksrt-ticket-prereply__handle"
:animation="160"
class="aksrt-ticket-prereply"
class="aksrt-sortable"
>
<template #item="{ element, index }">
<div class="aksrt-ticket-prereply__row">
<span class="aksrt-ticket-prereply__handle" title="拖动排序">⋮⋮</span>
<span class="aksrt-ticket-prereply__index">{{ index + 1 }}</span>
<span class="aksrt-ticket-prereply__content">{{ element.content }}</span>
<n-space size="small">
<n-button size="tiny" type="primary" quaternary @click="openEdit(element)">编辑</n-button>
<n-button size="tiny" type="error" quaternary @click="handleDelete(element)">删除</n-button>
</n-space>
<div class="aksrt-sortable__item">
<div class="aksrt-sortable__head">
<AksSortHandle />
<span class="aksrt-sortable__index">{{ index + 1 }}</span>
<div class="aksrt-sortable__main">
<span class="aksrt-ticket-prereply__content">{{ element.content }}</span>
</div>
<div class="aksrt-sortable__actions">
<n-button size="tiny" type="primary" quaternary @click="openEdit(element)">编辑</n-button>
<n-button size="tiny" type="error" quaternary @click="handleDelete(element)">删除</n-button>
</div>
</div>
</div>
</template>
</draggable>
@@ -71,6 +74,8 @@ import { computed, onMounted, ref } from 'vue'
import { NButton, NSpace, useDialog, useMessage } from 'naive-ui'
import draggable from 'vuedraggable'
import api from '@/api'
import AksSortHandle from '@/components/form/AksSortHandle.vue'
import { SORTABLE_LIST_ATTRS } from '@/components/form/sortable-options.js'
const message = useMessage()
const dialog = useDialog()
@@ -188,38 +193,11 @@ defineExpose({ reload: load })
</script>
<style scoped>
.aksrt-ticket-prereply {
display: grid;
gap: 8px;
}
.aksrt-ticket-prereply__row {
display: grid;
grid-template-columns: 20px 28px 1fr auto;
align-items: center;
gap: 10px;
padding: 10px 12px;
border: 1px var(--n-border-color, #efeff5) solid;
border-radius: 8px;
background: var(--n-color, #fff);
}
.aksrt-ticket-prereply__handle {
color: var(--aksrt-color-muted);
cursor: grab;
user-select: none;
}
.aksrt-ticket-prereply__index {
color: var(--aksrt-color-muted);
font-size: 12px;
font-variant-numeric: tabular-nums;
}
.aksrt-ticket-prereply__content {
white-space: pre-wrap;
word-break: break-word;
color: var(--aksrt-color-text, #1F2923);
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
</style>