color-picker封装(开箱即用)
目录
调整颜色色相(hue 值)的滑块代码:HueSlider.vue
调整颜色透明度(alpha 值)的滑块代码:AlphaSlider.vue
由于UI需求,需要使用直接取色的取色器面板,有点类似于element-plus里的el-color-pick,但是不需要有展开的操作。于是模仿element-plus写了该组件。技术栈vue3.js + javascript
该组件包含三个部分:
- 一个饱和度 - 明度(SV)面板,用于调整颜色的饱和度和明度;
- 用于调整颜色色相(hue 值)的滑块;
-
用于调整颜色透明度(alpha 值)的滑块。
代码实现包含:index.vue、SvPanel.vue、HueSlider.vue、AlphaSlider.vue、utils.js
入口代码:index.vue
<template>
<div class="color-dropdown__main-wrapper">
<SvPanel ref="svPanel" :color="state.color"></SvPanel>
<HueSlider ref="hueSlider" :color="state.color"></HueSlider>
</div>
<AlphaSlider ref="alphaSlider" :color="state.color"></AlphaSlider>
</template>
<script setup>
import {
reactive,
onBeforeUnmount,
onMounted,
watch,
ref,
computed,
} from "vue";
import SvPanel from "./SvPanel.vue";
import HueSlider from "./HueSlider.vue";
import AlphaSlider from "./AlphaSlider.vue";
import Color from "/public/native/core/color.js";
const state = reactive({
color: new Color(),
});
const emits = defineEmits({
change: null,
});
const alphaSlider = ref(null);
let colorValue = computed(() => {
const hue = state.color.get("hue");
const value = state.color.get("value");
const saturation = state.color.get("saturation");
const alpha = state.color.get("alpha");
return { hue, value, saturation, alpha };
});
let props = defineProps({
modelValue: {
type: String,
required: false,
default: "#ffffff",
},
});
watch(
() => colorValue,
(colorValue) => {
alphaSlider.value.update();
let alpha = colorValue.value.alpha
? colorValue.value.alpha / 101
: 0.99;
emits("change", {
color: state.color.tohex(),
alpha,
});
},
{ deep: true }
);
watch(
() => props.modelValue,
(modelValue) => {
console.log("modelValue-change:", modelValue);
state.color.fromHex(modelValue);
}
);
onMounted(() => {
state.color.fromHex(props.modelValue);
});
onBeforeUnmount(() => {
//console.log('onBeforeUnmount')
});
</script>
<style scoped lang="less">
.color-dropdown__main-wrapper {
margin-bottom: 3px;
display: flex;
&:after {
content: "";
display: table;
clear: both;
}
}
</style>
饱和度 - 明度(SV)面板代码:SvPanel.vue
<template>
<div
class="color-svpanel"
:style="{
backgroundColor: state.background,
}"
>
<div class="color-svpanel__white"></div>
<div class="color-svpanel__black"></div>
<div
class="color-svpanel__cursor"
:style="{
top: state.cursorTop + 'px',
left: state.cursorLeft + 'px',
}"
>
<div></div>
</div>
</div>
</template>
<script setup>
import {
reactive,
onBeforeUnmount,
onMounted,
getCurrentInstance,
computed,
watch,
} from "vue";
import { draggable, getClientXY } from "./utils.js";
let { vnode } = getCurrentInstance();
let props = defineProps({
color: {
type: Object,
required: true,
},
});
const state = reactive({
cursorTop: 0,
cursorLeft: 0,
background: "hsl(0, 100%, 50%)",
});
let colorValue = computed(() => {
const hue = props.color.get("hue");
const value = props.color.get("value");
return { hue, value };
});
watch(
() => colorValue.value,
() => {
update();
}
);
onMounted(() => {
draggable(vnode.el, {
drag: (event) => {
handleDrag(event);
},
end: (event) => {
handleDrag(event);
},
});
update();
});
onBeforeUnmount(() => {
//console.log('onBeforeUnmount')
});
function update() {
const saturation = props.color.get("saturation");
const value = props.color.get("value");
const el = vnode.el;
const { clientWidth, clientHeight } = el;
state.cursorLeft = (saturation * clientWidth) / 100;
state.cursorTop = ((100 - value) * clientHeight) / 100;
state.background = `hsl(${props.color.get("hue")}, 100%, 50%)`;
}
function handleDrag(event) {
const el = vnode.el;
const rect = el.getBoundingClientRect();
const { clientX, clientY } = getClientXY(event);
let left = clientX - rect.left;
let top = clientY - rect.top;
left = Math.max(0, left);
left = Math.min(left, rect.width);
top = Math.max(0, top);
top = Math.min(top, rect.height);
state.cursorLeft = left;
state.cursorTop = top;
props.color.set({
saturation: (left / rect.width) * 100,
value: 100 - (top / rect.height) * 100,
});
}
</script>
<style scoped lang="less">
.color-svpanel {
position: relative;
width: 128px;
height: 82px;
margin-right: 3px;
background-color: #fff;
.color-svpanel__white,
.color-svpanel__black {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.color-svpanel__white {
background: linear-gradient(to right, #fff, rgba(255, 255, 255, 0));
}
.color-svpanel__black {
background: linear-gradient(to top, #000, rgba(0, 0, 0, 0));
}
.color-svpanel__cursor {
position: absolute;
& > div {
cursor: head;
width: 4px;
height: 4px;
box-shadow: 0 0 0 1.5px #fff, inset 0 0 1px 1px #0000004d,
0 0 1px 2px #0006;
border-radius: 50%;
transform: translate(-2px, -2px);
}
}
}
</style>
调整颜色色相(hue 值)的滑块代码:HueSlider.vue
<template>
<div class="color-hue-slider is-vertical hue-slider">
<div class="color-hue-slider__bar" ref="bar"></div>
<div
class="color-hue-slider__thumb"
ref="thumb"
:style="{
left: 0,
top: state.thumbTop + 'px',
}"
></div>
</div>
</template>
<script setup>
import {
ref,
reactive,
onBeforeUnmount,
onMounted,
getCurrentInstance,
watch,
defineProps,
nextTick,
} from "vue";
import { draggable, getClientXY } from "./utils.js";
let { vnode } = getCurrentInstance();
let props = defineProps({
color: {
type: Object,
required: true,
},
});
const state = reactive({
thumbLeft: 0,
thumbTop: 0,
hue: 0,
});
const thumb = ref(null);
const bar = ref(null);
onMounted(() => {
const dragConfig = {
drag: (event) => {
handleDrag(event);
},
end: (event) => {
handleDrag(event);
},
};
draggable(bar.value, dragConfig);
draggable(thumb.value, dragConfig);
nextTick(() => {
update();
});
});
onBeforeUnmount(() => {
//console.log('onBeforeUnmount')
});
watch(
() => state.hue,
(hue) => {
update();
}
);
function handleDrag(event) {
if (!bar.value || !thumb.value) return;
const el = vnode.el;
const rect = el.getBoundingClientRect();
const { clientY } = getClientXY(event);
let top = clientY - rect.top;
top = Math.min(top, rect.height - thumb.value.offsetHeight / 2);
top = Math.max(thumb.value.offsetHeight / 2, top);
state.hue = Math.round(
((top - thumb.value.offsetHeight / 2) /
(rect.height - thumb.value.offsetHeight)) *
360
);
props.color.set("hue", state.hue);
}
function update() {
state.thumbTop = getThumbTop();
}
function getThumbTop() {
if (!thumb.value) return 0;
const el = vnode.el;
if (!el) return 0;
return Math.round(
(state.hue * (el.offsetHeight - thumb.value.offsetHeight / 2)) / 360
);
}
</script>
<style scoped lang="less">
.color-hue-slider__bar {
position: relative;
background: linear-gradient(
to right,
#f00 0%,
#ff0 17%,
#0f0 33%,
#0ff 50%,
#00f 67%,
#f0f 83%,
#f00 100%
);
height: 100%;
}
.color-hue-slider__thumb {
position: absolute;
cursor: pointer;
box-sizing: border-box;
left: 0;
top: 0;
width: 4px;
height: 100%;
border-radius: 1px;
background: #fff;
border: 1px solid var(--el-border-color-lighter);
box-shadow: 0 0 2px #0009;
z-index: 1;
}
.color-hue-slider {
position: relative;
box-sizing: border-box;
width: 280px;
height: 12px;
background-color: red;
padding: 0 2px;
float: right;
&.is-vertical {
width: 6px;
height: 82px;
padding: 2px 0;
.color-hue-slider__bar {
background: linear-gradient(
to bottom,
#f00 0%,
#ff0 17%,
#0f0 33%,
#0ff 50%,
#00f 67%,
#f0f 83%,
#f00 100%
);
}
.color-hue-slider__thumb {
left: 0;
top: 0;
width: 100%;
height: 4px;
}
}
}
</style>
调整颜色透明度(alpha 值)的滑块代码:AlphaSlider.vue
<template>
<div class="color-alpha-slider">
<div
ref="bar"
class="color-alpha-slider__bar"
@click="handleClick"
:style="{ background: state.background }"
></div>
<div
ref="thumb"
class="color-alpha-slider__thumb"
:style="thumbStyle"
></div>
</div>
</template>
<script setup>
import {
reactive,
onBeforeUnmount,
onMounted,
getCurrentInstance,
ref,
computed,
watch,
} from "vue";
import { draggable, getClientXY } from "./utils.js";
let { vnode } = getCurrentInstance();
const state = reactive({
thumbLeft: 0,
thumbTop: 0,
background: "",
});
let props = defineProps({
color: {
type: Object,
required: true,
},
vertical: {
type: Boolean,
default: false,
},
});
const thumb = ref(null);
const bar = ref(null);
const thumbStyle = computed(() => ({
left: addUnit(state.thumbLeft),
top: addUnit(state.thumbTop),
}));
watch(
() => props.color.get("alpha"),
() => {
update();
}
);
watch(
() => props.color.get("value"),
() => {
update();
}
);
onMounted(() => {
if (!bar.value || !thumb.value) return;
const dragConfig = {
drag: (event) => {
handleDrag(event);
},
end: (event) => {
handleDrag(event);
},
};
draggable(bar.value, dragConfig);
draggable(thumb.value, dragConfig);
update();
});
onBeforeUnmount(() => {
//console.log('onBeforeUnmount')
});
function handleClick(event) {
const target = event.target;
if (target !== thumb.value) {
handleDrag(event);
}
}
function handleDrag(event) {
if (!bar.value || !thumb.value) return;
const el = vnode.el;
const rect = el.getBoundingClientRect();
const { clientX } = getClientXY(event);
let left = clientX - rect.left;
left = Math.max(thumb.value.offsetWidth / 2, left);
left = Math.min(left, rect.width - thumb.value.offsetWidth / 2);
props.color.set(
"alpha",
Math.round(
((left - thumb.value.offsetWidth / 2) /
(rect.width - thumb.value.offsetWidth)) *
100
)
);
}
function update() {
state.thumbLeft = getThumbLeft();
state.thumbTop = getThumbTop();
state.background = getBackground();
}
function getThumbLeft() {
if (!thumb.value) return 0;
if (props.vertical) return 0;
const el = vnode.el;
const alpha = props.color.get("alpha");
if (!el) return 0;
return Math.round(
(alpha * (el.offsetWidth - thumb.value.offsetWidth / 2)) / 100
);
}
function getThumbTop() {
if (!thumb.value) return 0;
const el = vnode.el;
if (!props.vertical) return 0;
const alpha = props.color.get("alpha");
if (!el) return 0;
return Math.round(
(alpha * (el.offsetHeight - thumb.value.offsetHeight / 2)) / 100
);
}
function getBackground() {
if (props.color && props.color.get("value")) {
const { r, g, b } = props.color.toRgba();
return `linear-gradient(to right, rgba(${r}, ${g}, ${b}, 0) 0%, rgba(${r}, ${g}, ${b}, 1) 100%)`;
}
return "";
}
function addUnit(value, defaultUnit = "px") {
if (!value) return "";
if (isNumber(value) || isStringNumber(value)) {
return `${value}${defaultUnit}`;
} else if (isString(value)) {
return value;
}
}
const isStringNumber = (val) => {
if (!isString(val)) {
return false;
}
return !Number.isNaN(Number(val));
};
const isNumber = (val) => typeof val === "number";
const isString = (val) => typeof val === "string";
defineExpose({
update,
bar,
thumb,
});
</script>
<style scoped lang="less">
.color-alpha-slider {
position: relative;
box-sizing: border-box;
width: 128px;
height: 8px;
background-image: linear-gradient(45deg, #ccc 25%, transparent 25%),
linear-gradient(135deg, #ccc 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #ccc 75%),
linear-gradient(135deg, transparent 75%, #ccc 75%);
background-size: 8px 8px;
background-position: 0 0, 4px 0, 4px -4px, 0 4px;
.color-alpha-slider__bar {
position: relative;
background: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
#ffffff 100%
);
height: 100%;
}
.color-alpha-slider__thumb {
position: absolute;
cursor: pointer;
box-sizing: border-box;
left: 0;
top: 0;
width: 4px;
height: 100%;
border-radius: 1px;
background: #fff;
border: 1px solid #ebeef5;
box-shadow: 0 0 2px #0009;
z-index: 1;
}
}
</style>
工具代码:utils.js
let isDragging = false;
export function draggable(element, options) {
const moveFn = function (event) {
options.drag?.(event);
};
const upFn = function (event) {
document.removeEventListener("mousemove", moveFn);
document.removeEventListener("mouseup", upFn);
document.removeEventListener("touchmove", moveFn);
document.removeEventListener("touchend", upFn);
document.onselectstart = null;
document.ondragstart = null;
isDragging = false;
options.end?.(event);
};
const downFn = function (event) {
if (isDragging) return;
event.preventDefault();
document.onselectstart = () => false;
document.ondragstart = () => false;
document.addEventListener("mousemove", moveFn);
document.addEventListener("mouseup", upFn);
document.addEventListener("touchmove", moveFn);
document.addEventListener("touchend", upFn);
isDragging = true;
options.start?.(event);
};
element.addEventListener("mousedown", downFn);
element.addEventListener("touchstart", downFn);
}
export const getClientXY = (event) => {
let clientX;
let clientY;
if (event.type === "touchend") {
clientY = event.changedTouches[0].clientY;
clientX = event.changedTouches[0].clientX;
} else if (event.type.startsWith("touch")) {
clientY = event.touches[0].clientY;
clientX = event.touches[0].clientX;
} else {
clientY = event.clientY;
clientX = event.clientX;
}
return {
clientX,
clientY,
};
};
使用示例:
<template>
<ColorPicker
v-model="color"
@change="changeColor"
></ColorPicker>
</template>
<script setup>
import { ref } from "vue";
import ColorPicker from "./color-picker/index.vue";
const color=ref("#ffffff")
function changeColor({ color, alpha }) {
console.log(color,alpha)
}
</script>
成果图片:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理