refactor(color): unify color sanitizing and picking behind shared components
- Add AKSRT\Core\Color::sanitize($value, $allow_alpha) as the single PHP color sanitizer; replace sanitize_hex_color calls and per-class regexes (FloatButtons, FieldSchema, ThemeSearch, icon cards, TicketStatusService) - SchemaProvider / ControllerBase sanitize `color` fields as colors; alpha kept only when the schema sets showAlpha - Split SchemaProvider OAuth validation and option matching into sibling traits to stay under the 1000-line limit - Add AksColorPicker.vue + utils/color.js mirroring the PHP rules; AksField, FloatBtnCommonFields and TicketStatusPanel use it - Widget background color input now uses wpColorPicker - Remove dead AksCompositeField dimensions/spacing/background branches and the unused Gutenberg ColorPicker var - Add color-unification contract test; update harness requires and tests Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
74021d26bb
commit
57ff405691
32 files changed
+522
-456
No files matched your search
@@ -10,22 +10,20 @@
|
||||
<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']"
|
||||
<AksColorPicker
|
||||
:model-value="modelValue.color?.color || ''"
|
||||
alpha
|
||||
clearable
|
||||
@update:value="updateColor('color', $event)"
|
||||
@update:model-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']"
|
||||
<AksColorPicker
|
||||
:model-value="modelValue.color?.bg || ''"
|
||||
alpha
|
||||
clearable
|
||||
@update:value="updateColor('bg', $event)"
|
||||
@update:model-value="updateColor('bg', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,6 +33,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AksColorPicker from '@/components/form/AksColorPicker.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Object, required: true },
|
||||
})
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<n-color-picker
|
||||
:value="pickerValue"
|
||||
:modes="modes"
|
||||
:show-alpha="alpha"
|
||||
:swatches="swatches"
|
||||
:actions="clearable ? ['clear'] : undefined"
|
||||
:disabled="disabled"
|
||||
@update:value="onUpdate"
|
||||
>
|
||||
<template v-if="$slots.trigger" #trigger="slotProps">
|
||||
<slot name="trigger" v-bind="slotProps" />
|
||||
</template>
|
||||
</n-color-picker>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/**
|
||||
* 后台唯一取色器:所有颜色字段都经由此组件,输出与
|
||||
* PHP `AKSRT\Core\Color::sanitize($value, $allow_alpha)` 同一规则的规范值。
|
||||
* 清空 / 非法值统一回传空串。
|
||||
*/
|
||||
import { computed } from 'vue'
|
||||
import { normalizeColor } from '@/utils/color.js'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
alpha: { type: Boolean, default: false },
|
||||
clearable: { type: Boolean, default: false },
|
||||
swatches: { type: Array, default: undefined },
|
||||
disabled: { type: Boolean, default: false },
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const modes = computed(() => (props.alpha ? ['hex', 'rgb'] : ['hex']))
|
||||
const pickerValue = computed(() => normalizeColor(props.modelValue, props.alpha) || null)
|
||||
|
||||
function onUpdate(value) {
|
||||
emit('update:modelValue', normalizeColor(value, props.alpha))
|
||||
}
|
||||
</script>
|
||||
@@ -1,35 +1,6 @@
|
||||
<template>
|
||||
<!-- Dimensions (width x height) -->
|
||||
<div v-if="schema.type === 'dimensions'" class="aksrt-composite aksrt-composite--dimensions">
|
||||
<n-input-number
|
||||
:value="current.width"
|
||||
:min="0"
|
||||
:placeholder="schema.widthPlaceholder || '宽度'"
|
||||
:disabled="schema.disabled"
|
||||
style="flex: 1"
|
||||
@update:value="update('width', $event)"
|
||||
/>
|
||||
<span class="aksrt-composite__sep">×</span>
|
||||
<n-input-number
|
||||
:value="current.height"
|
||||
:min="0"
|
||||
:placeholder="schema.heightPlaceholder || '高度'"
|
||||
:disabled="schema.disabled"
|
||||
style="flex: 1"
|
||||
@update:value="update('height', $event)"
|
||||
/>
|
||||
<n-select
|
||||
v-if="schema.unit"
|
||||
:value="current.unit"
|
||||
:options="dimensionUnitOptions"
|
||||
:disabled="schema.disabled"
|
||||
style="width: 80px"
|
||||
@update:value="update('unit', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Between number (min/max pair) -->
|
||||
<div v-else-if="schema.type === 'between_number'" class="aksrt-composite aksrt-composite--between">
|
||||
<div class="aksrt-composite aksrt-composite--between">
|
||||
<n-input-number
|
||||
:value="betweenValue('min')"
|
||||
:min="schema.min"
|
||||
@@ -53,56 +24,11 @@
|
||||
/>
|
||||
<span v-if="schema.unit" class="aksrt-composite__unit">{{ schema.unit }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Spacing (top right bottom left) -->
|
||||
<div v-else-if="schema.type === 'spacing'" class="aksrt-composite--spacing">
|
||||
<template v-for="(side, i) in spacingSides" :key="side">
|
||||
<div class="aksrt-composite__label">{{ schema.spacingLabels?.[i] || spacingDefaults[i] }}</div>
|
||||
<n-input-number
|
||||
:value="current[side]"
|
||||
:min="0"
|
||||
:disabled="schema.disabled"
|
||||
@update:value="update(side, $event)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Background (color + image + size + position + repeat) -->
|
||||
<div v-else-if="schema.type === 'background'" class="aksrt-composite--background">
|
||||
<div class="aksrt-composite__row">
|
||||
<label class="aksrt-composite__label">背景颜色</label>
|
||||
<n-color-picker
|
||||
:value="current.color"
|
||||
:show-alpha="true"
|
||||
:disabled="schema.disabled"
|
||||
@update:value="update('color', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="aksrt-composite__row">
|
||||
<label class="aksrt-composite__label">背景图片</label>
|
||||
<AksMediaField
|
||||
:model-value="current.image"
|
||||
:accept="'image/*'"
|
||||
:disabled="schema.disabled"
|
||||
@update:model-value="update('image', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="row in backgroundSelects" :key="row.key" class="aksrt-composite__row">
|
||||
<label class="aksrt-composite__label">{{ row.label }}</label>
|
||||
<n-select
|
||||
:value="current[row.key]"
|
||||
:options="row.options"
|
||||
:disabled="schema.disabled"
|
||||
@update:value="update(row.key, $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
/** 对象型复合字段:dimensions / between_number / spacing / background。 */
|
||||
/** 对象型复合字段:between_number(min / max 数值区间)。 */
|
||||
import { computed } from 'vue'
|
||||
import AksMediaField from './AksMediaField.vue'
|
||||
|
||||
const props = defineProps({
|
||||
schema: { type: Object, required: true },
|
||||
@@ -123,43 +49,6 @@ function update(part, value) {
|
||||
function betweenValue(part) {
|
||||
return current.value[part] !== undefined ? current.value[part] : (props.schema.default?.[part] ?? 0)
|
||||
}
|
||||
|
||||
const dimensionUnitOptions = ['px', '%', 'em', 'rem', 'vw'].map(unit => ({ label: unit, value: unit }))
|
||||
const spacingSides = ['top', 'right', 'bottom', 'left']
|
||||
const spacingDefaults = ['上', '右', '下', '左']
|
||||
|
||||
const backgroundSelects = [
|
||||
{
|
||||
key: 'size',
|
||||
label: '图片尺寸',
|
||||
options: [
|
||||
{ label: '覆盖', value: 'cover' },
|
||||
{ label: '包含', value: 'contain' },
|
||||
{ label: '原始大小', value: 'auto' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'position',
|
||||
label: '图片位置',
|
||||
options: [
|
||||
{ label: '居中', value: 'center center' },
|
||||
{ label: '顶部居中', value: 'top center' },
|
||||
{ label: '底部居中', value: 'bottom center' },
|
||||
{ label: '左侧居中', value: 'center left' },
|
||||
{ label: '右侧居中', value: 'center right' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'repeat',
|
||||
label: '图片重复',
|
||||
options: [
|
||||
{ label: '不重复', value: 'no-repeat' },
|
||||
{ label: '平铺', value: 'repeat' },
|
||||
{ label: '水平平铺', value: 'repeat-x' },
|
||||
{ label: '垂直平铺', value: 'repeat-y' },
|
||||
],
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -183,49 +72,4 @@ const backgroundSelects = [
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksrt-composite--spacing {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto 1fr auto 1fr auto 1fr;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.aksrt-composite--background {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.aksrt-composite__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.aksrt-composite__label {
|
||||
min-width: 70px;
|
||||
color: var(--aksrt-color-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.aksrt-composite--spacing .aksrt-composite__label {
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.aksrt-composite--dimensions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.aksrt-composite--spacing {
|
||||
grid-template-columns: auto 1fr;
|
||||
}
|
||||
|
||||
.aksrt-composite__row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -199,13 +199,13 @@
|
||||
</n-checkbox-group>
|
||||
|
||||
<!-- Color picker -->
|
||||
<n-color-picker
|
||||
<AksColorPicker
|
||||
v-else-if="schema.type === 'color'"
|
||||
:value="modelValue"
|
||||
:modes="['hex']"
|
||||
:show-alpha="schema.showAlpha || false"
|
||||
:disabled="schema.disabled"
|
||||
@update:value="emit('update:modelValue', $event)"
|
||||
:model-value="typeof modelValue === 'string' ? modelValue : ''"
|
||||
:alpha="!!schema.showAlpha"
|
||||
:disabled="!!schema.disabled"
|
||||
clearable
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
|
||||
<div v-else-if="schema.type === 'palette'" class="aksrt-field__palette">
|
||||
@@ -315,9 +315,9 @@
|
||||
@update:value="emit('update:modelValue', $event)"
|
||||
/>
|
||||
|
||||
<!-- Object composites: dimensions / between_number / spacing / background -->
|
||||
<!-- Object composite: between_number (min/max pair) -->
|
||||
<AksCompositeField
|
||||
v-else-if="['dimensions', 'between_number', 'spacing', 'background'].includes(schema.type)"
|
||||
v-else-if="schema.type === 'between_number'"
|
||||
:schema="schema"
|
||||
:model-value="modelValue"
|
||||
@update:model-value="emit('update:modelValue', $event)"
|
||||
@@ -525,6 +525,7 @@ 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'
|
||||
|
||||
Reference in new issue
Block a user