这个选择器也挺好看的, 只是貌似不能外部打开选择器面板
官网:Webpack Apphttps://aesoper101.github.io/vue3-colorpicker/?path=/story/example-introduction--page
优点:
- 支持外部打开选择器面板
- 支持渐变色
安装
npm i -S vue3-colorpicker
引入
import Vue3ColorPicker from "vue3-colorpicker";
import "vue3-colorpicker/style.css";
createApp(App)
.use(router)
.use(Vue3ColorPicker)
.mount("#app");
使用:
<template>
<div class="main">
<el-button @click="isShow = true">打开</el-button>
{
{ pureColor }}
<div v-show="isShow" style="box-shadow: 0 0 10px #00000026">
<color-picker
:pureColor="pureColor"
:isWidget="isShow"
:disableHistory="true"
format="hex"
:zIndex="1"
@pureColorChange="handle_pureColorChange"
/>
<div style="text-align: right; padding: 0 16px 10px 16px">
<el-button @click="isShow = false">确定</el-button>
<el-button @click="isShow = false">关闭</el-button>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
const pureColor = ref("#71afe5");
const isShow = ref(false);
//
const handle_pureColorChange = (e) => {
console.log(e);
pureColor.value = e;
};
</script>
<style lang="scss" scoped>
.main {
:deep(.vc-colorpicker) {
padding: 0;
box-shadow: none;
border-radius: 0;
}
:deep(.vc-color-wrap) {
width: 0;
}
}
</style>
效果
封装为组件:ColorPickerV3.vue
点击选择器外面可自动关闭选择器
ColorPickerV3.vue:
<template>
<div class="main" ref="ref_main" v-if="isShow">
<div style="box-shadow: 0 0 10px #00000026">
<color-picker
:pureColor="pureColor"
:isWidget="isShow"
:disableHistory="true"
:pickerType="pickerType"
format="hex"
:zIndex="1"
:disableAlpha="disableAlpha"
@pureColorChange="handle_pureColorChange"
/>
<div
style="
text-align: right;
padding: 0 16px 10px 16px;
background-color: white;
"
>
<el-button type="primary" @click="confirm">确定</el-button>
<el-button @click="isShowColorPicker(false)">关闭</el-button>
</div>
</div>
</div>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue';
const pureColor = ref('#71afe5');
const isShow = ref(false);
const ref_main = ref();
// 点击选择器外面会自动关闭选择器
const isFirstOpen = ref(false);
onMounted(() => {
document.addEventListener('click', (e) => {
if (!ref_main.value?.contains(e.target) && !isFirstOpen.value) {
isShow.value = false;
}
isFirstOpen.value = false;
});
});
const props = defineProps({
color: { type: String, default: '' },
pickerType: { type: String, default: 'fk' }, //fk 、chrome
disableAlpha: { type: Boolean, default: true },
});
// 初始化颜色
pureColor.value = props.color;
watch(
() => props.color,
() => {
pureColor.value = props.color;
},
);
// 设置颜色选择器面板显隐
const isShowColorPicker = (data = false) => {
isShow.value = data;
isFirstOpen.value = true;
};
// 颜色变化事件
const handle_pureColorChange = (e) => {
pureColor.value = e;
};
// 定义确定事件
const emit = defineEmits(['confirm']);
// 确定颜色
const confirm = () => {
isShow.value = false;
emit('confirm', pureColor.value);
};
// 暴露方法出去
defineExpose({
isShowColorPicker,
confirm,
});
</script>
<style lang="scss" scoped>
.main {
:deep(.vc-colorpicker) {
padding: 0;
box-shadow: none;
border-radius: 0;
}
:deep(.vc-color-wrap) {
width: 0;
}
}
</style>
使用:
<template>
<div class="main">
<el-button @click="open">打开</el-button>{
{ selectedColor }}
<ColorPickerV3
ref="ref_ColorPickerV3"
color="orange"
@confirm="confirm"
pickerType="chrome"
></ColorPickerV3>
</div>
</template>
<script setup>
import { ref } from "vue";
import ColorPickerV3 from "./components/ColorPickerV3.vue";
const selectedColor = ref();
const ref_ColorPickerV3 = ref();
// 打开颜色选择器
const open = () => {
ref_ColorPickerV3.value.isShowColorPicker(true);
};
// 确定颜色
const confirm = (color) => {
console.log("color:", color);
selectedColor.value = color;
};
</script>
<style lang="scss" scoped>
.main {
}
</style>
例2
缺点:不能在颜色面板随意拖动,只能点击颜色
ColorPicker.vue:
<template>
<div class="modal hu-color-picker">
<div class="color-panel">
<!-- 默认颜色列表选择区 -->
<ul class="colors color-box">
<li
v-for="item in colorsDefault"
:key="item"
class="item"
:style="{ background: item }"
@click="selectColor(item)"
></li>
</ul>
<div class="color-set">
<!-- 颜色面板 -->
<div class="saturation" @mousedown.prevent.stop="selectSaturation">
<canvas ref="canvasSaturationRef" width="150" height="80"></canvas>
<div :style="position.pointPosition" class="slide"></div>
</div>
<!-- 颜色卡条 -->
<div class="hue" @mousedown.prevent.stop="selectHue">
<canvas ref="canvasHueRef" width="12" height="80"></canvas>
<div :style="position.slideHueStyle" class="slide"></div>
</div>
</div>
</div>
<!-- 颜色预览和颜色输入 -->
<div class="color-view">
<!-- 颜色预览区 -->
<div :style="{ background: rgbString }" class="color-show"></div>
<!-- 颜色输入区 -->
<div class="input">
<div class="color-type">
<span class="name"> HEX </span>
<input v-model="attr.modelHex" class="value" @blur="inputHex" />
</div>
<div class="color-type">
<span class="name"> RGB </span>
<input v-model="attr.modelRgb" class="value" @blur="inputRgb" />
</div>
</div>
</div>
<div class="btn">
<button>清空</button>
<button @click="changeColor">确认</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, nextTick, onMounted, watch } from "vue";
import {
rgb2hex,
createLinearGradient,
hex2rgb,
rgb2hsv,
isHex,
isRgb,
} from "./ColorPicker";
const props = defineProps({
initColor: {
type: String,
default: "#000000",
},
});
const emits = defineEmits(["changeColor"]);
// 默认颜色列表
const colorsDefault = reactive([
"#ff7e79",
"#fefe7f",
"#00ff81",
"#007ffe",
"#ff80c0",
"#ff0104",
"#00fcff",
"#847cc2",
"#fe00fe",
"#7e0101",
"#fc7f01",
"#027e04",
"#65b2f3",
"#f9b714",
"#068081",
"#8305a1",
"#b0cf29",
"#0bfa49",
"#9e255e",
"#ffffff",
]);
const canvasSaturationRef = ref(null);
const canvasHueRef = ref(null);
const position = reactive({
pointPosition: {
top: "0px",
left: "0px",
},
slideHueStyle: {},
});
const attr = reactive({
modelRgb: "",
modelHex: "",
r: 0,
g: 0,
b: 0,
h: 0,
s: 0,
v: 0,
});
const rgbString = computed(() => {
return `rgb(${attr.r}, ${attr.g}, ${attr.b})`;
});
// 渲染面板颜色
const renderSaturationColor = (color: string) => {
const canvas: any = canvasSaturationRef.value;
const height = canvas.height;
const width = canvas.width;
const ctx = canvas.getContext("2d");
ctx.fillStyle = color;
ctx.fillRect(0, 0, width, height);
createLinearGradient(
"l",
ctx,
width,
height,
"#FFFFFF",
"rgba(255,255,255,0)"
);
createLinearGradient("p", ctx, width, height, "rgba(0,0,0,0)", "#000000");
};
// 颜色面板点击
const selectSaturation = (e: MouseEvent) => {
const canvas: any = canvasSaturationRef.value;
const height = canvas.height;
const width = canvas.width;
let x = e.offsetX,
y = e.offsetY;
if (x < 0) x = 0;
if (x > width) x = width;
if (y < 0) y = 0;
if (y > height) y = height;
position.pointPosition = {
top: y - 5 + "px",
left: x - 5 + "px",
};
var ctx = canvas.getContext("2d");
var imageData = ctx.getImageData(
Math.max(x - 5, 0),
Math.max(0, y - 5),
1,
1
);
setRGBHSV(imageData.data);
attr.modelHex = rgb2hex({ r: attr.r, g: attr.g, b: attr.b }, true);
};
// 渲染调色器颜色
const renderHueColor = () => {
const canvas: any = canvasHueRef.value;
const ctx = canvas.getContext("2d");
const width = canvas.width;
const height = canvas.height;
const gradient = ctx.createLinearGradient(0, 0, 0, height);
gradient.addColorStop(0, "#FF0000"); // red
gradient.addColorStop(0.17 * 1, "#FF00FF"); // purple
gradient.addColorStop(0.17 * 2, "#0000FF"); // blue
gradient.addColorStop(0.17 * 3, "#00FFFF"); // green
gradient.addColorStop(0.17 * 4, "#00FF00"); // green
gradient.addColorStop(0.17 * 5, "#FFFF00"); // yellow
gradient.addColorStop(1, "#FF0000"); // red
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
};
// 颜色条选中
const selectHue = (e: any) => {
const canvas: any = canvasHueRef.value;
const { top: hueTop, height } = canvas.getBoundingClientRect();
const ctx = e.target.getContext("2d");
const mousemove = (e: any) => {
let y = e.clientY - hueTop;
if (y < 0) y = 0;
if (y > height) y = height;
position.slideHueStyle = {
top: y - 2 + "px",
};
// 先获取颜色条上的颜色在颜色面板上进行渲染
const imgData = ctx.getImageData(0, Math.min(y, height - 1), 1, 1);
const [r, g, b] = imgData.data;
renderSaturationColor(`rgb(${r},${g},${b})`);
// 再根据颜色面板上选中的点的颜色,来修改输入框的值
nextTick(() => {
const canvas: any = canvasSaturationRef.value;
const ctx = canvas.getContext("2d");
const pointX = parseFloat(position.pointPosition.left);
const pointY = parseFloat(position.pointPosition.top);
const pointRgb = ctx.getImageData(
Math.max(0, pointX),
Math.max(0, pointY),
1,
1
);
setRGBHSV(pointRgb.data);
attr.modelHex = rgb2hex({ r: attr.r, g: attr.g, b: attr.b }, true);
});
};
mousemove(e);
const mouseup = () => {
document.removeEventListener("mousemove", mousemove);
document.removeEventListener("mouseup", mouseup);
};
document.addEventListener("mousemove", mousemove);
document.addEventListener("mouseup", mouseup);
};
// 默认颜色选择区选择颜色
function selectColor(color: string) {
setRGBHSV(color);
attr.modelRgb = rgbString.value.substring(4, rgbString.value.length - 1);
attr.modelHex = rgb2hex({ r: attr.r, g: attr.g, b: attr.b }, true);
renderSaturationColor(rgbString.value);
position.pointPosition = {
left: Math.max(attr.s * 150 - 5, 0) + "px",
top: Math.max((1 - attr.v) * 80 - 5, 0) + "px",
};
renderSlide();
}
// 调色卡的位置
const renderSlide = () => {
position.slideHueStyle = {
top: (1 - attr.h / 360) * 78 + "px",
};
};
// hex输入框失去焦点
function inputHex() {
if (isHex(attr.modelHex)) {
selectColor(attr.modelHex);
} else {
alert("请输入3位或者6位合法十六进制值");
}
}
function inputRgb() {
if (isRgb(attr.modelRgb)) {
const [r, g, b] = attr.modelRgb.split(",");
const hex = rgb2hex({ r, g, b }, true);
attr.modelHex = hex;
selectColor(attr.modelHex);
} else {
alert("请输入合法的rgb数值");
}
}
// color可能是 #fff 也可能是 123,21,11 这两种格式
function setRGBHSV(color: any, initHex = false) {
let rgb: any = { r: "0", g: "0", b: "0" };
if (typeof color !== "string") {
rgb = { r: color[0], g: color[1], b: color[2] };
} else {
if (!color.includes("#")) {
const [r, g, b] = color.split(",");
rgb = { r: r, g: g, b: b };
} else {
rgb = hex2rgb(color);
}
}
const hsv = rgb2hsv(rgb);
attr.r = rgb.r;
attr.g = rgb.g;
attr.b = rgb.b;
attr.h = hsv.h;
attr.s = hsv.s;
attr.v = hsv.v;
if (initHex) attr.modelHex = rgb2hex(rgb, true);
attr.modelRgb = rgbString.value.substring(4, rgbString.value.length - 1);
}
// 确认选择的颜色
function changeColor() {
if (!isHex(attr.modelHex) || !isRgb(attr.modelRgb)) {
return;
} else {
emits("changeColor", attr.modelHex);
}
}
watch(
() => props.initColor,
(newVal, _) => {
selectColor(newVal);
}
);
onMounted(() => {
selectColor(props.initColor);
renderHueColor();
});
</script>
<style scoped lang="scss">
.hu-color-picker {
background: #242c3e;
border-radius: 4px;
box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.16);
z-index: 1;
canvas {
vertical-align: top;
}
.color-set {
display: flex;
margin-left: 5px;
padding: 8px 0 8px 8px;
border-left: 1px solid #323e53;
}
.color-show {
height: 56px;
width: 100px;
margin: 8px auto;
display: flex;
}
}
.color-panel {
display: flex;
padding: 8px 8px 0 8px;
.color-box {
width: 100px;
}
}
.color-view {
display: flex;
padding: 0 8px;
}
.input {
flex: 1;
margin-left: 8px;
}
// 颜色面板
.saturation {
position: relative;
cursor: pointer;
.slide {
position: absolute;
left: 100px;
top: 0;
width: 8px;
height: 8px;
border-radius: 50%;
border: 1px solid #fff;
box-shadow: 0 0 1px 1px rgba(0, 0, 0, 0.3);
pointer-events: none;
}
}
// 颜色调节条
.hue {
position: relative;
margin-left: 8px;
cursor: pointer;
.slide {
box-sizing: border-box;
position: absolute;
left: 0;
width: 100%;
height: 4px;
background: #fff;
border: 1px solid #f0f0f0;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
pointer-events: none;
border-radius: 1px;
}
}
.color-type {
display: flex;
margin: 8px auto;
font-size: 12px;
.name {
width: 32px;
height: 24px;
float: left;
display: flex;
justify-content: center;
align-items: center;
color: #a0acc0;
}
.value {
display: block;
box-sizing: border-box;
flex: 1;
height: 24px;
padding: 0 8px;
border: 1px solid #42516c;
color: #eff0f4;
background: #2e3850;
box-sizing: border-box;
width: 100px;
caret-color: #49a4ff;
&:focus-visible {
outline: 1px solid rgba(18, 107, 190, 0.5);
}
}
}
// 默认颜色
.colors {
display: flex;
flex-wrap: wrap;
padding: 0;
margin: 0;
.item {
flex-basis: calc(20% - 4px);
margin: 2px;
width: 16px;
height: 16px;
border-radius: 1px;
box-sizing: border-box;
vertical-align: top;
display: inline-block;
transition: all 0.1s;
cursor: pointer;
&:hover {
transform: scale(1.2);
}
}
}
.btn {
margin-top: 0 8px 8px;
border-top: 1px solid #323e53;
text-align: right;
padding: 8px;
button {
margin-left: 8px;
width: 52px;
height: 20px;
font-size: 12px;
font-weight: 400;
color: #fff;
background-color: #49a4ff;
border-radius: 2px;
border: none;
&:active {
background-color: #1890ff;
}
}
}
</style>
ColorPicker.ts:
// ColorPicker.ts
// rgb转hex
export function rgb2hex({ r, g, b }: any, toUpper: boolean) {
const change = (val: any) => ("0" + Number(val).toString(16)).slice(-2);
const color = `#${change(r)}${change(g)}${change(b)}`;
return toUpper ? color.toUpperCase() : color;
}
export function createLinearGradient(
direction: any,
ctx: any,
width: any,
height: any,
color1: any,
color2: any
) {
// l horizontal p vertical
const isL = direction === "l";
const gradient = ctx.createLinearGradient(
0,
0,
isL ? width : 0,
isL ? 0 : height
);
gradient.addColorStop(0.01, color1);
gradient.addColorStop(0.99, color2);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
}
// hex转rgb
export function hex2rgb(hex: any) {
hex = hex.slice(1);
if (hex.length === 3) {
hex = "" + hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
const change = (val: any) => parseInt(val, 16) || 0; // Avoid NaN situations
return {
r: change(hex.slice(0, 2)),
g: change(hex.slice(2, 4)),
b: change(hex.slice(4, 6)),
};
}
// rgb转hsv
export function rgb2hsv({ r, g, b }: any) {
r = r / 255;
g = g / 255;
b = b / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const delta = max - min;
let h = 0;
if (max === min) {
h = 0;
} else if (max === r) {
if (g >= b) {
h = (60 * (g - b)) / delta;
} else {
h = (60 * (g - b)) / delta + 360;
}
} else if (max === g) {
h = (60 * (b - r)) / delta + 120;
} else if (max === b) {
h = (60 * (r - g)) / delta + 240;
}
h = Math.floor(h);
const s = parseFloat((max === 0 ? 0 : 1 - min / max).toFixed(2));
const v = parseFloat(max.toFixed(2));
return { h, s, v };
}
// 验证输入的hex是否合法
export function isHex(str: string) {
return /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/.test(str);
}
// 验证输入的rgb是否合法
export function isRgb(str: string) {
const regex = /^(\d{1,3}),\s?(\d{1,3}),\s?(\d{1,3})$/; // 匹配rgb格式的正则表达式
const match = str.match(regex); // 使用match方法进行匹配
if (match) {
// 如果匹配成功
const r = parseInt(match[1]); // 获取红色值
const g = parseInt(match[2]); // 获取绿色值
const b = parseInt(match[3]); // 获取蓝色值
if (r >= 0 && r <= 255 && g >= 0 && g <= 255 && b >= 0 && b <= 255) {
// 判断RGB值是否在合法范围内
return true; // 如果合法,返回true
}
}
return false; // 如果不合法,返回false
}