feat(float): rebuild right floating buttons 1:1 with reference theme
- Storage: aksrt_floating_buttons now uses reference float_btn shape (fixed 8 sortable types, more-buttons group); single source of defaults/sanitize in AKSRT\Settings\FloatButtons - Admin SPA: CSF-parity page split into components/floating/* - Frontend: aksrt_float_right() on wp_footer mirrors zib_float_right() (wp_is_mobile, PC tooltip, toggle-theme, ontop/scrollTopTo, qrcode, wechat/qq, pay_vip, add, more) - CSS: floating-buttons.css rewritten from reference rules; legacy float rules removed from main/part/shell/overlay-primitives - Remove dead code: reorder route/API, get_floating_buttons_items, floatingSchema, floatActionsId, float-theme-toggle bindings, common/floating.css, aksrt-runtime-win.php, .aksrt-float-btn
This commit is contained in:
@@ -11,7 +11,6 @@ export const globalApiMethods = {
|
||||
|
||||
getFloatingButtons() { return this.request('/settings/floating-buttons') },
|
||||
saveFloatingButtons(data) { return this.request('/settings/floating-buttons', { method: 'POST', body: JSON.stringify(data) }) },
|
||||
reorderFloatingButtons(ids) { return this.request('/settings/floating-buttons/reorder', { method: 'POST', body: JSON.stringify({ ids }) }) },
|
||||
|
||||
getFriendLinks() { return this.request('/friend-links') },
|
||||
addFriendLink(data) { return this.request('/friend-links', { method: 'POST', body: JSON.stringify(data) }) },
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<n-form label-placement="left" label-width="110" class="aksrt-form float-common-fields">
|
||||
<n-form-item label="PC端显示" :show-feedback="false">
|
||||
<n-switch :value="!!modelValue.pc_s" @update:value="update('pc_s', $event)" />
|
||||
</n-form-item>
|
||||
<n-form-item label="移动端显示" :show-feedback="false">
|
||||
<n-switch :value="!!modelValue.m_s" @update:value="update('m_s', $event)" />
|
||||
</n-form-item>
|
||||
<n-form-item label="颜色" :show-feedback="false">
|
||||
<div class="float-common-fields__colors">
|
||||
<div class="float-common-fields__color">
|
||||
<span>自定义图标颜色</span>
|
||||
<n-color-picker
|
||||
:value="modelValue.color?.color || null"
|
||||
:show-alpha="true"
|
||||
:modes="['hex', 'rgb']"
|
||||
clearable
|
||||
@update:value="updateColor('color', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="float-common-fields__color">
|
||||
<span>自定义背景颜色</span>
|
||||
<n-color-picker
|
||||
:value="modelValue.color?.bg || null"
|
||||
:show-alpha="true"
|
||||
:modes="['hex', 'rgb']"
|
||||
clearable
|
||||
@update:value="updateColor('bg', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<div class="float-common-fields__desc">自定义颜色请注意日间、夜间模式的适配</div>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
modelValue: { type: Object, required: true },
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
function update(key, value) {
|
||||
emit('update:modelValue', { ...props.modelValue, [key]: value })
|
||||
}
|
||||
|
||||
function updateColor(key, value) {
|
||||
emit('update:modelValue', {
|
||||
...props.modelValue,
|
||||
color: { ...(props.modelValue.color || { color: '', bg: '' }), [key]: value || '' },
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.float-common-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.float-common-fields__colors {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.float-common-fields__color {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 180px;
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: var(--n-text-color-3);
|
||||
}
|
||||
|
||||
.float-common-fields__desc {
|
||||
font-size: 12px;
|
||||
color: var(--n-text-color-3);
|
||||
padding-left: 110px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div class="float-btn-panel">
|
||||
<div class="float-btn-panel__head" @click="expanded = !expanded">
|
||||
<button type="button" class="float-btn-panel__handle" aria-label="拖拽排序" @click.stop>
|
||||
<i class="fa fas fa-arrows-alt-v" />
|
||||
</button>
|
||||
<strong class="float-btn-panel__title">{{ meta.title }}</strong>
|
||||
<template v-if="modelValue.type !== 'more'">
|
||||
<n-tag v-if="modelValue.pc_s" size="small" type="info" :bordered="false">PC</n-tag>
|
||||
<n-tag v-if="modelValue.m_s" size="small" type="warning" :bordered="false">移动</n-tag>
|
||||
<n-tag v-if="!modelValue.pc_s && !modelValue.m_s" size="small" :bordered="false">未显示</n-tag>
|
||||
</template>
|
||||
<n-tag v-else size="small" :bordered="false">{{ modelValue.buttons.length }} 个</n-tag>
|
||||
<i class="fa fas float-btn-panel__arrow" :class="expanded ? 'fa-angle-up' : 'fa-angle-down'" />
|
||||
</div>
|
||||
|
||||
<div v-if="meta.warn" class="float-btn-panel__warn">
|
||||
<i class="fa fas fa-info-circle" /> {{ meta.warn }}
|
||||
</div>
|
||||
|
||||
<div v-show="expanded" class="float-btn-panel__body">
|
||||
<div class="float-btn-panel__subtitle">{{ meta.panel }}</div>
|
||||
|
||||
<FloatMoreButtonsEditor
|
||||
v-if="modelValue.type === 'more'"
|
||||
:model-value="modelValue.buttons"
|
||||
@update:model-value="patch({ buttons: $event })"
|
||||
/>
|
||||
|
||||
<template v-else>
|
||||
<FloatBtnCommonFields :model-value="modelValue" @update:model-value="emit('update:modelValue', $event)" />
|
||||
|
||||
<n-form label-placement="left" label-width="110" class="aksrt-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>
|
||||
|
||||
<template v-if="modelValue.type === 'add'">
|
||||
<n-form-item label="选择按钮">
|
||||
<div class="float-btn-panel__block">
|
||||
<n-select
|
||||
:value="modelValue.btns"
|
||||
:options="ADD_BTN_OPTIONS"
|
||||
multiple
|
||||
placeholder="请选择按钮"
|
||||
@update:value="patch({ btns: $event })"
|
||||
/>
|
||||
<div class="float-btn-panel__desc">选择并排序按钮,未开启的功能不会显示</div>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item label="按钮图标">
|
||||
<FaIconPicker v-model="addIcon" placeholder="选择图标" />
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<n-form-item v-if="modelValue.type === 'service_qq'" label="QQ号码">
|
||||
<n-input :value="modelValue.qq" placeholder="仅填写数字" @update:value="patch({ qq: $event })" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item v-if="modelValue.type === 'service_wechat'" label="微信二维码">
|
||||
<AksMediaField
|
||||
:model-value="modelValue.wechat_img"
|
||||
placeholder="选择二维码图片"
|
||||
@update:model-value="patch({ wechat_img: $event })"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import FaIconPicker from '@/components/form/FaIconPicker.vue'
|
||||
import AksMediaField from '@/components/form/AksMediaField.vue'
|
||||
import FloatBtnCommonFields from './FloatBtnCommonFields.vue'
|
||||
import FloatMoreButtonsEditor from './FloatMoreButtonsEditor.vue'
|
||||
import { ADD_BTN_OPTIONS, FLOAT_BTN_META } from './floating-defaults.js'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Object, required: true },
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const expanded = ref(false)
|
||||
const meta = computed(() => FLOAT_BTN_META[props.modelValue.type] || { title: props.modelValue.type, panel: '' })
|
||||
|
||||
const addIcon = computed({
|
||||
get: () => props.modelValue.icon || '',
|
||||
set: (value) => patch({ icon: value || '' }),
|
||||
})
|
||||
|
||||
function patch(partial) {
|
||||
emit('update:modelValue', { ...props.modelValue, ...partial })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.float-btn-panel {
|
||||
border: 1px solid var(--n-border-color, rgba(0, 0, 0, 0.09));
|
||||
border-radius: 8px;
|
||||
background: var(--n-card-color, #fff);
|
||||
}
|
||||
|
||||
.float-btn-panel__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.float-btn-panel__handle {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--n-text-color-3, #999);
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.float-btn-panel__handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.float-btn-panel__title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.float-btn-panel__arrow {
|
||||
margin-left: auto;
|
||||
color: var(--n-text-color-3, #999);
|
||||
}
|
||||
|
||||
.float-btn-panel__warn {
|
||||
padding: 0 14px 10px 48px;
|
||||
font-size: 12px;
|
||||
color: #f0a020;
|
||||
}
|
||||
|
||||
.float-btn-panel__body {
|
||||
padding: 12px 16px 16px;
|
||||
border-top: 1px solid var(--n-border-color, rgba(0, 0, 0, 0.09));
|
||||
}
|
||||
|
||||
.float-btn-panel__subtitle {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.float-btn-panel__extra {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.float-btn-panel__block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.float-btn-panel__desc {
|
||||
font-size: 12px;
|
||||
color: var(--n-text-color-3, #999);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div class="float-more-editor">
|
||||
<n-collapse :default-expanded-names="[]" accordion>
|
||||
<n-collapse-item
|
||||
v-for="(btn, index) in modelValue"
|
||||
:key="index"
|
||||
:name="index"
|
||||
>
|
||||
<template #header>
|
||||
<span class="float-more-editor__head">
|
||||
<IconValue v-if="btn.icon" :value="btn.icon" />
|
||||
<span>{{ btn.desc || `按钮 ${index + 1}` }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<n-button
|
||||
size="tiny"
|
||||
quaternary
|
||||
type="error"
|
||||
:disabled="modelValue.length <= 1"
|
||||
@click.stop="remove(index)"
|
||||
>
|
||||
<template #icon><i class="fa fas fa-trash-alt" /></template>
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<n-form label-placement="left" label-width="110" class="aksrt-form">
|
||||
<n-form-item label="按钮简介">
|
||||
<n-input :value="btn.desc" @update:value="patch(index, { desc: $event })" />
|
||||
</n-form-item>
|
||||
<n-form-item label="按钮图标">
|
||||
<FaIconPicker :model-value="btn.icon" placeholder="选择图标" @update:model-value="patch(index, { icon: $event || '' })" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="!btn.hover" label="按钮链接">
|
||||
<div class="float-more-editor__link">
|
||||
<n-input
|
||||
:value="btn.link.url"
|
||||
placeholder="https://"
|
||||
@update:value="patch(index, { link: { ...btn.link, url: $event } })"
|
||||
/>
|
||||
<n-checkbox
|
||||
:checked="btn.link.target === '_blank'"
|
||||
@update:checked="patch(index, { link: { ...btn.link, target: $event ? '_blank' : '' } })"
|
||||
>
|
||||
新窗口打开
|
||||
</n-checkbox>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item label="悬浮内容">
|
||||
<div class="float-more-editor__block">
|
||||
<div class="float-more-editor__subtitle">鼠标移到此处时显示内容</div>
|
||||
<n-input
|
||||
type="textarea"
|
||||
:value="btn.hover"
|
||||
:autosize="{ minRows: 2, maxRows: 8 }"
|
||||
@update:value="patch(index, { hover: $event })"
|
||||
/>
|
||||
<div class="float-more-editor__desc">设置此处后,按钮链接将失效<br>支持HTML代码,请注意代码规范及标签闭合</div>
|
||||
</div>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="btn.hover" label="悬浮内容宽度">
|
||||
<div class="float-more-editor__block">
|
||||
<n-input-number
|
||||
:value="btn.hover_width"
|
||||
:min="50"
|
||||
:max="500"
|
||||
:step="10"
|
||||
style="width: 180px;"
|
||||
@update:value="patch(index, { hover_width: $event ?? 200 })"
|
||||
>
|
||||
<template #suffix>PX</template>
|
||||
</n-input-number>
|
||||
<div class="float-more-editor__desc">移动端宽度最大240px</div>
|
||||
</div>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<FloatBtnCommonFields :model-value="btn" @update:model-value="replace(index, $event)" />
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
|
||||
<div class="float-more-editor__footer">
|
||||
<n-button size="small" type="primary" @click="add">
|
||||
<template #icon><i class="fa fas fa-plus" /></template>
|
||||
添加按钮
|
||||
</n-button>
|
||||
<span class="float-more-editor__desc">此处内容至少保留一个,如一个都无需显示请关闭PC端和移动端显示即可</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FaIconPicker from '@/components/form/FaIconPicker.vue'
|
||||
import IconValue from '@/components/form/IconValue.vue'
|
||||
import FloatBtnCommonFields from './FloatBtnCommonFields.vue'
|
||||
import { defaultMoreButton } from './floating-defaults.js'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Array, required: true },
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
function replace(index, value) {
|
||||
const next = props.modelValue.slice()
|
||||
next.splice(index, 1, value)
|
||||
emit('update:modelValue', next)
|
||||
}
|
||||
|
||||
function patch(index, partial) {
|
||||
replace(index, { ...props.modelValue[index], ...partial })
|
||||
}
|
||||
|
||||
function add() {
|
||||
emit('update:modelValue', [...props.modelValue, defaultMoreButton()])
|
||||
}
|
||||
|
||||
function remove(index) {
|
||||
if (props.modelValue.length <= 1) return
|
||||
const next = props.modelValue.slice()
|
||||
next.splice(index, 1)
|
||||
emit('update:modelValue', next)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.float-more-editor__head {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.float-more-editor__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.float-more-editor__block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.float-more-editor__subtitle,
|
||||
.float-more-editor__desc {
|
||||
font-size: 12px;
|
||||
color: var(--n-text-color-3);
|
||||
}
|
||||
|
||||
.float-more-editor__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 右侧悬浮按钮:类型元数据与默认值(与 src/Settings/FloatButtons.php 保持一致)。
|
||||
*/
|
||||
|
||||
export const emptyColor = () => ({ color: '', bg: '' })
|
||||
|
||||
/** 类型元数据:面板标题、折叠标题、黄色提示。 */
|
||||
export const FLOAT_BTN_META = {
|
||||
back_top: { title: '返回顶部', panel: '返回顶部', warn: '此按钮只能放置在最顶部或者最底部!' },
|
||||
qrcode: { title: '当前页面二维码', panel: '当前页面二维码' },
|
||||
add: { title: 'Add发布内容', panel: '发布投稿', warn: '此按钮尽量放在上方,否则会显示遮挡' },
|
||||
theme_mode: { title: '日夜主题切换', panel: '日夜主题切换' },
|
||||
pay_vip: { title: 'VIP购买', panel: 'VIP购买' },
|
||||
service_qq: { title: 'QQ客服', panel: 'QQ客服' },
|
||||
service_wechat: { title: '微信客服', panel: '微信客服' },
|
||||
more: { title: '更多按钮', panel: '添加更多自定义按钮' },
|
||||
}
|
||||
|
||||
/** 发布按钮可选项。 */
|
||||
export const ADD_BTN_OPTIONS = [{ label: '文章投稿', value: 'post' }]
|
||||
|
||||
export function defaultMoreButton() {
|
||||
return {
|
||||
desc: '',
|
||||
icon: 'fa fas fa-heart',
|
||||
link: { url: '', target: '' },
|
||||
hover: '',
|
||||
hover_width: 200,
|
||||
pc_s: false,
|
||||
m_s: false,
|
||||
color: { color: '', bg: 'rgba(255, 111, 6, 0.2)' },
|
||||
}
|
||||
}
|
||||
|
||||
export function defaultItems() {
|
||||
return [
|
||||
{ type: 'more', buttons: [defaultMoreButton()] },
|
||||
{ type: 'pay_vip', pc_s: true, m_s: true, color: { color: '#f2c97d', bg: 'rgba(62,62,67,0.9)' } },
|
||||
{ type: 'add', pc_s: true, m_s: false, color: emptyColor(), btns: ['post'], icon: 'fa fas fa-pencil-alt' },
|
||||
{ type: 'service_qq', pc_s: false, m_s: false, color: emptyColor(), qq: '' },
|
||||
{ type: 'service_wechat', pc_s: true, m_s: true, color: emptyColor(), wechat_img: '' },
|
||||
{ type: 'theme_mode', pc_s: false, m_s: false, color: emptyColor() },
|
||||
{ type: 'qrcode', pc_s: true, m_s: false, color: emptyColor(), desc: '在手机上浏览此页面' },
|
||||
{ type: 'back_top', pc_s: true, m_s: true, color: emptyColor() },
|
||||
]
|
||||
}
|
||||
|
||||
export function defaultSettings() {
|
||||
return {
|
||||
style: 'round',
|
||||
position: 'bottom',
|
||||
filter_blur: ['m_s'],
|
||||
scroll_hide: false,
|
||||
scroll_down_hide: true,
|
||||
items: defaultItems(),
|
||||
}
|
||||
}
|
||||
|
||||
/** 规范化颜色组:确保 color/bg 都为字符串。 */
|
||||
export function normalizeColor(color) {
|
||||
const c = color && typeof color === 'object' ? color : {}
|
||||
return { color: c.color || '', bg: c.bg || '' }
|
||||
}
|
||||
|
||||
/** 用默认值补齐服务端返回的按钮(按服务端顺序)。 */
|
||||
export function normalizeItems(items) {
|
||||
const defaults = Object.fromEntries(defaultItems().map((item) => [item.type, item]))
|
||||
const source = Array.isArray(items) && items.length ? items : defaultItems()
|
||||
const out = []
|
||||
const seen = new Set()
|
||||
for (const raw of source) {
|
||||
const base = defaults[raw?.type]
|
||||
if (!base || seen.has(raw.type)) continue
|
||||
seen.add(raw.type)
|
||||
if (raw.type === 'more') {
|
||||
const buttons = (Array.isArray(raw.buttons) && raw.buttons.length ? raw.buttons : base.buttons).map((btn) => ({
|
||||
...defaultMoreButton(),
|
||||
...btn,
|
||||
link: { url: btn?.link?.url || '', target: btn?.link?.target || '' },
|
||||
color: normalizeColor(btn?.color),
|
||||
}))
|
||||
out.push({ type: 'more', buttons })
|
||||
continue
|
||||
}
|
||||
out.push({ ...base, ...raw, color: normalizeColor(raw.color) })
|
||||
}
|
||||
for (const item of defaultItems()) {
|
||||
if (!seen.has(item.type)) out.push(item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
||||
export const floatingSchema = {
|
||||
key: 'global.floating',
|
||||
title: '右侧悬浮按钮',
|
||||
icon: 'fa fas fa-ellipsis-v',
|
||||
description: '页面右侧浮动按钮的显示与配置。',
|
||||
defaults: {},
|
||||
groups: [],
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import { layoutSchema } from './layout.js'
|
||||
import { searchSchema } from './search.js'
|
||||
// Email schema is served by the PHP template catalog to keep defaults and fields canonical.
|
||||
import { tabbarSchema } from './tabbar.js'
|
||||
import { floatingSchema } from './floating.js'
|
||||
import { i18nSchema } from './i18n.js'
|
||||
import { customCodeSchema } from './custom-code.js'
|
||||
import { seoSchema } from './seo.js'
|
||||
@@ -23,7 +22,6 @@ export const globalSchemas = {
|
||||
search: searchSchema,
|
||||
email: null,
|
||||
tabbar: tabbarSchema,
|
||||
floating: floatingSchema,
|
||||
i18n: i18nSchema,
|
||||
'custom-code': customCodeSchema,
|
||||
seo: seoSchema,
|
||||
|
||||
Reference in New Issue
Block a user