chore: initialize qiming workspace repository
This commit is contained in:
443
qiming-mobile/uni_modules/jz-h5-recorder-manager/js/index.js
Normal file
443
qiming-mobile/uni_modules/jz-h5-recorder-manager/js/index.js
Normal file
@@ -0,0 +1,443 @@
|
||||
// H5录音管理插件
|
||||
// 完全兼容 uni.getRecorderManager API
|
||||
|
||||
// 检测当前平台
|
||||
function isH5Platform() {
|
||||
// #ifdef H5
|
||||
return true;
|
||||
// #endif
|
||||
return false;
|
||||
}
|
||||
|
||||
// H5录音管理器实现
|
||||
class H5RecorderManager {
|
||||
constructor() {
|
||||
this.mediaRecorder = null;
|
||||
this.audioContext = null;
|
||||
this.analyser = null;
|
||||
this.audioChunks = [];
|
||||
this.startTime = null;
|
||||
this.isPaused = false;
|
||||
this.isRecording = false;
|
||||
// 触摸结束取消录音(自定义变量)
|
||||
this.touchEndCancelFlag = false;
|
||||
|
||||
// 事件回调函数
|
||||
this.callbacks = {
|
||||
onStart: [],
|
||||
onPause: [],
|
||||
onResume: [],
|
||||
onStop: [],
|
||||
onError: [],
|
||||
onFrameRecorded: [],
|
||||
onInterruptionBegin: [],
|
||||
onInterruptionEnd: [],
|
||||
};
|
||||
|
||||
// 默认配置
|
||||
this.defaultOptions = {
|
||||
duration: 60000, // 录音时长,单位ms,最大值600000(10分钟)
|
||||
sampleRate: 44100, // 采样率,有效值 8000/16000/44100
|
||||
numberOfChannels: 1, // 录音通道数,有效值 1/2
|
||||
encodeBitRate: 192000, // 编码码率,有效值见下表格
|
||||
format: 'mp3', // 音频格式,有效值 aac/mp3/wav/PCM
|
||||
frameSize: null, // 指定帧大小,单位 KB
|
||||
};
|
||||
}
|
||||
|
||||
// 开始录音
|
||||
start(options = {}) {
|
||||
if (this.isRecording) {
|
||||
this._triggerError('录音已在进行中');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = Object.assign({}, this.defaultOptions, options);
|
||||
// 检查浏览器支持
|
||||
if (
|
||||
typeof navigator.mediaDevices === 'undefined' ||
|
||||
!navigator.mediaDevices.getUserMedia
|
||||
) {
|
||||
// 这里做一个消息原因增加说明
|
||||
const isHttpsProtocol =
|
||||
(typeof window !== 'undefined' &&
|
||||
window.location.protocol === 'https:') ||
|
||||
false;
|
||||
|
||||
const msg = !isHttpsProtocol
|
||||
? '需要在HTTPS模式下且同意浏览器录音授权时支持语音'
|
||||
: '当前浏览器不支持录音功能';
|
||||
|
||||
this._triggerError(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// 请求麦克风权限并开始录音
|
||||
navigator.mediaDevices
|
||||
.getUserMedia({
|
||||
audio: {
|
||||
sampleRate: config.sampleRate,
|
||||
channelCount: config.numberOfChannels,
|
||||
},
|
||||
})
|
||||
.then(stream => {
|
||||
console.log('获取麦克风权限成功: stream', stream);
|
||||
if (this.touchEndCancelFlag) {
|
||||
this.touchEndCancelFlag = false;
|
||||
return;
|
||||
}
|
||||
this._initRecorder(stream, config);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取麦克风权限失败:', error);
|
||||
this._triggerError('获取麦克风权限失败: ' + error.message);
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化录音器
|
||||
_initRecorder(stream, config) {
|
||||
try {
|
||||
// 创建AudioContext用于音频处理
|
||||
this.audioContext = new (window.AudioContext ||
|
||||
window.webkitAudioContext)();
|
||||
|
||||
// 创建MediaRecorder
|
||||
const mimeType = this._getMimeType(config.format);
|
||||
|
||||
if (!MediaRecorder.isTypeSupported(mimeType)) {
|
||||
uni.showToast({
|
||||
title: `不支持${config.format}格式,将使用默认格式`,
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
|
||||
this.mediaRecorder = new MediaRecorder(stream, {
|
||||
mimeType: MediaRecorder.isTypeSupported(mimeType)
|
||||
? mimeType
|
||||
: undefined,
|
||||
audioBitsPerSecond: config.encodeBitRate,
|
||||
});
|
||||
console.log(stream, this.mediaRecorder.mimeType);
|
||||
|
||||
this.audioChunks = [];
|
||||
this.startTime = Date.now();
|
||||
this.isRecording = true;
|
||||
this.isPaused = false;
|
||||
|
||||
// 设置事件监听
|
||||
this.mediaRecorder.ondataavailable = event => {
|
||||
if (event.data.size > 0) {
|
||||
this.audioChunks.push(event.data);
|
||||
|
||||
// 触发帧录制回调
|
||||
if (config.frameSize && this.callbacks.onFrameRecorded.length > 0) {
|
||||
this._triggerFrameRecorded(event.data, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.mediaRecorder.onstop = () => {
|
||||
this._handleRecordStop(config);
|
||||
};
|
||||
|
||||
this.mediaRecorder.onerror = event => {
|
||||
this._triggerError('录音过程中出现错误: ' + event.error);
|
||||
};
|
||||
|
||||
// 开始录音
|
||||
if (config.frameSize) {
|
||||
// 如果设置了frameSize,定时收集数据
|
||||
this.mediaRecorder.start(config.frameSize * 1024); // frameSize is in KB
|
||||
} else {
|
||||
this.mediaRecorder.start();
|
||||
}
|
||||
|
||||
// 设置录音时长限制
|
||||
if (config.duration && config.duration > 0) {
|
||||
setTimeout(() => {
|
||||
if (this.isRecording && !this.isPaused) {
|
||||
this.stop();
|
||||
}
|
||||
}, config.duration);
|
||||
}
|
||||
|
||||
// 触发开始事件
|
||||
this._triggerStart();
|
||||
} catch (error) {
|
||||
console.error('初始化录音器失败:', error);
|
||||
this._triggerError('初始化录音器失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取MIME类型
|
||||
_getMimeType(format) {
|
||||
const mimeTypes = {
|
||||
mp3: 'audio/mpeg',
|
||||
wav: 'audio/wav',
|
||||
aac: 'audio/aac',
|
||||
PCM: 'audio/wav',
|
||||
};
|
||||
return mimeTypes[format] || 'audio/webm';
|
||||
}
|
||||
|
||||
// 暂停录音
|
||||
pause() {
|
||||
if (!this.isRecording || this.isPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
|
||||
this.mediaRecorder.pause();
|
||||
this.isPaused = true;
|
||||
this._triggerPause();
|
||||
}
|
||||
}
|
||||
|
||||
// 继续录音
|
||||
resume() {
|
||||
if (!this.isRecording || !this.isPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mediaRecorder && this.mediaRecorder.state === 'paused') {
|
||||
this.mediaRecorder.resume();
|
||||
this.isPaused = false;
|
||||
this._triggerResume();
|
||||
}
|
||||
}
|
||||
|
||||
// 停止录音
|
||||
stop() {
|
||||
if (!this.isRecording) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
||||
this.mediaRecorder.stop();
|
||||
}
|
||||
|
||||
// 停止所有音频轨道
|
||||
if (this.mediaRecorder && this.mediaRecorder.stream) {
|
||||
this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
|
||||
}
|
||||
|
||||
this.isRecording = false;
|
||||
this.isPaused = false;
|
||||
}
|
||||
|
||||
// 处理录音停止
|
||||
_handleRecordStop(config) {
|
||||
if (this.audioChunks.length === 0) {
|
||||
this._triggerError('没有录制到音频数据');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建音频文件
|
||||
const audioBlob = new Blob(this.audioChunks, {
|
||||
type: this._getMimeType(config.format),
|
||||
});
|
||||
|
||||
// 创建临时文件URL
|
||||
const tempFilePath = URL.createObjectURL(audioBlob);
|
||||
|
||||
// 计算录音时长
|
||||
const duration = this.startTime ? (Date.now() - this.startTime) / 1000 : 0;
|
||||
|
||||
// 触发停止事件
|
||||
this._triggerStop({
|
||||
tempFilePath: tempFilePath,
|
||||
duration: duration,
|
||||
fileSize: audioBlob.size,
|
||||
});
|
||||
|
||||
// 清理资源
|
||||
this._cleanup();
|
||||
}
|
||||
|
||||
// 清理资源
|
||||
_cleanup() {
|
||||
if (this.audioContext) {
|
||||
this.audioContext.close();
|
||||
this.audioContext = null;
|
||||
}
|
||||
|
||||
this.mediaRecorder = null;
|
||||
this.audioChunks = [];
|
||||
this.startTime = null;
|
||||
this.touchEndCancelFlag = false;
|
||||
}
|
||||
|
||||
// 事件监听方法
|
||||
onStart(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
this.callbacks.onStart.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
onPause(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
this.callbacks.onPause.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
onResume(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
this.callbacks.onResume.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
onStop(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
this.callbacks.onStop.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
onError(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
this.callbacks.onError.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
onFrameRecorded(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
this.callbacks.onFrameRecorded.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
onInterruptionBegin(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
this.callbacks.onInterruptionBegin.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
onInterruptionEnd(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
this.callbacks.onInterruptionEnd.push(callback);
|
||||
}
|
||||
}
|
||||
|
||||
// 移除事件监听方法(支付宝小程序兼容)
|
||||
offStart(callback) {
|
||||
this._removeCallback('onStart', callback);
|
||||
}
|
||||
|
||||
offPause(callback) {
|
||||
this._removeCallback('onPause', callback);
|
||||
}
|
||||
|
||||
offResume(callback) {
|
||||
this._removeCallback('onResume', callback);
|
||||
}
|
||||
|
||||
offStop(callback) {
|
||||
this._removeCallback('onStop', callback);
|
||||
}
|
||||
|
||||
offFrameRecorded(callback) {
|
||||
this._removeCallback('onFrameRecorded', callback);
|
||||
}
|
||||
|
||||
// 移除回调函数
|
||||
_removeCallback(eventName, callback) {
|
||||
if (callback && this.callbacks[eventName]) {
|
||||
const index = this.callbacks[eventName].indexOf(callback);
|
||||
if (index > -1) {
|
||||
this.callbacks[eventName].splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 触发事件的私有方法
|
||||
_triggerStart() {
|
||||
this.callbacks.onStart.forEach(callback => {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
console.error('onStart callback error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_triggerPause() {
|
||||
this.callbacks.onPause.forEach(callback => {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
console.error('onPause callback error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_triggerResume() {
|
||||
// 重置触摸结束取消录音标志
|
||||
this.touchEndCancelFlag = false;
|
||||
this.callbacks.onResume.forEach(callback => {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
console.error('onResume callback error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_triggerStop(result) {
|
||||
// 重置触摸结束取消录音标志
|
||||
this.touchEndCancelFlag = false;
|
||||
this.callbacks.onStop.forEach(callback => {
|
||||
try {
|
||||
callback(result);
|
||||
} catch (error) {
|
||||
console.error('onStop callback error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_triggerError(errMsg) {
|
||||
// 重置触摸结束取消录音标志
|
||||
this.touchEndCancelFlag = false;
|
||||
this.callbacks.onError.forEach(callback => {
|
||||
try {
|
||||
callback({ errMsg });
|
||||
} catch (error) {
|
||||
console.error('onError callback error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_triggerFrameRecorded(frameBuffer, isLastFrame = false) {
|
||||
// 重置触摸结束取消录音标志
|
||||
this.touchEndCancelFlag = false;
|
||||
this.callbacks.onFrameRecorded.forEach(callback => {
|
||||
try {
|
||||
callback({
|
||||
frameBuffer,
|
||||
isLastFrame,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('onFrameRecorded callback error:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 全局实例
|
||||
let recorderManagerInstance = null;
|
||||
|
||||
// 主函数:获取录音管理器
|
||||
function getRecorderManager() {
|
||||
if (isH5Platform()) {
|
||||
// H5平台使用自定义实现
|
||||
if (!recorderManagerInstance) {
|
||||
recorderManagerInstance = new H5RecorderManager();
|
||||
}
|
||||
return recorderManagerInstance;
|
||||
} else {
|
||||
// 非H5平台直接使用uni.getRecorderManager
|
||||
return uni.getRecorderManager();
|
||||
}
|
||||
}
|
||||
|
||||
// 导出
|
||||
export default {
|
||||
getRecorderManager,
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "jz-h5-recorder-manager",
|
||||
"version": "1.0.0",
|
||||
"description": "一个完全兼容 uni.getRecorderManager API 的H5录音插件,支持H5、App、小程序等全平台",
|
||||
"main": "js/index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"uniapp",
|
||||
"uni-app",
|
||||
"recorder",
|
||||
"audio",
|
||||
"h5",
|
||||
"recording",
|
||||
"microphone",
|
||||
"cross-platform",
|
||||
"插件",
|
||||
"录音",
|
||||
"音频"
|
||||
],
|
||||
"author": {
|
||||
"name": "jz",
|
||||
"email": ""
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitcode.com/weixin_47770976/jz-h5-getRecorderManager.git"
|
||||
},
|
||||
"homepage": "https://gitcode.com/weixin_47770976/jz-h5-getRecorderManager",
|
||||
"bugs": {
|
||||
"url": "https://gitcode.com/weixin_47770976/jz-h5-getRecorderManager/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"files": [
|
||||
"js/",
|
||||
"README.md",
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
@import '~@/uni_modules/lime-style/index.scss';
|
||||
|
||||
/* #ifdef uniVersion >= 4.75 */
|
||||
$use-css-var: true;
|
||||
/* #endif */
|
||||
|
||||
$badge-size: create-var(badge-size , $spacer);
|
||||
$badge-color: create-var(badge-color , white);
|
||||
// $badge-padding: create-var(badge-padding, 0 4px);
|
||||
$badge-padding-x: create-var(badge-padding-x, $spacer-tn); // 水平方向(左右)
|
||||
$badge-padding-y: create-var(badge-padding-y, 0); // 垂直方向(上下)
|
||||
|
||||
$badge-font-size: create-var(badge-font-size, $font-size-sm);
|
||||
$badge-font-weight: create-var(badge-font-weight, bold);
|
||||
$badge-border-width: create-var(badge-border-width, 1rpx);
|
||||
$badge-border-color: create-var(badge-border-color, white);
|
||||
$badge-bg-color: create-var(badge-bg-color, $error-color);
|
||||
$badge-dot-color: create-var(badge-dot-color, $error-color);
|
||||
$badge-dot-size: create-var(badge-dot-size, 8px);
|
||||
$badge-font: create-var(badge-font, -apple-system-font, helvetica neue, arial, sans-serif);
|
||||
$badge-border-radius: create-var(badge-border-radius, $border-radius-hg);
|
||||
|
||||
|
||||
|
||||
.l-badge {
|
||||
/* #ifndef UNI-APP-X */
|
||||
display: inline-block;
|
||||
/* #endif */
|
||||
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
min-width: $badge-size;
|
||||
/* #endif */
|
||||
box-sizing: border-box;
|
||||
|
||||
// padding: $badge-padding;
|
||||
@include padding($badge-padding-y $badge-padding-x);
|
||||
// box-sizing: content-box;
|
||||
color: $badge-color;
|
||||
font-weight: $badge-font-weight;
|
||||
font-size: $badge-font-size;
|
||||
font-family: $badge-font;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
background-color: $badge-bg-color;
|
||||
// border: $badge-border-width solid $badge-border-color;
|
||||
border-width: $badge-border-width;
|
||||
border-style: solid;
|
||||
border-color: $badge-border-color;
|
||||
// border-radius: $badge-border-radius;
|
||||
@include border-radius($badge-border-radius);
|
||||
overflow: visible;
|
||||
|
||||
|
||||
&--fixed {
|
||||
position: absolute;
|
||||
transform-origin: 100%;
|
||||
z-index: 1
|
||||
}
|
||||
&--offscreen {
|
||||
position: fixed !important;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&--top-left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
&--top-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: translate(50%, -50%);
|
||||
}
|
||||
|
||||
&--bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform: translate(-50%, 50%);
|
||||
}
|
||||
|
||||
&--bottom-right {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: translate(50%, 50%);
|
||||
}
|
||||
|
||||
&--dot {
|
||||
width: $badge-dot-size;
|
||||
min-width: 0;
|
||||
height: $badge-dot-size;
|
||||
background: $badge-dot-color;
|
||||
border-radius: 99px;
|
||||
// border: none;
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
|
||||
/* #ifndef UNI-APP-X */
|
||||
display: inline-block;
|
||||
/* #endif */
|
||||
/* #ifdef UNI-APP-X */
|
||||
// align-self: flex-start;
|
||||
/* #endif */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
@import '~@/uni_modules/lime-style/index.scss';
|
||||
|
||||
$badge-size: create-var(badge-size , 16px);
|
||||
$badge-color: create-var(badge-color , white);
|
||||
$badge-padding: create-var(badge-padding, 0 3px);
|
||||
$badge-font-size: create-var(badge-font-size, 12px);
|
||||
$badge-font-weight: create-var(badge-font-weight, bold);
|
||||
$badge-border-width: create-var(badge-border-width, 1px);
|
||||
$badge-border-color: create-var(badge-border-color, white);
|
||||
$badge-background: create-var(badge-background, $error-color);
|
||||
$badge-dot-color: create-var(badge-dot-color, $error-color);
|
||||
$badge-dot-size: create-var(badge-dot-size, 8px);
|
||||
$badge-font: create-var(badge-font, -apple-system-font, helvetica neue, arial, sans-serif);
|
||||
$badge-border-radius: create-var(badge-border-radius, 999px);
|
||||
|
||||
|
||||
.l-badge {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
min-width: $badge-size;
|
||||
padding: $badge-padding;
|
||||
color: $badge-color;
|
||||
font-weight: $badge-font-weight;
|
||||
font-size: $badge-font-size;
|
||||
font-family: $badge-font;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
background-color: $badge-background;
|
||||
border: $badge-border-width solid $badge-border-color;
|
||||
border-radius: $badge-border-radius;
|
||||
|
||||
&--fixed {
|
||||
position: absolute;
|
||||
transform-origin: 100%;
|
||||
}
|
||||
|
||||
&--top-left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
&--top-right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: translate(50%, -50%);
|
||||
}
|
||||
|
||||
&--bottom-left {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
transform: translate(-50%, 50%);
|
||||
}
|
||||
|
||||
&--bottom-right {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform: translate(50%, 50%);
|
||||
}
|
||||
|
||||
&--dot {
|
||||
width: $badge-dot-size;
|
||||
min-width: 0;
|
||||
height: $badge-dot-size;
|
||||
background: $badge-dot-color;
|
||||
border-radius: 100%;
|
||||
// border: none;
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
position: relative;
|
||||
/* #ifndef UNI-APP-X */
|
||||
display: inline-block;
|
||||
/* #endif */
|
||||
/* #ifdef UNI-APP-X */
|
||||
// display: inline-block;
|
||||
width: fit-content;
|
||||
align-items: flex-start;
|
||||
/* #endif */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<view class="l-badge__wrapper" v-if="$slots['default'] != null">
|
||||
<slot></slot>
|
||||
<text v-if="hasContent || dot" class="l-badge" ref="textRef" :class="classes" :style="[styles]">
|
||||
<slot name="content">{{renderContent}}</slot>
|
||||
</text>
|
||||
<!-- #ifdef APP-HARMONY -->
|
||||
<text v-if="hasContent || dot" class="l-badge l-badge--offscreen" ref="offscreenRef" >
|
||||
<slot name="content">{{renderContent}}</slot>
|
||||
</text>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<text v-else-if="hasContent || dot" class="l-badge" :class="classes" :style="[styles]">
|
||||
<slot name="content">{{renderContent}}</slot>
|
||||
</text>
|
||||
</template>
|
||||
<script lang="uts" setup>
|
||||
/**
|
||||
* Badge 徽标组件
|
||||
* @description 用于展示状态标记、消息数量等提示信息,支持多种形态和定位方式
|
||||
* <br> 插件类型:LBadgeComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-badge
|
||||
*
|
||||
* @property {string} [color] 徽标背景色(支持CSS颜色值)
|
||||
* @property {number | string | any} content 显示内容(数字/文字)
|
||||
* @property {boolean} dot 是否显示为小红点(优先级高于content)
|
||||
* @property {number} max 数字最大值(超出显示为${max}+)
|
||||
* @property {Array<string | number> | any[]} offset 位置偏移量([x, y])
|
||||
* @property {'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'} position 定位位置
|
||||
* @property {'circle' | 'square' | 'bubble' | 'ribbon'} [shape] 形状(当前版本未实现)
|
||||
* @property {boolean} showZero 数值为0时是否显示
|
||||
* @property {'medium' | 'large'} [size] 尺寸(当前版本未实现)
|
||||
* @property {string | number} [content] 支持字符串模板(例:'${count}条')
|
||||
* @property {Array<string | number>} offset 支持单位(例:['-10rpx', '20px'])
|
||||
*/
|
||||
|
||||
import { isNumeric } from '@/uni_modules/lime-shared/isNumeric'
|
||||
import { isNumber } from '@/uni_modules/lime-shared/isNumber'
|
||||
import { addUnit } from '@/uni_modules/lime-shared/addUnit'
|
||||
import { toBoolean } from '@/uni_modules/lime-shared/toBoolean'
|
||||
import { getOffsetWithMinusString } from './utils'
|
||||
import { BadgeProps } from './type'
|
||||
const name = 'l-badge'
|
||||
const props = withDefaults(defineProps<BadgeProps>(), {
|
||||
dot: false,
|
||||
max: 99,
|
||||
showZero: false,
|
||||
// #ifdef APP-ANDROID
|
||||
// offset: [] as any[],
|
||||
// #endif
|
||||
// #ifndef APP-ANDROID
|
||||
// offset: [] as (string | number)[],
|
||||
// #endif
|
||||
position: 'top-right'
|
||||
})
|
||||
|
||||
|
||||
|
||||
const context = getCurrentInstance()!
|
||||
const classes = computed(():Map<string, boolean>=>{
|
||||
const cls = new Map<string, boolean>()
|
||||
cls.set(`${name}--fixed`, toBoolean(context.slots['default']));
|
||||
cls.set(`${name}--dot`, props.dot);
|
||||
cls.set(`${name}--${props.position}`, context.slots['default'] != null);
|
||||
return cls
|
||||
})
|
||||
const styles = computed(():Map<string, any|null>=>{
|
||||
const style = new Map<string, any|null>()
|
||||
if(toBoolean(props.color)) {
|
||||
style.set('background-color', props.color!)
|
||||
}
|
||||
const positions = props.position.split('-');
|
||||
const offset = props.offset;
|
||||
if(offset != null && offset.length == 2) {
|
||||
const x = offset[0];
|
||||
const y = offset[1];
|
||||
if(context.slots['default'] != null) {
|
||||
if(positions.length == 2) {
|
||||
const offsetY = positions[0], offsetX = positions[1];
|
||||
if(isNumber(y)) {
|
||||
const _y = y as number
|
||||
style.set(offsetY, addUnit(offsetY == 'top' ? _y : -_y))
|
||||
} else {
|
||||
style.set(offsetY, offsetY == 'top' ? addUnit(y) : getOffsetWithMinusString(`${y}`))
|
||||
}
|
||||
if(isNumber(x)) {
|
||||
const _x = x as number
|
||||
style.set(offsetX, addUnit(offsetX == 'left' ? _x : -_x))
|
||||
} else {
|
||||
style.set(offsetY, offsetY == 'left' ? addUnit(x) : getOffsetWithMinusString(`${x}`))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
style.set('margin-top', addUnit(y))
|
||||
style.set('margin-left', addUnit(x))
|
||||
}
|
||||
}
|
||||
return style
|
||||
});
|
||||
const hasContent = computed<boolean>(():boolean => {
|
||||
if(toBoolean(context.slots['content'])) {
|
||||
return true
|
||||
}
|
||||
const content = props.content;
|
||||
return (content != '' && content != null && (props.showZero || content !== '0'));
|
||||
});
|
||||
const renderContent = computed<string>(():string=>{
|
||||
const dot = props.dot
|
||||
const max = props.max
|
||||
const content = props.content
|
||||
if(!dot && hasContent.value) {
|
||||
if(max != 0 && isNumeric(content) && parseFloat(content.toString()) > max) {
|
||||
return `${max}+`
|
||||
}
|
||||
}
|
||||
if(dot) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return `${content ?? ""}`
|
||||
})
|
||||
// #ifdef APP-HARMONY
|
||||
// 鸿蒙BUG 显示不存
|
||||
// 暂时先绕一下
|
||||
const textRef = ref<UniTextElement|null>(null)
|
||||
const offscreenRef = ref<UniTextElement|null>(null)
|
||||
const resizeObserver = new UniResizeObserver((entries : Array<UniResizeObserverEntry>) => {
|
||||
offscreenRef.value?.getBoundingClientRectAsync()?.then(res=>{
|
||||
// const width = entries[0].target.getBoundingClientRect().width
|
||||
textRef.value!.style.setProperty('width', res.width*1.05)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
const stopWatch = watch(offscreenRef, (el:UniElement|null) => {
|
||||
if(el== null) return
|
||||
resizeObserver.observe(el)
|
||||
})
|
||||
|
||||
onUnmounted(()=>{
|
||||
stopWatch()
|
||||
resizeObserver.disconnect()
|
||||
})
|
||||
// #endif
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import './index-u';
|
||||
</style>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<view class="l-badge__wrapper" v-if="$slots.default">
|
||||
<slot></slot>
|
||||
<view v-if="hasContent || props.dot" class="l-badge" :class="classes" :style="[styles]">
|
||||
<slot v-if="$slots.content" name="content"></slot>
|
||||
<block v-else-if="renderContent">{{ renderContent }}</block>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="hasContent || props.dot" class="l-badge" :class="classes" :style="[styles]">
|
||||
<slot v-if="$slots.content" name="content"></slot>
|
||||
<block v-else-if="renderContent">{{ renderContent }}</block>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Badge 徽标组件
|
||||
* @description 用于展示状态标记、消息数量等提示信息,支持多种形态和定位方式
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-badge
|
||||
*
|
||||
* @property {string} [color] 徽标背景色(支持CSS颜色值)
|
||||
* @property {number | string | any} content 显示内容(数字/文字)
|
||||
* @property {boolean} dot 是否显示为小红点(优先级高于content)
|
||||
* @property {number} max 数字最大值(超出显示为${max}+)
|
||||
* @property {Array<string | number> | any[]} offset 位置偏移量([x, y])
|
||||
* @property {'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'} position 定位位置
|
||||
* @property {'circle' | 'square' | 'bubble' | 'ribbon'} [shape] 形状(当前版本未实现)
|
||||
* @property {boolean} showZero 数值为0时是否显示
|
||||
* @property {'medium' | 'large'} [size] 尺寸(当前版本未实现)
|
||||
* @property {string | number} [content] 支持字符串模板(例:'${count}条')
|
||||
* @property {Array<string | number>} offset 支持单位(例:['-10rpx', '20px'])
|
||||
*/
|
||||
import { computed, defineComponent, getCurrentInstance, onMounted } from '@/uni_modules/lime-shared/vue'
|
||||
import badgeProps from './props'
|
||||
import { isNumeric } from '@/uni_modules/lime-shared/isNumeric'
|
||||
import { isNumber } from '@/uni_modules/lime-shared/isNumber'
|
||||
import { addUnit } from '@/uni_modules/lime-shared/addUnit'
|
||||
import { isDef } from '@/uni_modules/lime-shared/isDef'
|
||||
import { getClassStr } from '@/uni_modules/lime-shared/getClassStr'
|
||||
import { getOffsetWithMinusString } from './utils'
|
||||
const name = 'l-badge'
|
||||
interface CSSProperties {
|
||||
[key : string] : string | number | undefined;
|
||||
}
|
||||
export default defineComponent({
|
||||
name,
|
||||
props: badgeProps,
|
||||
setup(props) {
|
||||
const context = getCurrentInstance()
|
||||
// vue2 setup 解构的 slots 为空
|
||||
const classes = computed(() => {
|
||||
return getClassStr({
|
||||
[`${name}--fixed`]: context.slots.default,
|
||||
[`${name}--dot`]: props.dot,
|
||||
[`${name}--${props.position}`]: Boolean(context.slots['default'])
|
||||
})
|
||||
})
|
||||
const styles = computed(() => {
|
||||
const style : CSSProperties = {
|
||||
background: props.color,
|
||||
};
|
||||
if (props.offset) {
|
||||
const [x, y] = props.offset;
|
||||
const { position } = props;
|
||||
const [offsetY, offsetX] = `${position}`.split('-') as ['top' | 'bottom', 'left' | 'right'];
|
||||
if (context.slots.default) {
|
||||
if (isNumber(y)) {
|
||||
style[offsetY] = addUnit(offsetY === 'top' ? y : -y);
|
||||
} else {
|
||||
style[offsetY] = offsetY === 'top' ? addUnit(y) : getOffsetWithMinusString(`${y}`);
|
||||
}
|
||||
if (isNumber(x)) {
|
||||
style[offsetX] = addUnit(offsetX === 'left' ? x : -x);
|
||||
} else {
|
||||
style[offsetX] = offsetX === 'left' ? addUnit(x) : getOffsetWithMinusString(`${x}`);
|
||||
}
|
||||
} else {
|
||||
style.marginTop = addUnit(y);
|
||||
style.marginLeft = addUnit(x);
|
||||
}
|
||||
}
|
||||
return style
|
||||
})
|
||||
const hasContent = computed(() => {
|
||||
if (Boolean(context.slots.content)) {
|
||||
return !0
|
||||
}
|
||||
const { content, showZero } = props;
|
||||
return (isDef(content) && content !== '' && (showZero || (content !== 0 && content !== '0')));
|
||||
|
||||
})
|
||||
const renderContent = computed(() => {
|
||||
const { dot, max, content } = props;
|
||||
|
||||
if (!dot && hasContent.value) {
|
||||
//@ts-ignore
|
||||
if (isDef(max) && max != 0 && isDef(content) && isNumeric(content!) && +content > +max) {
|
||||
return `${max}+`
|
||||
}
|
||||
}
|
||||
return content
|
||||
})
|
||||
|
||||
|
||||
return {
|
||||
props,
|
||||
classes,
|
||||
styles,
|
||||
hasContent,
|
||||
renderContent
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import './index-u';
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
// @ts-nocheck
|
||||
import type {PropType} from '@/uni_modules/lime-shared/vue'
|
||||
export type BadgePosition =
|
||||
| 'top-left'
|
||||
| 'top-right'
|
||||
| 'bottom-left'
|
||||
| 'bottom-right';
|
||||
type Numeric = string | number
|
||||
export default {
|
||||
dot: Boolean,
|
||||
max: Number,
|
||||
color: String,
|
||||
offset: Array as unknown as PropType<[Numeric, Numeric]>,
|
||||
content: [Number , String],
|
||||
showZero: Boolean,
|
||||
position: {
|
||||
type: String as PropType<BadgePosition>,
|
||||
default: 'top-right'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// @ts-nocheck
|
||||
export interface BadgeProps {
|
||||
/**
|
||||
* 颜色
|
||||
* @default ''
|
||||
*/
|
||||
color?: string;
|
||||
/**
|
||||
* 徽标内容
|
||||
*/
|
||||
// #ifndef APP-ANDROID
|
||||
content?: string|number;
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
content?: any;
|
||||
// #endif
|
||||
/**
|
||||
* 是否为红点
|
||||
*/
|
||||
dot: boolean;
|
||||
/**
|
||||
* 封顶的数字值
|
||||
* @default 99
|
||||
*/
|
||||
max: number;
|
||||
/**
|
||||
* 设置状态点的位置偏移,示例:[-10, 20] 或 ['10rpx', '8rpx']
|
||||
*/
|
||||
// #ifndef APP-ANDROID
|
||||
offset?: Array<string | number>;
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
offset?: any[];
|
||||
// #endif
|
||||
position: string;
|
||||
/**
|
||||
* 形状 未实现
|
||||
* @default circle
|
||||
*/
|
||||
shape?: 'circle' | 'square' | 'bubble' | 'ribbon';
|
||||
/**
|
||||
* 当数值为 0 时,是否展示徽标
|
||||
*/
|
||||
showZero: boolean;
|
||||
/**
|
||||
* 尺寸 未实现
|
||||
* @default medium
|
||||
*/
|
||||
size?: 'medium' | 'large';
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function getOffsetWithMinusString(val : string):string {
|
||||
return val.startsWith('-') ? val.replace('-', '') : `-${val}`
|
||||
};
|
||||
107
qiming-mobile/uni_modules/lime-badge/package.json
Normal file
107
qiming-mobile/uni_modules/lime-badge/package.json
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"id": "lime-badge",
|
||||
"displayName": "lime-badge 徽标",
|
||||
"version": "0.1.2",
|
||||
"description": "lime-badge 实现的在右上角展示徽标数字或小红点,兼容uniapp/uniappx",
|
||||
"keywords": [
|
||||
"badge",
|
||||
"徽标"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.26",
|
||||
"uni-app": "^4.74",
|
||||
"uni-app-x": "^4.75"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "√",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"lime-shared",
|
||||
"lime-style"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "√",
|
||||
"vue3": "√"
|
||||
},
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"vue": "√",
|
||||
"nvue": "-",
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√",
|
||||
"alipay": "-",
|
||||
"toutiao": "-",
|
||||
"baidu": "-",
|
||||
"kuaishou": "-",
|
||||
"jd": "-",
|
||||
"harmony": "-",
|
||||
"qq": "-",
|
||||
"lark": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "-",
|
||||
"union": "-"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
@import '@/uni_modules/lime-style/index.scss';
|
||||
/* #ifdef uniVersion >= 4.75 */
|
||||
$use-css-var: true;
|
||||
/* #endif */
|
||||
|
||||
$button: #{$prefix}-button;
|
||||
|
||||
$button-gap: create-var(button-gap, 4px);
|
||||
$button-border-width: create-var(button-border-width, 0.71px);
|
||||
$button-border-radius: create-var(button-border-radius, $border-radius-sm);
|
||||
$button-solid-text-color: create-var(button-solid-text-color, white);
|
||||
$button-disabled-opacity: create-var(button-disabled-opacity, 0.6);
|
||||
|
||||
$button-default-type-map:(
|
||||
solid-color: create-var(button-default-solid-text-color, white),
|
||||
color: create-var(button-default-color, $gray-14),
|
||||
hover-color: create-var(button-default-hover-color, rgba(0,0,0,1)),
|
||||
light: create-var(button-default-light-color, $gray-2),
|
||||
light-hover: create-var(button-default-light-hover-color, $gray-3),
|
||||
border-color: create-var(button-default-border-color, $gray-5)
|
||||
);
|
||||
|
||||
$button-primary-type-map:(
|
||||
solid-color: create-var(button-primary-solid-text-color, white),
|
||||
color: create-var(button-primary-color, $primary-color),
|
||||
hover-color: create-var(button-primary-hover-color, $primary-color-7),
|
||||
light: create-var(button-primary-light-color, $primary-color-1),
|
||||
light-hover: create-var(button-primary-light-hover-color, $primary-color-2),
|
||||
border-color: create-var(button-primary-border-color, $primary-color)
|
||||
);
|
||||
|
||||
$button-danger-type-map:(
|
||||
solid-color: create-var(button-dangert-solid-text-color, white),
|
||||
color: create-var(button-danger-color, $error-color),
|
||||
hover-color: create-var(button-danger-hover-color, $error-color-7),
|
||||
light: create-var(button-danger-light-color, $error-color-1),
|
||||
light-hover: create-var(button-danger-light-hover-color, $error-color-3),
|
||||
border-color: create-var(button-danger-border-color, $error-color)
|
||||
);
|
||||
|
||||
$button-warning-type-map:(
|
||||
solid-color: create-var(button-warning-solid-text-color, white),
|
||||
color: create-var(button-warning-color, $warning-color),
|
||||
hover-color: create-var(button-warning-hover-color, $warning-color-7),
|
||||
light: create-var(button-warning-light-color, $warning-color-1),
|
||||
light-hover: create-var(button-warning-light-hover-color, $warning-color-2),
|
||||
border-color: create-var(button-warning-border-color, $warning-color)
|
||||
);
|
||||
|
||||
$button-success-type-map:(
|
||||
solid-color: create-var(button-success-solid-text-color, white),
|
||||
color: create-var(button-success-color, $success-color),
|
||||
hover-color: create-var(button-success-hover-color, $success-color-7),
|
||||
light: create-var(button-success-light-color, $success-color-1),
|
||||
light-hover: create-var(button-success-light-hover-color, $success-color-2),
|
||||
border-color: create-var(button-success-border-color, $success-color)
|
||||
);
|
||||
|
||||
$button-info-type-map:(
|
||||
solid-color: create-var(button-info-solid-text-color, white),
|
||||
color: create-var(button-info-color, $info-color),
|
||||
hover-color: create-var(button-info-hover-color, $info-color-7),
|
||||
light: create-var(button-info-light-color, $info-color-2),
|
||||
light-hover: create-var(button-info-light-hover-color, $info-color-3),
|
||||
border-color: create-var(button-info-border-color, $info-color)
|
||||
);
|
||||
|
||||
|
||||
$button-type-map:(
|
||||
default: $button-default-type-map,
|
||||
primary: $button-primary-type-map,
|
||||
danger: $button-danger-type-map,
|
||||
warning: $button-warning-type-map,
|
||||
success: $button-success-type-map,
|
||||
info: $button-info-type-map,
|
||||
);
|
||||
|
||||
|
||||
$button-icon-size-map:(
|
||||
mini: create-var(button-icon-size, 32rpx),
|
||||
small: create-var(button-icon-size, 36rpx),
|
||||
medium: create-var(button-icon-size, 36rpx),
|
||||
large: create-var(button-icon-size, 48rpx),
|
||||
);
|
||||
|
||||
$button-font-size-map:(
|
||||
mini: create-var(button-font-size, $font-size-sm),
|
||||
small: create-var(button-font-size, $font-size),
|
||||
medium: create-var(button-font-size, $font-size-md),
|
||||
large: create-var(button-font-size, $font-size-md),
|
||||
);
|
||||
|
||||
// $button-padding-map:(
|
||||
// mini: create-var(button-padding, 0 $spacer-xs),
|
||||
// small: create-var(button-padding,0 $spacer-sm),
|
||||
// medium: create-var(button-padding, 0 $spacer),
|
||||
// large: create-var(button-padding, 0 $spacer-md),
|
||||
// );
|
||||
|
||||
$button-padding-x-map:(
|
||||
mini: create-var(button-padding-x, $spacer-xs),
|
||||
small: create-var(button-padding-x, $spacer-sm),
|
||||
medium: create-var(button-padding-x, $spacer),
|
||||
large: create-var(button-padding-x, $spacer-md),
|
||||
);
|
||||
|
||||
$button-padding-y-map:(
|
||||
mini: create-var(button-padding-y, 0),
|
||||
small: create-var(button-padding-y, 0),
|
||||
medium: create-var(button-padding-y, 0),
|
||||
large: create-var(button-padding-y, 0),
|
||||
);
|
||||
|
||||
|
||||
|
||||
$button-height-map:(
|
||||
mini: create-var(button-mini-height, 56rpx),
|
||||
small: create-var(button-small-height, 64rpx),
|
||||
medium: create-var(button-medium-height, 80rpx),
|
||||
large: create-var(button-large-height, 96rpx),
|
||||
);
|
||||
|
||||
@mixin button-size($size) {
|
||||
$iconSize: map-get($button-icon-size-map, $size);
|
||||
$fontSize: map-get($button-font-size-map, $size);
|
||||
// $padding: map-get($button-padding-map, $size);
|
||||
$paddingX: map-get($button-padding-x-map, $size);
|
||||
$paddingY: map-get($button-padding-y-map, $size);
|
||||
$height: map-get($button-height-map, $size);
|
||||
|
||||
.#{$button}--#{$size} {
|
||||
// padding: $padding;
|
||||
@include padding($paddingY $paddingX);
|
||||
height: $height;
|
||||
|
||||
/* #ifndef UNI-APP-X */
|
||||
font-size: $fontSize;
|
||||
--l-loading-size: #{$iconSize};
|
||||
/* #endif */
|
||||
|
||||
/* #ifdef UNI-APP-X */
|
||||
.#{$button} {
|
||||
&__content {
|
||||
font-size: $fontSize;
|
||||
// line-height: 1;
|
||||
}
|
||||
// &__icon {
|
||||
// font-size: $iconSize;
|
||||
// }
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
&__loading {
|
||||
--l-loading-size: #{$iconSize};
|
||||
font-size: $iconSize;
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
&.#{$button} {
|
||||
&--square,&--circle {
|
||||
width: $height;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@mixin button-type($type) {
|
||||
$type-map: map-get($button-type-map, $type);
|
||||
|
||||
$color: map-get($type-map, color);
|
||||
$solid-color: map-get($type-map, solid-color);
|
||||
$hoverColor: map-get($type-map, hover-color);
|
||||
$borderColor: map-get($type-map, border-color);
|
||||
$lightColor: map-get($type-map, light);
|
||||
$lightHoverColor: map-get($type-map, light-hover);
|
||||
|
||||
.#{$button}--hover {
|
||||
&.#{$button}--#{$type} {
|
||||
background-color: $lightColor;
|
||||
&.#{$button}--solid {
|
||||
background-color: $hoverColor;
|
||||
}
|
||||
&.#{$button}--light {
|
||||
background-color: $lightHoverColor;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.#{$button}--#{$type} {
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
color: $color;
|
||||
&:not(.#{$button}--solid) {
|
||||
--l-loading-color: #{$color};
|
||||
}
|
||||
/* #endif */
|
||||
.#{$button} {
|
||||
/* #ifdef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
&__content {
|
||||
color: $color;
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
&.#{$button}--solid {
|
||||
background-color: $color;
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
color: $solid-color;
|
||||
--l-loading-color: #{$solid-color};
|
||||
&:after {
|
||||
border-color: transparent;
|
||||
}
|
||||
/* #endif */
|
||||
/* #ifdef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
// border-style: solid;
|
||||
.#{$button} {
|
||||
&__content {
|
||||
color: $solid-color;
|
||||
}
|
||||
&__icon {
|
||||
color: $solid-color;
|
||||
}
|
||||
&__loading {
|
||||
// color: $button-solid-text-color;
|
||||
}
|
||||
}
|
||||
/* #endif*/
|
||||
}
|
||||
|
||||
/* #ifdef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
&.#{$button}--outline {
|
||||
border-color: $borderColor;
|
||||
}
|
||||
&.#{$button}--dashed {
|
||||
border-color: $borderColor;
|
||||
}
|
||||
/* #endif */
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
&.#{$button}--outline {
|
||||
&:after {
|
||||
border-color: $borderColor;
|
||||
}
|
||||
}
|
||||
&.#{$button}--dashed {
|
||||
&:after {
|
||||
border-color: $borderColor;
|
||||
}
|
||||
}
|
||||
/* #endif */
|
||||
&.#{$button}--light {
|
||||
background-color: $lightColor;
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
&:after {
|
||||
border-color: transparent;
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
&.#{$button}--ghost {
|
||||
background-color: transparent;
|
||||
/* #ifdef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
border-color: $borderColor;
|
||||
/* #endif */
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
&:after {
|
||||
border-color: $borderColor;
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include button-size(mini);
|
||||
@include button-size(small);
|
||||
@include button-size(medium);
|
||||
@include button-size(large);
|
||||
|
||||
|
||||
@include button-type(default);
|
||||
@include button-type(primary);
|
||||
@include button-type(danger);
|
||||
@include button-type(info);
|
||||
@include button-type(warning);
|
||||
@include button-type(success);
|
||||
|
||||
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY || MP-WEIXIN */
|
||||
#{$button} {
|
||||
&[data-block]{
|
||||
width:100%;
|
||||
display: flex;
|
||||
margin-right: 0!important;
|
||||
.#{$button} {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* #endif */
|
||||
.#{$button} {
|
||||
opacity: 1; // uniapp x IOS必须存在
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
display: inline-flex;
|
||||
white-space: nowrap;
|
||||
font-family: PingFang SC, Microsoft YaHei, Arial Regular;
|
||||
vertical-align: top;
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
background-color: transparent;
|
||||
/* #endif */
|
||||
|
||||
position: relative;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
// css var 不支持过渡?
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
transition-duration: 200ms;
|
||||
/* #endif */
|
||||
// background-color ios 无法恢复
|
||||
transition-property:opacity,border-color,width,height;
|
||||
|
||||
// border-radius: $button-border-radius;
|
||||
@include border-radius($button-border-radius);
|
||||
|
||||
|
||||
&__button {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
// border-width: $button-border-width;
|
||||
// border-color: transparent;
|
||||
//,&--loading
|
||||
&.#{$button}--disabled {
|
||||
opacity: $button-disabled-opacity;
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
cursor: no-drop;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
&--block {
|
||||
/* #ifdef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
// border-style: solid;
|
||||
/* #endif*/
|
||||
width: 100%;
|
||||
align-self: auto
|
||||
}
|
||||
// &--solid {
|
||||
// /* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
// color: $button-solid-text-color;
|
||||
// --l-loading-color: #{$button-solid-text-color};
|
||||
// /* #endif*/
|
||||
// /* #ifdef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
// // border-style: solid;
|
||||
// .#{$button} {
|
||||
// &__content {
|
||||
// color: $button-solid-text-color;
|
||||
// }
|
||||
// &__icon {
|
||||
// color: $button-solid-text-color;
|
||||
// }
|
||||
// &__loading {
|
||||
// // color: $button-solid-text-color;
|
||||
// }
|
||||
// }
|
||||
// /* #endif*/
|
||||
// }
|
||||
/* #ifdef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
&--outline {
|
||||
border-style: solid;
|
||||
border-width: $button-border-width;
|
||||
}
|
||||
&--dashed {
|
||||
border-style: dashed;
|
||||
border-width: $button-border-width;
|
||||
}
|
||||
/* #endif*/
|
||||
&__loading,&__icon{
|
||||
align-self: center;
|
||||
}
|
||||
&--round,&--circle {
|
||||
border-radius: 999px;
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
--l-button-border-radius: 999px
|
||||
/* #endif*/
|
||||
}
|
||||
/* #ifdef UNI-APP-X */
|
||||
.gap {
|
||||
margin-left: $button-gap;
|
||||
}
|
||||
/* #endif*/
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY || MP-WEIXIN */
|
||||
&::after {
|
||||
border-radius: calc($button-border-radius * 2);
|
||||
border-color: var(--l-button-border-color);
|
||||
}
|
||||
&--text {
|
||||
&::after {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
view+&__content:not(:empty),
|
||||
l-button+&__content:not(:empty),
|
||||
l-icon+&__content:not(:empty),
|
||||
&__loading+&__content:not(:empty),
|
||||
&__icon+&__content:not(:empty) {
|
||||
margin-left: $button-gap;
|
||||
}
|
||||
/* #endif*/
|
||||
}
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<!-- #ifdef APP-ANDROID || APP-IOS || APP-HARMONY -->
|
||||
<view class="l-button"
|
||||
ref="rootRef"
|
||||
:class="classes"
|
||||
:hover-class="disabled || loading || color != null ? '': hoverClass ?? 'l-button--hover'"
|
||||
:style="[styles, lStyle]"
|
||||
:hover-start-time="hoverStartTime"
|
||||
:hover-stay-time="hoverStayTime"
|
||||
:data-disabled="disabled||loading"
|
||||
@tap.stop="handleTap" >
|
||||
<l-loading class="l-button__loading" :color="loadingColor" v-if="loading"></l-loading>
|
||||
<l-icon class="l-button__icon" :size="innerIconSize" :color="loadingColor" v-if="icon" :name="icon"></l-icon>
|
||||
<text class="l-button__content" :class="gapClass" ref="buttonTextRef" :style="[contentStyle]">
|
||||
<slot>{{content}}</slot>
|
||||
</text>
|
||||
<button
|
||||
class="l-button__button"
|
||||
hover-class="none"
|
||||
v-if="formType != null || openType != null"
|
||||
@agreeprivacyauthorization="agreeprivacyauthorization"
|
||||
:disabled="disabled || loading"
|
||||
:form-type="disabled || loading ? '' : formType"
|
||||
:open-type="disabled || loading ? '' : openType" >
|
||||
</button>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-ANDROID || APP-IOS || APP-HARMONY -->
|
||||
<button class="l-button l-class"
|
||||
:id="lId"
|
||||
:style="[styles, lStyle]"
|
||||
:class="[
|
||||
'l-button--' + size,
|
||||
'l-button--' + type,
|
||||
'l-button--' + shape,
|
||||
'l-button--' + computedVariant,
|
||||
{
|
||||
'l-button--ghost': ghost,
|
||||
'l-button--block': block,
|
||||
'l-button--disabled': disabled,
|
||||
'l-button--loading': loading,
|
||||
}
|
||||
]"
|
||||
:form-type="disabled || loading ? '' : formType"
|
||||
:open-type="disabled || loading ? '' : openType"
|
||||
:hover-stop-propagation="hoverStopPropagation"
|
||||
:hover-start-time="hoverStartTime"
|
||||
:hover-stay-time="hoverStayTime" :lang="lang"
|
||||
:session-from="sessionFrom"
|
||||
:hover-class="hoverClasses"
|
||||
:send-message-title="sendMessageTitle"
|
||||
:send-message-path="sendMessagePath"
|
||||
:send-message-img="sendMessageImg"
|
||||
:app-parameter="appParameter"
|
||||
:show-message-card="showMessageCard"
|
||||
@tap.stop="handleTap"
|
||||
@getuserinfo="getuserinfo"
|
||||
@contact="contact"
|
||||
@getphonenumber="getphonenumber"
|
||||
@error="error"
|
||||
@opensetting="opensetting"
|
||||
@launchapp="launchapp"
|
||||
@chooseavatar="chooseavatar"
|
||||
@agreeprivacyauthorization="agreeprivacyauthorization"
|
||||
:aria-label="ariaLabel"
|
||||
:data-disabled="disabled||loading">
|
||||
<view class="l-button__loading" v-if="loading">
|
||||
<l-loading inheritColor></l-loading>
|
||||
</view>
|
||||
|
||||
<l-icon class="l-button__icon" v-if="icon" :name="icon"></l-icon>
|
||||
<view class="l-button__content" v-if="content || $slots.default" :style="[contentStyle]" :class="[gapClass]">
|
||||
<slot>{{content}}</slot>
|
||||
</view>
|
||||
</button>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
<script lang="uts" setup>
|
||||
/**
|
||||
* Button 按钮组件
|
||||
* @description 提供丰富的按钮样式和交互功能,支持多种形态和平台特性
|
||||
* <br> 插件类型:LButtonComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-button
|
||||
*
|
||||
* @property {string} ariaLabel 无障碍标签(用于屏幕阅读器)
|
||||
* @property {boolean} block 块级布局(占满父容器宽度)
|
||||
* @property {boolean} disabled 禁用状态
|
||||
* @property {boolean} ghost 幽灵模式(透明背景+边框)
|
||||
* @property {string} icon 左侧图标名称/路径
|
||||
* @property {string} iconSize 图标尺寸(支持CSS单位)
|
||||
* @property {boolean} loading 加载状态(显示加载动画)
|
||||
* @property {UTSJSONObject} loadingProps 加载动画配置
|
||||
* @property {'rectangle' | 'square' | 'round' | 'circle'} shape 按钮形状
|
||||
* @value rectangle 长方形(默认)
|
||||
* @value square 正方形
|
||||
* @value round 圆角矩形
|
||||
* @value circle 圆形
|
||||
* @property {SizeEnum} size 按钮尺寸
|
||||
* @value mini 小号
|
||||
* @value small 小号
|
||||
* @value medium 中号(默认)
|
||||
* @value large 大号
|
||||
* @property {string} suffix 右侧附加内容(图标/文本)
|
||||
* @property {'default' | 'primary' | 'danger' | 'warning' | 'success' | 'info'} type 色彩类型
|
||||
* @value default
|
||||
* @value primary
|
||||
* @value danger
|
||||
* @value warning
|
||||
* @value success
|
||||
* @value info
|
||||
* @property {'solid' | 'outline' | 'text' | 'light' | 'dashed'} variant 样式变体
|
||||
* @value solid
|
||||
* @value outline
|
||||
* @value text
|
||||
* @value light
|
||||
* @value dashed
|
||||
* @property {string} radius 自定义圆角(覆盖shape设置)
|
||||
* @property {string} fontSize 文字字号(支持CSS单位)
|
||||
* @property {string} textColor 文字颜色(支持CSS颜色值)
|
||||
* @property {string} color 主色(背景/边框色)
|
||||
*
|
||||
* @platform 微信小程序
|
||||
* @property {string} formType 表单类型(submit/reset)
|
||||
* @property {string} openType 开放能力(contact/share等)
|
||||
* @property {string} lang 用户信息语言(zh_CN/zh_TW/en)
|
||||
* @property {string} sessionFrom 会话来源(contact模式有效)
|
||||
* @property {string} sendMessageTitle 消息卡片标题(contact模式有效)
|
||||
* @property {string} sendMessagePath 消息跳转路径(contact模式有效)
|
||||
* @property {string} sendMessageImg 消息卡片图片(contact模式有效)
|
||||
* @property {string} appParameter APP启动参数(launchApp模式)
|
||||
* @property {boolean} showMessageCard 显示消息卡片提示
|
||||
*
|
||||
* @property {string} hoverClass 点击态样式类(默认:button-hover)
|
||||
* @property {boolean} hoverStopPropagation 阻止祖先点击态
|
||||
* @property {number} hoverStartTime 点击态延迟(默认:20ms)
|
||||
* @property {number} hoverStayTime 点击态保留时间(默认:70ms)
|
||||
* @event {Function} click 点击事件(禁用状态下不触发)
|
||||
* @event {Function} getuserinfo 用户信息授权回调
|
||||
* @event {Function} contact 客服会话回调
|
||||
* @event {Function} agreeprivacyauthorization
|
||||
* @event {Function} chooseavatar
|
||||
* @event {Function} getphonenumber
|
||||
* @event {Function} error
|
||||
* @event {Function} opensetting
|
||||
* @event {Function} launchapp
|
||||
*/
|
||||
|
||||
import { ButtonProps } from './type'
|
||||
// #ifdef APP
|
||||
import { getCssVariableColor } from './utils'
|
||||
// #endif
|
||||
defineOptions({
|
||||
behaviors: ['wx://form-field-button']
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click','agreeprivacyauthorization', 'chooseavatar', 'getuserinfo','contact', 'getphonenumber', 'error', 'opensetting', 'launchapp'])
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
block: false,
|
||||
disabled: false,
|
||||
ghost: false,
|
||||
loading: false,
|
||||
shape: 'rectangle',
|
||||
size: 'medium',
|
||||
type: 'default',
|
||||
// hoverClass: '',
|
||||
hoverStopPropagation: false,
|
||||
hoverStartTime: 20,
|
||||
hoverStayTime: 70,
|
||||
lang: 'en',
|
||||
sessionFrom: '',
|
||||
sendMessageTitle: '',
|
||||
sendMessagePath: '',
|
||||
sendMessageImg: '',
|
||||
appParameter: '',
|
||||
showMessageCard: false
|
||||
// variant: 'solid'
|
||||
})
|
||||
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
// const rootRef = ref<UniElement|null>(null)
|
||||
const buttonTextRef = ref<UniTextElement|null>(null)
|
||||
const hasContent = computed(():boolean => {
|
||||
return props.content != null || instance?.proxy?.$slots?.['default'] != null
|
||||
})
|
||||
const variant = computed(():string => props.variant ?? (props.color != null ? 'solid' : (props.type == 'default' ? 'outline': 'solid')))
|
||||
const classes = computed(():Map<string, any>=>{
|
||||
const cls = new Map<string, any>()
|
||||
const name = 'l-button'
|
||||
cls.set(`${name}--${props.size}`, true)
|
||||
cls.set(`${name}--${props.type}`, true)
|
||||
cls.set(`${name}--${variant.value}`, true)
|
||||
cls.set(`${name}--${props.shape}`, true)
|
||||
cls.set(`${name}--disabled`, props.disabled)
|
||||
cls.set(`${name}--loading`, props.loading)
|
||||
cls.set(`${name}--ghost`, props.ghost)
|
||||
cls.set(`${name}--block`, props.block)
|
||||
|
||||
return cls
|
||||
})
|
||||
|
||||
const styles = computed(():Map<string, any>=>{
|
||||
const style = new Map<string, any>()
|
||||
// #ifndef APP
|
||||
if(props.gap) {
|
||||
style.set('--l-button-gap', props.gap)
|
||||
}
|
||||
// #endif
|
||||
if(props.radius != null) {
|
||||
style.set('border-radius', props.radius!)
|
||||
}
|
||||
if(props.color != null) {
|
||||
if(variant.value == 'solid') {
|
||||
style.set('background', props.color!)
|
||||
} else if(['outline', 'dashed'].includes(variant.value)) {
|
||||
// #ifdef APP
|
||||
style.set('border-color', props.color!)
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
style.set('--l-button-default-border-color', props.color!)
|
||||
style.set('--l-button-border-color', props.color!)
|
||||
// #endif
|
||||
}
|
||||
|
||||
}
|
||||
return style
|
||||
})
|
||||
|
||||
const sizes = new Map<string, string>([
|
||||
// #ifdef APP-ANDROID || APP-IOS || APP-HARMONY
|
||||
['mini', '16px'],
|
||||
['small', '18px'],
|
||||
['medium', '18px'],
|
||||
['large', '24px'],
|
||||
// #endif
|
||||
])
|
||||
const innerIconSize = computed(():string|null=> {
|
||||
return props.iconSize ?? props.fontSize ?? sizes.get(props.size)
|
||||
})
|
||||
const colors = new Map<string, string>([
|
||||
// #ifdef APP-ANDROID || APP-IOS || APP-HARMONY
|
||||
// ['default', 'rgba(0,0,0,0.88)'],
|
||||
// ['primary', '#3283ff'],
|
||||
// ['danger', '#FF4D4F'],
|
||||
// ['warning', '#ffb400'],
|
||||
// ['success', '#34c471'],
|
||||
// ['info', '#3283ff'],
|
||||
// #endif
|
||||
])
|
||||
const loadingColor = computed(():string|null => {
|
||||
// #ifdef APP
|
||||
console.log(`111props.textColor ?? (variant.value == 'solid' ? 'white' : getCssVariableColor(buttonTextRef.value)) ?? ''`, props.textColor ?? (variant.value == 'solid' ? 'white' : getCssVariableColor(buttonTextRef.value)) ?? '')
|
||||
return props.textColor ?? (variant.value == 'solid' ? 'white' : getCssVariableColor(buttonTextRef.value)) ?? ''//colors.get(props.type))
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
return props.textColor ?? (variant.value == 'solid' ? 'white' : colors.get(props.type))
|
||||
// #endif
|
||||
})
|
||||
|
||||
const gapClass = computed((): string => {
|
||||
return props.loading || props.icon != null ? 'gap' : ''
|
||||
})
|
||||
const contentStyle = computed(():Map<string, any> => {
|
||||
const style = new Map<string, any>()
|
||||
if(props.gap != null && (props.loading || props.icon != null)) {
|
||||
style.set('margin-left', props.gap!)
|
||||
}
|
||||
if(props.textColor != null || props.color != null && variant.value != 'solid') {
|
||||
style.set('color', (props.textColor ?? props.color)!)
|
||||
}
|
||||
if(props.fontSize != null) {
|
||||
style.set('font-size', props.fontSize!)
|
||||
}
|
||||
return style
|
||||
})
|
||||
// #ifndef APP
|
||||
const hoverClasses = computed(():string => {
|
||||
return props.disabled || props.loading ? '' : (props.hoverClass || `l-button--hover`)
|
||||
})
|
||||
const computedVariant = computed(():string => props.variant ?? (props.color ? 'solid' : (props.type == 'default' ? 'outline': 'solid')))
|
||||
// #endif
|
||||
const getuserinfo = (e: UniEvent) => {
|
||||
emit('getuserinfo', e);
|
||||
}
|
||||
const contact = (e: UniEvent) => {
|
||||
emit('contact', e);
|
||||
}
|
||||
const getphonenumber = (e: UniEvent) => {
|
||||
emit('getphonenumber', e);
|
||||
}
|
||||
const error = (e: UniEvent) => {
|
||||
emit('error', e);
|
||||
}
|
||||
const opensetting = (e: UniEvent) => {
|
||||
emit('opensetting', e);
|
||||
}
|
||||
const launchapp = (e: UniEvent) => {
|
||||
emit('launchapp', e);
|
||||
}
|
||||
const chooseavatar = (e: UniEvent) => {
|
||||
emit('chooseavatar', e);
|
||||
}
|
||||
const agreeprivacyauthorization = (e: UniEvent) => {
|
||||
emit('agreeprivacyauthorization', e);
|
||||
}
|
||||
const handleTap = (e: UniPointerEvent)=>{
|
||||
if (props.disabled || props.loading) return;
|
||||
emit('click', e);
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import './index-u';
|
||||
</style>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<button class="l-button l-class"
|
||||
:id="lId"
|
||||
:style="[styles, lStyle]"
|
||||
:class="[
|
||||
'l-button--' + size,
|
||||
'l-button--' + type,
|
||||
'l-button--' + shape,
|
||||
'l-button--' + computedVariant,
|
||||
{
|
||||
'l-button--ghost': ghost,
|
||||
'l-button--block': block,
|
||||
'l-button--disabled': disabled,
|
||||
'l-button--loading': loading,
|
||||
}
|
||||
]"
|
||||
:form-type="disabled || loading ? '' : formType"
|
||||
:open-type="disabled || loading ? '' : openType"
|
||||
:hover-stop-propagation="hoverStopPropagation"
|
||||
:hover-start-time="hoverStartTime"
|
||||
:hover-stay-time="hoverStayTime" :lang="lang"
|
||||
:session-from="sessionFrom"
|
||||
:hover-class="hoverClasses"
|
||||
:send-message-title="sendMessageTitle"
|
||||
:send-message-path="sendMessagePath"
|
||||
:send-message-img="sendMessageImg"
|
||||
:app-parameter="appParameter"
|
||||
:show-message-card="showMessageCard"
|
||||
@tap.stop="handleTap"
|
||||
@getuserinfo="getuserinfo"
|
||||
@contact="contact"
|
||||
@getphonenumber="getphonenumber"
|
||||
@error="error"
|
||||
@opensetting="opensetting"
|
||||
@launchapp="launchapp"
|
||||
@chooseavatar="chooseavatar"
|
||||
@agreeprivacyauthorization="agreeprivacyauthorization"
|
||||
:aria-label="ariaLabel"
|
||||
:data-disabled="disabled||loading">
|
||||
<view class="l-button__loading" v-if="loading">
|
||||
<l-loading inheritColor></l-loading>
|
||||
</view>
|
||||
<l-icon class="l-button__icon" v-if="icon" :name="icon"></l-icon>
|
||||
<view class="l-button__content" v-if="content || $slots.default">
|
||||
<slot>{{content}}</slot>
|
||||
</view>
|
||||
</button>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Button 按钮组件
|
||||
* @description 提供丰富的按钮样式和交互功能,支持多种形态和平台特性
|
||||
* <br> 插件类型:LButtonComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-button
|
||||
*
|
||||
* @property {string} ariaLabel 无障碍标签(用于屏幕阅读器)
|
||||
* @property {boolean} block 块级布局(占满父容器宽度)
|
||||
* @property {boolean} disabled 禁用状态
|
||||
* @property {boolean} ghost 幽灵模式(透明背景+边框)
|
||||
* @property {string} icon 左侧图标名称/路径
|
||||
* @property {string} iconSize 图标尺寸(支持CSS单位)
|
||||
* @property {boolean} loading 加载状态(显示加载动画)
|
||||
* @property {UTSJSONObject} loadingProps 加载动画配置
|
||||
* @property {'rectangle' | 'square' | 'round' | 'circle'} shape 按钮形状
|
||||
* @value rectangle 长方形(默认)
|
||||
* @value square 正方形
|
||||
* @value round 圆角矩形
|
||||
* @value circle 圆形
|
||||
* @property {SizeEnum} size 按钮尺寸
|
||||
* @value small 小号
|
||||
* @value medium 中号(默认)
|
||||
* @value large 大号
|
||||
* @property {string} suffix 右侧附加内容(图标/文本)
|
||||
* @property {'default' | 'primary' | 'danger' | 'warning' | 'success' | 'info'} type 色彩类型
|
||||
* @value default
|
||||
* @value primary
|
||||
* @value danger
|
||||
* @value warning
|
||||
* @value success
|
||||
* @value info
|
||||
* @property {'solid' | 'outline' | 'text' | 'light' | 'dashed'} variant 样式变体
|
||||
* @value solid
|
||||
* @value outline
|
||||
* @value text
|
||||
* @value light
|
||||
* @value dashed
|
||||
* @property {string} radius 自定义圆角(覆盖shape设置)
|
||||
* @property {string} fontSize 文字字号(支持CSS单位)
|
||||
* @property {string} textColor 文字颜色(支持CSS颜色值)
|
||||
* @property {string} color 主色(背景/边框色)
|
||||
*
|
||||
* @platform 微信小程序
|
||||
* @property {string} formType 表单类型(submit/reset)
|
||||
* @property {string} openType 开放能力(contact/share等)
|
||||
* @property {string} lang 用户信息语言(zh_CN/zh_TW/en)
|
||||
* @property {string} sessionFrom 会话来源(contact模式有效)
|
||||
* @property {string} sendMessageTitle 消息卡片标题(contact模式有效)
|
||||
* @property {string} sendMessagePath 消息跳转路径(contact模式有效)
|
||||
* @property {string} sendMessageImg 消息卡片图片(contact模式有效)
|
||||
* @property {string} appParameter APP启动参数(launchApp模式)
|
||||
* @property {boolean} showMessageCard 显示消息卡片提示
|
||||
*
|
||||
* @property {string} hoverClass 点击态样式类(默认:button-hover)
|
||||
* @property {boolean} hoverStopPropagation 阻止祖先点击态
|
||||
* @property {number} hoverStartTime 点击态延迟(默认:20ms)
|
||||
* @property {number} hoverStayTime 点击态保留时间(默认:70ms)
|
||||
* @event {Function} click 点击事件(禁用状态下不触发)
|
||||
* @event {Function} getuserinfo 用户信息授权回调
|
||||
* @event {Function} contact 客服会话回调
|
||||
* @event {Function} agreeprivacyauthorization
|
||||
* @event {Function} chooseavatar
|
||||
* @event {Function} getphonenumber
|
||||
* @event {Function} error
|
||||
* @event {Function} opensetting
|
||||
* @event {Function} launchapp
|
||||
*/
|
||||
import { computed, defineComponent } from '@/uni_modules/lime-shared/vue';
|
||||
import ButtonProps from './props'
|
||||
const name = 'l-button'
|
||||
export default defineComponent({
|
||||
name,
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true
|
||||
},
|
||||
behaviors: ['wx://form-field-button'],
|
||||
externalClasses: ['l-class',`l-class-icon`, `l-class-loading`],
|
||||
props: ButtonProps,
|
||||
emits: ['click','agreeprivacyauthorization', 'chooseavatar', 'getuserinfo','contact', 'getphonenumber', 'error', 'opensetting', 'launchapp'],
|
||||
setup(props, { emit }) {
|
||||
|
||||
const computedVariant = computed(():string => props.variant || (props.color ? 'solid' : (props.type == 'default' ? 'outline': 'solid')))
|
||||
const classes = computed(() => {
|
||||
return {
|
||||
[`${name}--${props.size}`]: true,
|
||||
[`${name}--${props.type}`]: true,
|
||||
[`${name}--${props.shape}`]: true,
|
||||
[`${name}--${computedVariant .value}`]: true,
|
||||
// [`${name}--color`]: props.color,
|
||||
[`${name}--ghost`]: props.ghost,
|
||||
[`${name}--block`]: props.block,
|
||||
[`${name}--disabled`]: props.disabled,
|
||||
[`${name}--loading`]: props.loading,
|
||||
}
|
||||
})
|
||||
const hoverClasses = computed(() => {
|
||||
return props.disabled || props.loading ? '' : (props.hoverClass || `l-button--hover`)
|
||||
})
|
||||
const styles = computed(()=>{
|
||||
const style:Record<string, any> = {}
|
||||
if(props.gap) {
|
||||
style['--l-button-gap'] = props.gap
|
||||
}
|
||||
if(props.color){
|
||||
if(['outline'].includes(props.variant) ){
|
||||
style['--l-button-border-color'] = props.color
|
||||
style['color'] = props.color
|
||||
} else {
|
||||
style['--l-button-border-color'] = props.color
|
||||
style['background'] = props.color
|
||||
style['color'] = 'white'
|
||||
}
|
||||
}
|
||||
|
||||
return style
|
||||
})
|
||||
const getuserinfo = (e) => {
|
||||
emit('getuserinfo', e);
|
||||
}
|
||||
const contact = (e) => {
|
||||
emit('contact', e);
|
||||
}
|
||||
const getphonenumber = (e) => {
|
||||
emit('getphonenumber', e);
|
||||
}
|
||||
const error = (e) => {
|
||||
emit('error', e);
|
||||
}
|
||||
const opensetting = (e) => {
|
||||
emit('opensetting', e);
|
||||
}
|
||||
const launchapp = (e) => {
|
||||
emit('launchapp', e);
|
||||
}
|
||||
const chooseavatar = (e) => {
|
||||
emit('chooseavatar', e);
|
||||
}
|
||||
const agreeprivacyauthorization = (e) => {
|
||||
emit('agreeprivacyauthorization', e);
|
||||
}
|
||||
const handleTap = (e)=>{
|
||||
if (props.disabled || props.loading) return;
|
||||
emit('click', e);
|
||||
}
|
||||
|
||||
return {
|
||||
classes,
|
||||
hoverClasses,
|
||||
styles,
|
||||
computedVariant,
|
||||
agreeprivacyauthorization,
|
||||
chooseavatar,
|
||||
getuserinfo,
|
||||
launchapp,
|
||||
opensetting,
|
||||
error,
|
||||
getphonenumber,
|
||||
contact,
|
||||
handleTap
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import './index-u';
|
||||
</style>
|
||||
@@ -0,0 +1,152 @@
|
||||
export const ariaProps = {
|
||||
ariaHidden: Boolean,
|
||||
ariaRole: String,
|
||||
ariaLabel: String,
|
||||
ariaLabelledby: String,
|
||||
ariaDescribedby: String,
|
||||
ariaBusy: Boolean,
|
||||
lStyle: String
|
||||
}
|
||||
export default {
|
||||
...ariaProps,
|
||||
// lClass:String,
|
||||
lId: String,
|
||||
type: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator(val : string) : boolean {
|
||||
return ['default', 'primary', 'danger', 'success', 'warning'].includes(val);
|
||||
},
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: null,
|
||||
validator(val : string) : boolean {
|
||||
return ['solid', 'outline', 'dashed','text', 'light'].includes(val);
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator(val : string) : boolean {
|
||||
if (val == '') return true;
|
||||
return ['mini', 'small', 'medium', 'large'].includes(val);
|
||||
},
|
||||
},
|
||||
shape: {
|
||||
type: String,
|
||||
default: 'rectangle',
|
||||
validator(val : string) : boolean {
|
||||
return ['rectangle', 'square', 'round', 'circle'].includes(val);
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
type: String
|
||||
},
|
||||
iconSize: {
|
||||
type: String
|
||||
},
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ghost: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
radius: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
textColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
formType: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
openType: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
|
||||
/** 指定按钮按下去的样式类,按钮不为加载或禁用状态时有效。当 `hover-class="none"` 时,没有点击态效果 */
|
||||
hoverClass: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
/** 指定是否阻止本节点的祖先节点出现点击态 */
|
||||
hoverStopPropagation: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 按住后多久出现点击态,单位毫秒 */
|
||||
hoverStartTime: {
|
||||
type: Number,
|
||||
default: 20,
|
||||
},
|
||||
/** 手指松开后点击态保留时间,单位毫秒 */
|
||||
hoverStayTime: {
|
||||
type: Number,
|
||||
default: 70,
|
||||
},
|
||||
/** 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。。<br />具体释义:<br />`en` 英文;<br />`zh_CN` 简体中文;<br />`zh_TW` 繁体中文。<br />[小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/component/button.html) */
|
||||
lang: {
|
||||
type: String,
|
||||
default: 'en',
|
||||
},
|
||||
/** 会话来源,open-type="contact"时有效 */
|
||||
sessionFrom: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
/** 会话内消息卡片标题,open-type="contact"时有效 */
|
||||
sendMessageTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
/** 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效 */
|
||||
sendMessagePath: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
/** 会话内消息卡片图片,open-type="contact"时有效 */
|
||||
sendMessageImg: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
/** 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效 */
|
||||
appParameter: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
/** 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,用户点击后可以快速发送小程序消息,open-type="contact"时有效 */
|
||||
showMessageCard: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
gap: {
|
||||
type: String
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// @ts-nocheck
|
||||
export type SizeEnum = 'small' | 'medium' | 'large';
|
||||
export interface ButtonProps {
|
||||
ariaLabel?: string;
|
||||
lId ?: string;
|
||||
content ?: string
|
||||
/**
|
||||
* 是否为块级元素
|
||||
*/
|
||||
block : boolean;
|
||||
/**
|
||||
* 禁用状态
|
||||
*/
|
||||
disabled : boolean;
|
||||
/**
|
||||
* 是否为幽灵按钮(镂空按钮)
|
||||
*/
|
||||
ghost : boolean;
|
||||
/**
|
||||
* 按钮内部图标,可完全自定义
|
||||
*/
|
||||
icon ?: string;
|
||||
iconSize ?: string;
|
||||
|
||||
/**
|
||||
* 是否显示为加载状态
|
||||
*/
|
||||
loading : boolean;
|
||||
/**
|
||||
* 透传加载组件全部属性
|
||||
*/
|
||||
loadingProps ?: UTSJSONObject//LoadingProps;
|
||||
/**
|
||||
* 按钮形状:长方形、正方形、圆角长方形、圆形
|
||||
*/
|
||||
shape : 'rectangle' | 'square' | 'round' | 'circle';
|
||||
/**
|
||||
* 组件尺寸
|
||||
*/
|
||||
size : SizeEnum;
|
||||
/**
|
||||
* 右侧内容,可用于定义右侧图标
|
||||
*/
|
||||
suffix ?: string;
|
||||
/**
|
||||
* 组件风格,依次为品牌色、危险色、
|
||||
*/
|
||||
type : 'default' | 'primary' | 'danger' | 'warning' | 'success' | 'info';
|
||||
/**
|
||||
* 按钮形式,基础、线框、文字、高亮、虚线
|
||||
*/
|
||||
variant ?: 'solid' | 'outline' | 'text' | 'light' | 'dashed';
|
||||
radius ?: string
|
||||
fontSize ?: string;
|
||||
textColor ?: string;
|
||||
color ?: string;
|
||||
lStyle?: string;
|
||||
gap?: string;
|
||||
formType ?: string
|
||||
openType ?: string
|
||||
|
||||
/** 指定按钮按下去的样式类,按钮不为加载或禁用状态时有效。当 `hover-class="none"` 时,没有点击态效果 */
|
||||
hoverClass ?: string
|
||||
/** 指定是否阻止本节点的祖先节点出现点击态 */
|
||||
hoverStopPropagation : boolean
|
||||
/** 按住后多久出现点击态,单位毫秒 */
|
||||
hoverStartTime : number
|
||||
/** 手指松开后点击态保留时间,单位毫秒 */
|
||||
hoverStayTime : number
|
||||
/** 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。。<br />具体释义:<br />`en` 英文;<br />`zh_CN` 简体中文;<br />`zh_TW` 繁体中文。<br />[小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/component/button.html) */
|
||||
lang :string
|
||||
/** 会话来源,open-type="contact"时有效 */
|
||||
sessionFrom : string,
|
||||
/** 会话内消息卡片标题,open-type="contact"时有效 */
|
||||
sendMessageTitle : string,
|
||||
/** 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效 */
|
||||
sendMessagePath : string,
|
||||
/** 会话内消息卡片图片,open-type="contact"时有效 */
|
||||
sendMessageImg : string,
|
||||
/** 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效 */
|
||||
appParameter : string,
|
||||
/** 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,用户点击后可以快速发送小程序消息,open-type="contact"时有效 */
|
||||
showMessageCard : boolean,
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 简单检查是否是有效的颜色值
|
||||
* @param {string} value
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isValidColor(value : string | null) {
|
||||
if (value == null) return false;
|
||||
|
||||
// 常见颜色格式检查
|
||||
return (
|
||||
// 颜色关键字(red, blue 等)
|
||||
/^[a-z]+$/i.test(value!) ||
|
||||
// 十六进制(#fff, #ffffff)
|
||||
/^#([0-9a-f]{3}){1,2}$/i.test(value!) ||
|
||||
// rgb/rgba
|
||||
/^rgba?\([\s\d.,%]+\)$/.test(value!) ||
|
||||
// hsl/hsla
|
||||
/^hsla?\([\s\d.,%]+\)$/.test(value!)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function getCssVariableColor(element : UniTextElement | null) : string | null {
|
||||
if (element == null) return null
|
||||
let value = element!.style.getPropertyValue('color').trim();
|
||||
|
||||
// 如果没有找到变量值,直接返回 null
|
||||
if (value == '') return null;
|
||||
if(value.startsWith('var(')) {
|
||||
const innerVar = value.match(/var\(([^,)]+)(?:,\s*(.*))?\)/);
|
||||
if(innerVar == null) return null
|
||||
const [, , innerFallback] = innerVar!;
|
||||
return innerFallback
|
||||
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
|
||||
}
|
||||
110
qiming-mobile/uni_modules/lime-button/package.json
Normal file
110
qiming-mobile/uni_modules/lime-button/package.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"id": "lime-button",
|
||||
"displayName": "lime-button 按钮",
|
||||
"version": "0.1.4",
|
||||
"description": "lime-button 按钮用于触发一个操作,支持自定义颜色等,兼容uniapp/uniappx",
|
||||
"keywords": [
|
||||
"lime-button",
|
||||
"button",
|
||||
"按钮"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.28",
|
||||
"uni-app": "^4.73",
|
||||
"uni-app-x": "^4.76"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "√",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"lime-style",
|
||||
"lime-shared",
|
||||
"lime-icon",
|
||||
"lime-loading"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "√"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "√",
|
||||
"vue3": "√"
|
||||
},
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"vue": "√",
|
||||
"nvue": "-",
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√",
|
||||
"alipay": "-",
|
||||
"toutiao": "-",
|
||||
"baidu": "-",
|
||||
"kuaishou": "-",
|
||||
"jd": "-",
|
||||
"harmony": "-",
|
||||
"qq": "-",
|
||||
"lark": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "-",
|
||||
"union": "-"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
232
qiming-mobile/uni_modules/lime-button/readme - 副本.md
Normal file
232
qiming-mobile/uni_modules/lime-button/readme - 副本.md
Normal file
@@ -0,0 +1,232 @@
|
||||
# lime-button 按钮
|
||||
按钮用于开始一个即时操作,是用户与应用交互的基础组件。本组件提供了丰富的按钮类型、样式和状态,满足各种场景需求。
|
||||
|
||||
> 插件依赖:`lime-style`、`lime-shared`、`lime-loading`、`lime-icon`
|
||||
|
||||
|
||||
|
||||
## 文档链接
|
||||
📚 组件详细文档请访问以下站点:
|
||||
- [按钮文档 - 站点1](https://limex.qcoon.cn/components/button.html)
|
||||
- [按钮文档 - 站点2](https://limeui.netlify.app/components/button.html)
|
||||
- [按钮文档 - 站点3](https://limeui.familyzone.top/components/button.html)
|
||||
|
||||
|
||||
## 安装方法
|
||||
1. 在uni-app插件市场中搜索并导入`lime-button`
|
||||
2. 导入后可能需要重新编译项目
|
||||
3. 在页面中使用`l-button`组件(组件)或`lime-button`(演示)
|
||||
|
||||
::: tip 注意🔔
|
||||
本插件依赖的[【lime-svg】](https://ext.dcloud.net.cn/plugin?id=18519)是原生插件,如果购买(收费为6元)则需要自定义基座,才能使用,
|
||||
若不需要删除即可
|
||||
:::
|
||||
|
||||
|
||||
## 代码演示
|
||||
|
||||
### 按钮主题
|
||||
|
||||
按钮支持 `default`、`primary`、`success`、`warning`、`danger` 五种主题,默认为 `default`。
|
||||
|
||||
```html
|
||||
<l-button type="primary">品牌色</l-button>
|
||||
<l-button type="success">成功色</l-button>
|
||||
<l-button type="warning">警告色</l-button>
|
||||
<l-button type="danger">危险色</l-button>
|
||||
<l-button>通用色</l-button>
|
||||
```
|
||||
|
||||
### 按钮变体
|
||||
|
||||
按钮支持 `solid`、`outline`、`dashed`、`light`、`text`,默认为 `solid`。
|
||||
|
||||
```html
|
||||
<l-button type="primary">默认按钮</l-button>
|
||||
<l-button type="primary" variant="outline">镂空按钮</l-button>
|
||||
<l-button type="primary" variant="light">高亮按钮</l-button>
|
||||
<l-button type="primary" variant="text">文本按钮</l-button>
|
||||
```
|
||||
|
||||
### 按钮尺寸
|
||||
|
||||
按钮支持 `large`、`medium`、`small`、`mini` 四种尺寸,默认为 `medium`。还有一个通栏 `block` 或 `data-block`,之所以多加了个`data-block`是因为它会编译到节点上
|
||||
|
||||
```html
|
||||
<l-button type="primary" size="large" data-block>通栏</l-button>
|
||||
<l-button type="primary" size="large">大</l-button>
|
||||
<l-button type="warning" size="medium">中</l-button>
|
||||
<l-button type="primary" size="small">小</l-button>
|
||||
<l-button type="success" size="mini">细</l-button>
|
||||
```
|
||||
|
||||
### 按钮形状
|
||||
|
||||
按钮支持 `circle`、`round`、`square`、`rectangle` 四种尺寸,默认为 `rectangle`。
|
||||
|
||||
```html
|
||||
<l-button type="primary" shape="circle">圆形</l-button>
|
||||
<l-button type="primary" shape="round">圆角矩形</l-button>
|
||||
<l-button type="primary" shape="square">正方形</l-button>
|
||||
<l-button type="primary">长方形</l-button>
|
||||
```
|
||||
|
||||
|
||||
### 自定义颜色
|
||||
|
||||
通过 `color` 属性来设置徽标的颜色。
|
||||
|
||||
```html
|
||||
<l-button color="#7232dd">单色按钮</l-button>
|
||||
<l-button color="#7232dd" type="outline">镂空按钮</l-button>
|
||||
<l-button color="linear-gradient(to right, rgb(255, 96, 52), rgb(238, 10, 36))">渐变按钮</l-button>
|
||||
```
|
||||
|
||||
### 加载状态
|
||||
|
||||
通过 `loading` 属性来禁用按钮,加载状态下按钮不可点击。
|
||||
|
||||
```html
|
||||
<l-button :loading="true" type="primary">加载中</l-button>
|
||||
```
|
||||
|
||||
|
||||
### 禁用状态
|
||||
|
||||
通过 `disabled` 属性来禁用按钮,禁用状态下按钮不可点击。
|
||||
|
||||
```html
|
||||
<l-button :disabled="true" type="primary">默认按钮</l-button>
|
||||
```
|
||||
|
||||
|
||||
## 快速预览
|
||||
导入插件后,可以直接使用以下标签查看演示效果:
|
||||
|
||||
```html
|
||||
<!-- 代码位于 uni_modules/lime-button/components/lime-button -->
|
||||
<lime-button />
|
||||
```
|
||||
|
||||
## Vue2使用说明
|
||||
本插件使用了`composition-api`,如需在Vue2项目中使用,请按照[官方教程](https://uniapp.dcloud.net.cn/tutorial/vue-composition-api.html)配置。
|
||||
|
||||
关键配置代码(在main.js中添加):
|
||||
```js
|
||||
// vue2
|
||||
import Vue from 'vue'
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
Vue.use(VueCompositionAPI)
|
||||
```
|
||||
|
||||
|
||||
|
||||
## API文档
|
||||
|
||||
### Props
|
||||
|
||||
名称 | 类型 | 默认值 | 说明 | 必传
|
||||
-- | -- | -- | -- | --
|
||||
l-id | String | - | 按钮标签id | N
|
||||
variant | String | solid | 按钮形式,基础、线框、文字。可选项:solid/outline/dashed/light/text | N
|
||||
type | String | default | 组件风格,依次为品牌色、危险色。可选项:default/primary/danger/warning/success | N
|
||||
block | Boolean | false | 是否为块级元素 | N
|
||||
content | String | - | 按钮内容 | N
|
||||
disabled | Boolean | false | 禁用状态 | N
|
||||
ghost | Boolean | false | 幽灵按钮 | N
|
||||
icon | String | - | 图标名称。值为字符串表示图标名称。 | N
|
||||
iconSize | String | - | 图标大小。 | N
|
||||
loading | Boolean | false | 是否显示为加载状态 | N
|
||||
shape | String | rectangle | 按钮形状,有 4 种:长方形、正方形、圆角长方形、圆形。可选项:rectangle/square/round/circle | N
|
||||
size | String | medium | 组件尺寸。可选项:mini/small/medium/large。TS 类型:`SizeEnum` | N
|
||||
radius | String | - | 圆角 | N
|
||||
fontSize | String | - | 文本大小 | N
|
||||
gap | String | - | 文本与图标的间距 | N
|
||||
textColor | String | - | 文本颜色 | N
|
||||
color | String | - | 按钮颜色 | N
|
||||
formType | String | - | 同小程序的 formType。可选项:submit/reset | N
|
||||
open-type | String | - | 微信开放能力。<br />具体释义:<br />`contact` 打开客服会话,如果用户在会话中点击消息卡片后返回小程序,可以从 bindcontact 回调中获得具体信息,<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/customer-message/customer-message.html">具体说明</a> (*小程序插件中不能使用*);<br />`share` 触发用户转发,使用前建议先阅读<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html#使用指引">使用指引</a>;<br />`getPhoneNumber` 获取用户手机号,可以从 bindgetphonenumber 回调中获取到用户信息,<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html">具体说明</a> (*小程序插件中不能使用*);<br />`getUserInfo` 获取用户信息,可以从 bindgetuserinfo 回调中获取到用户信息 (*小程序插件中不能使用*);<br />`launchApp` 打开APP,可以通过 app-parameter 属性设定向 APP 传的参数<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/launchApp.html">具体说明</a>;<br />`openSetting` 打开授权设置页;<br />`feedback` 打开“意见反馈”页面,用户可提交反馈内容并上传<a href="https://developers.weixin.qq.com/miniprogram/dev/api/base/debug/wx.getLogManager.html">日志</a>,开发者可以登录<a href="https://mp.weixin.qq.com/">小程序管理后台</a>后进入左侧菜单“客服反馈”页面获取到反馈内容;<br />`chooseAvatar` 获取用户头像,可以从 bindchooseavatar 回调中获取到头像信息;<br />`agreePrivacyAuthorization`用户同意隐私协议按钮。用户点击一次此按钮后,所有隐私接口可以正常调用。可通过`bindagreeprivacyauthorization`监听用户同意隐私协议事件。隐私合规开发指南详情可见《<a href="https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/PrivacyAuthorize.html">小程序隐私协议开发指南</a>》。<br />[小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/component/button.html)。可选项:contact/share/getPhoneNumber/getUserInfo/launchApp/openSetting/feedback/chooseAvatar/agreePrivacyAuthorization | N
|
||||
hover-class | String | '' | 指定按钮按下去的样式类,按钮不为加载或禁用状态时有效。当 `hover-class="none"` 时,没有点击态效果 | N
|
||||
hover-stop-propagation | Boolean | false | 指定是否阻止本节点的祖先节点出现点击态 | N
|
||||
hover-start-time | Number | 20 | 按住后多久出现点击态,单位毫秒 | N
|
||||
hover-stay-time | Number | 70 | 手指松开后点击态保留时间,单位毫秒 | N
|
||||
lang | String | en | 指定返回用户信息的语言,zh_CN 简体中文,zh_TW 繁体中文,en 英文。。<br />具体释义:<br />`en` 英文;<br />`zh_CN` 简体中文;<br />`zh_TW` 繁体中文。<br />[小程序官方文档](https://developers.weixin.qq.com/miniprogram/dev/component/button.html)。可选项:en/zh_CN/zh_TW | N
|
||||
session-from | String | - | 会话来源,open-type="contact"时有效 | N
|
||||
send-message-title | String | 当前标题 | 会话内消息卡片标题,open-type="contact"时有效 | N
|
||||
send-message-path | String | 当前分享路径 | 会话内消息卡片点击跳转小程序路径,open-type="contact"时有效 | N
|
||||
send-message-img | String | 截图 | 会话内消息卡片图片,open-type="contact"时有效 | N
|
||||
app-parameter | String | - | 打开 APP 时,向 APP 传递的参数,open-type=launchApp时有效 | N
|
||||
show-message-card | Boolean | false | 是否显示会话内消息卡片,设置此参数为 true,用户进入客服会话会在右下角显示"可能要发送的小程序"提示,用户点击后可以快速发送小程序消息,open-type="contact"时有效 | N
|
||||
getuserinfo | Eventhandle | - | 用户点击该按钮时,会返回获取到的用户信息,回调的 detail 数据与<a href="https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserInfo.html">wx.getUserInfo</a>返回的一致,open-type="getUserInfo"时有效 | N
|
||||
contact | Eventhandle | - | 客服消息回调,open-type="contact"时有效 | N
|
||||
getphonenumber | Eventhandle | - | 获取用户手机号回调,open-type=getPhoneNumber时有效 | N
|
||||
error | Eventhandle | - | 当使用开放能力时,发生错误的回调,open-type=launchApp时有效 | N
|
||||
opensetting | Eventhandle | - | 在打开授权设置页后回调,open-type=openSetting时有效 | N
|
||||
launchapp | Eventhandle | - | 打开 APP 成功的回调,open-type=launchApp时有效 | N
|
||||
chooseavatar | Eventhandle | - | 获取用户头像回调,open-type=chooseAvatar时有效 | N
|
||||
agreeprivacyauthorization | Eventhandle | - | 用户同意隐私协议事件回调,open-type=agreePrivacyAuthorization时有效 | N
|
||||
|
||||
### Slots
|
||||
|
||||
| 名称 | 说明 |
|
||||
| ------- | ---------------- |
|
||||
| default | 包裹的子元素 |
|
||||
|
||||
|
||||
## 主题定制
|
||||
|
||||
### 样式变量
|
||||
|
||||
组件提供了下列 CSS 变量,可用于自定义样式。uvue app无效
|
||||
|
||||
| 名称 | 默认值 | 描述 |
|
||||
| --- | --- | --- |
|
||||
--l-button-border-radius | 6rpx | -
|
||||
--l-button-border-width | 1px | -
|
||||
--l-button-disabled-opacity | 0.6 | -
|
||||
--l-button-solid-text-color | white | -
|
||||
--l-button-default-color | $text-color-1 | -
|
||||
--l-button-default-hover-color | rgba(0,0,0,1) | -
|
||||
--l-button-default-light-color | $gray-2 | -
|
||||
--l-button-default-light-hover-color | $gray-3 | -
|
||||
--l-button-default-border-color | $gray-5 | -
|
||||
--l-button-primary-color | $primary-color | -
|
||||
--l-button-primary-hover-color | $primary-color-7 | -
|
||||
--l-button-primary-light-color | $primary-color-1 | -
|
||||
--l-button-primary-light-hover-color | $primary-color-2 | -
|
||||
--l-button-primary-border-color | $primary-color | -
|
||||
--l-button-danger-color | $danger-color | -
|
||||
--l-button-danger-hover-color | $danger-color-7 | -
|
||||
--l-button-danger-light-color | $danger-color-1 | -
|
||||
--l-button-danger-light-hover-color | $danger-color-2 | -
|
||||
--l-button-danger-border-color | $danger-color | -
|
||||
--l-button-warning-color | $warning-color | -
|
||||
--l-button-warning-hover-color | $warning-color-7 | -
|
||||
--l-button-warning-light-color | $warning-color-1 | -
|
||||
--l-button-warning-light-hover-color | $warning-color-2 | -
|
||||
--l-button-warning-border-color | $warning-color | -
|
||||
--l-button-success-color | $success-color | -
|
||||
--l-button-success-hover-color | $success-color-7 | -
|
||||
--l-button-success-light-color | $success-color-1 | -
|
||||
--l-button-success-light-hover-color | $success-color-2 | -
|
||||
--l-button-success-border-color | $success-color | -
|
||||
--l-button-info-color | $blue | -
|
||||
--l-button-info-hover-color | $blue-7 | -
|
||||
--l-button-info-light-color | $blue-1 | -
|
||||
--l-button-info-light-hover-color | $blue-3 | -
|
||||
--l-button-info-border-color | $blue | -
|
||||
--l-button-mini-height | 56rpx | -
|
||||
--l-button-small-height | 64rpx | -
|
||||
--l-button-medium-height | 80rpx | -
|
||||
--l-button-large-height | 96rpx | -
|
||||
--l-button-button-padding | - | -
|
||||
--l-button-icon-size | - | -
|
||||
--l-button-font-size | - | -
|
||||
|
||||
|
||||
## 支持与赞赏
|
||||
|
||||
如果你觉得本插件解决了你的问题,可以考虑支持作者:
|
||||
| 支付宝赞助 | 微信赞助 |
|
||||
|------------|------------|
|
||||
|  |  |
|
||||
@@ -0,0 +1,274 @@
|
||||
# l-cascader 多选级联选择器使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
l-cascader 组件现已支持单选和多选两种模式,类似 Ant Design 的 Cascader 级联多选组件。多选模式下使用 lime-checkbox 组件作为选择框。
|
||||
|
||||
## 基本用法
|
||||
|
||||
### 单选模式(默认)
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view>
|
||||
<l-cascader
|
||||
:visible="showSingle"
|
||||
:options="options"
|
||||
v-model="singleValue"
|
||||
title="单选级联选择"
|
||||
@change="onSingleChange"
|
||||
@finish="onSingleFinish"
|
||||
/>
|
||||
<button @click="showSingle = true">打开单选级联选择器</button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
const showSingle = ref(false)
|
||||
const singleValue = ref('')
|
||||
|
||||
const options = [
|
||||
{
|
||||
label: '浙江省',
|
||||
value: 'zhejiang',
|
||||
children: [
|
||||
{
|
||||
label: '杭州市',
|
||||
value: 'hangzhou',
|
||||
children: [
|
||||
{ label: '西湖区', value: 'xihu' },
|
||||
{ label: '余杭区', value: 'yuhang' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '宁波市',
|
||||
value: 'ningbo',
|
||||
children: [
|
||||
{ label: '海曙区', value: 'haishu' },
|
||||
{ label: '江北区', value: 'jiangbei' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '江苏省',
|
||||
value: 'jiangsu',
|
||||
children: [
|
||||
{
|
||||
label: '南京市',
|
||||
value: 'nanjing',
|
||||
children: [
|
||||
{ label: '玄武区', value: 'xuanwu' },
|
||||
{ label: '鼓楼区', value: 'gulou' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const onSingleChange = (value: string, selectedOptions: any[]) => {
|
||||
console.log('单选值变化:', value, selectedOptions)
|
||||
}
|
||||
|
||||
const onSingleFinish = () => {
|
||||
console.log('单选完成:', singleValue.value)
|
||||
showSingle.value = false
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 多选模式
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<view>
|
||||
<l-cascader
|
||||
:visible="showMultiple"
|
||||
:options="options"
|
||||
:multiple="true"
|
||||
v-model:multipleValue="multipleValue"
|
||||
title="多选级联选择"
|
||||
:maxTagCount="5"
|
||||
:showCheckedCount="true"
|
||||
@change="onMultipleChange"
|
||||
@finish="onMultipleFinish"
|
||||
/>
|
||||
<button @click="showMultiple = true">打开多选级联选择器</button>
|
||||
<view v-if="multipleValue.length > 0">
|
||||
<text>已选择: {{ multipleValue.join(', ') }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
const showMultiple = ref(false)
|
||||
const multipleValue = ref<string[]>([])
|
||||
|
||||
const onMultipleChange = (values: string[], selectedOptions: any[]) => {
|
||||
console.log('多选值变化:', values, selectedOptions)
|
||||
}
|
||||
|
||||
const onMultipleFinish = () => {
|
||||
console.log('多选完成:', multipleValue.value)
|
||||
showMultiple.value = false
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 属性说明
|
||||
|
||||
### 多选相关属性
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| multiple | boolean | false | 是否启用多选模式 |
|
||||
| multipleValue | string[] | [] | 多选模式下的选中值数组(支持v-model) |
|
||||
| defaultMultipleValue | string[] | [] | 多选模式下的默认选中值数组 |
|
||||
| maxTagCount | number | -1 | 最大选择数量,-1表示无限制 |
|
||||
| showCheckedCount | boolean | true | 是否显示选中数量 |
|
||||
|
||||
### 其他属性
|
||||
|
||||
| 属性名 | 类型 | 默认值 | 说明 |
|
||||
|--------|------|--------|------|
|
||||
| visible | boolean | false | 控制组件显示/隐藏 |
|
||||
| options | UTSJSONObject[] | [] | 数据源 |
|
||||
| title | string | - | 主标题文本 |
|
||||
| placeholder | string | '选择选项' | 未选择时的提示文字 |
|
||||
| checkStrictly | boolean | false | 父子节点选择是否关联 |
|
||||
| closeable | boolean | true | 是否显示关闭按钮 |
|
||||
| activeColor | string | '#3283ff' | 选中状态主题色 |
|
||||
|
||||
## 事件说明
|
||||
|
||||
| 事件名 | 参数 | 说明 |
|
||||
|--------|------|------|
|
||||
| change | (value: string \| string[], selectedOptions: any[]) | 选项变化时触发,单选返回string,多选返回string[] |
|
||||
| pick | (level: number, index: number, value: string, selectedIndexes: number[]) | 选中时触发 |
|
||||
| close | - | 关闭时触发 |
|
||||
| finish | - | 点击完成时触发 |
|
||||
|
||||
## 数据格式
|
||||
|
||||
### 选项数据结构
|
||||
|
||||
```typescript
|
||||
interface CascaderOption {
|
||||
label: string; // 显示文本
|
||||
value: string; // 选项值
|
||||
children?: CascaderOption[]; // 子选项
|
||||
disabled?: boolean; // 是否禁用
|
||||
color?: string; // 自定义颜色
|
||||
}
|
||||
```
|
||||
|
||||
### 字段别名配置
|
||||
|
||||
```vue
|
||||
<l-cascader
|
||||
:options="options"
|
||||
:keys="{ label: 'name', value: 'id', children: 'subItems' }"
|
||||
/>
|
||||
```
|
||||
|
||||
## 高级用法
|
||||
|
||||
### 自定义字段别名
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<l-cascader
|
||||
:visible="show"
|
||||
:options="customOptions"
|
||||
:keys="{ label: 'name', value: 'code', children: 'subItems' }"
|
||||
v-model:multipleValue="values"
|
||||
:multiple="true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
const customOptions = [
|
||||
{
|
||||
name: '华东地区',
|
||||
code: 'east',
|
||||
subItems: [
|
||||
{
|
||||
name: '上海',
|
||||
code: 'shanghai',
|
||||
subItems: [
|
||||
{ name: '浦东新区', code: 'pudong' },
|
||||
{ name: '黄浦区', code: 'huangpu' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
</script>
|
||||
```
|
||||
|
||||
### 限制选择数量
|
||||
|
||||
```vue
|
||||
<l-cascader
|
||||
:visible="show"
|
||||
:options="options"
|
||||
:multiple="true"
|
||||
:maxTagCount="3"
|
||||
v-model:multipleValue="values"
|
||||
/>
|
||||
```
|
||||
|
||||
### 禁用某些选项
|
||||
|
||||
```vue
|
||||
<script setup lang="uts">
|
||||
const options = [
|
||||
{
|
||||
label: '浙江省',
|
||||
value: 'zhejiang',
|
||||
children: [
|
||||
{
|
||||
label: '杭州市',
|
||||
value: 'hangzhou',
|
||||
disabled: true, // 禁用此选项
|
||||
children: [
|
||||
{ label: '西湖区', value: 'xihu' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
</script>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **单选模式**:使用 `v-model` 绑定单个字符串值
|
||||
2. **多选模式**:使用 `v-model:multipleValue` 绑定字符串数组
|
||||
3. **父子关联**:`checkStrictly` 为 false 时,选择父节点会自动选择所有子节点
|
||||
4. **最大选择数**:`maxTagCount` 设置为 -1 表示无限制
|
||||
5. **样式定制**:可以通过 CSS 变量自定义主题色和样式
|
||||
|
||||
## 样式定制
|
||||
|
||||
```scss
|
||||
// 自定义主题色
|
||||
.l-cascader {
|
||||
--l-cascader-icon-color: #ff6b6b;
|
||||
--l-cascader-cell-title-color: #333;
|
||||
}
|
||||
|
||||
// 自定义checkbox样式
|
||||
.l-cascader__checkbox {
|
||||
.l-checkbox {
|
||||
--l-checkbox-icon-checked-color: #ff6b6b;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 兼容性
|
||||
|
||||
- 支持 H5、小程序、App 多端
|
||||
- 基于 uni-app-x 框架开发
|
||||
- 使用 UTS 语言编写
|
||||
- 兼容 lime-checkbox 组件
|
||||
112
qiming-mobile/uni_modules/lime-cascader/TEST_GUIDE.md
Normal file
112
qiming-mobile/uni_modules/lime-cascader/TEST_GUIDE.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# l-cascader 多选功能测试指南
|
||||
|
||||
## 测试页面
|
||||
|
||||
已创建测试页面:`/pages/test-cascader/test-cascader.uvue`
|
||||
|
||||
## 测试内容
|
||||
|
||||
### 1. 单选模式测试
|
||||
- 点击"打开单选级联选择器"按钮
|
||||
- 选择任意选项,验证单选功能
|
||||
- 检查选中值是否正确显示
|
||||
|
||||
### 2. 多选模式测试
|
||||
- 点击"打开多选级联选择器"按钮
|
||||
- 验证 checkbox 是否正常显示
|
||||
- 选择多个选项,验证多选功能
|
||||
- 检查选中值和数量是否正确显示
|
||||
|
||||
### 3. 限制选择数量测试
|
||||
- 点击"打开限制3个的多选级联选择器"按钮
|
||||
- 尝试选择超过3个选项
|
||||
- 验证是否显示限制提示
|
||||
- 检查选中数量是否正确限制
|
||||
|
||||
## 修复的问题
|
||||
|
||||
### 1. new-conversation-set.uvue 中的问题
|
||||
- **问题**:添加 `:multiple="true"` 后组件无法点击,checkbox 不显示
|
||||
- **原因**:
|
||||
- 使用了错误的 v-model 绑定(应该用 `v-model:multipleValue`)
|
||||
- 数据类型不匹配(单选用 string,多选用 string[])
|
||||
- **修复**:
|
||||
- 修改为 `v-model:multipleValue="cascaderMultipleValue"`
|
||||
- 添加 `cascaderMultipleValue` 响应式数据
|
||||
- 修改 `:multiple="isMultiple"` 动态控制模式
|
||||
|
||||
### 2. l-cascader 组件中的问题
|
||||
- **问题**:checkbox 组件属性使用错误
|
||||
- **原因**:使用了不存在的 `checked` 属性
|
||||
- **修复**:改为使用 `modelValue` 和 `@update:modelValue`
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 单选模式
|
||||
```vue
|
||||
<l-cascader
|
||||
:visible="show"
|
||||
:options="options"
|
||||
v-model="singleValue"
|
||||
title="单选级联选择"
|
||||
@change="onChange"
|
||||
/>
|
||||
```
|
||||
|
||||
### 多选模式
|
||||
```vue
|
||||
<l-cascader
|
||||
:visible="show"
|
||||
:options="options"
|
||||
:multiple="true"
|
||||
v-model:multipleValue="multipleValue"
|
||||
title="多选级联选择"
|
||||
@change="onChange"
|
||||
/>
|
||||
```
|
||||
|
||||
### 限制选择数量
|
||||
```vue
|
||||
<l-cascader
|
||||
:visible="show"
|
||||
:options="options"
|
||||
:multiple="true"
|
||||
:maxTagCount="3"
|
||||
v-model:multipleValue="limitedValue"
|
||||
title="限制选择数量"
|
||||
@change="onChange"
|
||||
/>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据绑定**:
|
||||
- 单选模式使用 `v-model` 绑定 string 类型
|
||||
- 多选模式使用 `v-model:multipleValue` 绑定 string[] 类型
|
||||
|
||||
2. **事件处理**:
|
||||
- `change` 事件在单选模式下返回 string
|
||||
- `change` 事件在多选模式下返回 string[]
|
||||
|
||||
3. **样式**:
|
||||
- 多选模式下使用 lime-checkbox 组件
|
||||
- 单选模式下保持原有的图标显示
|
||||
|
||||
4. **功能特性**:
|
||||
- 支持父子节点关联选择(checkStrictly 属性)
|
||||
- 支持限制最大选择数量(maxTagCount 属性)
|
||||
- 支持禁用某些选项(disabled 属性)
|
||||
|
||||
## 测试数据
|
||||
|
||||
测试页面使用了三级级联数据:
|
||||
- 省份(浙江省、江苏省、广东省)
|
||||
- 城市(杭州、宁波、温州等)
|
||||
- 区县(西湖区、余杭区等)
|
||||
|
||||
## 预期结果
|
||||
|
||||
1. **单选模式**:只能选择一个最终选项,显示选中路径
|
||||
2. **多选模式**:可以选择多个选项,显示所有选中值
|
||||
3. **限制模式**:最多选择指定数量的选项,超出时显示提示
|
||||
4. **交互体验**:点击流畅,checkbox 状态正确,数据绑定准确
|
||||
@@ -0,0 +1,149 @@
|
||||
|
||||
@import '~@/uni_modules/lime-style/index.scss';
|
||||
// @import './icon';
|
||||
@import '../../../lime-icon/components/l-icon/icon.scss';
|
||||
// @import '~@/uni_modules/lime-icon/components/l-icon/icon.scss';
|
||||
// @import '~@/uni_modules/lime-style/mixins/hairline.scss';
|
||||
|
||||
/* #ifdef uniVersion >= 4.75 */
|
||||
$use-css-var: true;
|
||||
/* #endif */
|
||||
$cascader: #{$prefix}-cascader;
|
||||
|
||||
$cascader-title-color: create-var(cascader-title-color, $text-color-1);
|
||||
$cascader-icon-color: create-var(cascader-icon-color, $primary-color);
|
||||
$cascader-icon-size: create-var(cascader-icon-size, 24px);
|
||||
$cascader-bg-color: create-var(cascader-bg-color, $bg-color-container);
|
||||
$cascader-border-radius: create-var(cascader-border-radius, $border-radius-lg);
|
||||
$cascader-height: create-var(cascader-height, 320px);
|
||||
$cascader-cell-height: create-var(cascader-cell-height, 50px);
|
||||
$cascader-cell-padding-x: create-var(cascader-cell-cell-padding, $spacer);
|
||||
$cascader-cell-padding-y: create-var(cascader-cell-cell-padding, $spacer-sm);
|
||||
$cascader-cell-title-color: create-var(cascader-cell-title-color, $text-color-1);
|
||||
$cascader-cell-title-font-size: create-var(cascader-cell-title-font-size, $font-size-md);
|
||||
|
||||
$cascader-disabled-color: create-var(cascader-disabled-color, $text-color-3);
|
||||
$cascader-title-height: create-var(cascader-title-height, 48px);
|
||||
$cascader-title-padding-top: create-var(cascader-title-padding-top, $spacer);
|
||||
$cascader-title-padding-bottom: create-var(cascader-title-padding-bottom, $spacer-xs);
|
||||
$cascader-title-font-size: create-var(cascader-title-font-size, 18px);
|
||||
$cascader-options-title-color: create-var(cascader-options-title-color, $text-color-3);
|
||||
$cascader-close-icon-color: create-var(cascader-close-icon-color, $text-color-2);
|
||||
|
||||
|
||||
.#{$cascader} {
|
||||
background-color: $cascader-bg-color;
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
color: $cascader-title-color;
|
||||
/* #endif */
|
||||
// border-radius: 12px 12px 0 0;
|
||||
@include border-radius($cascader-border-radius $cascader-border-radius 0 0);
|
||||
&__title {
|
||||
/* #ifndef UNI-APP-X */
|
||||
display: flex;
|
||||
// width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* #endif */
|
||||
position: relative;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: $cascader-title-color;
|
||||
// line-height: $cascader-title-height;
|
||||
// height: $cascader-title-height;
|
||||
// padding: 14px 0;
|
||||
@include padding($cascader-title-padding-top 0 $cascader-title-padding-bottom);
|
||||
font-size: $cascader-title-font-size;
|
||||
}
|
||||
&__close {
|
||||
&-btn {
|
||||
right: $cascader-cell-padding-x;
|
||||
top: 12px;
|
||||
position: absolute;
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
color: $cascader-close-icon-color !important;
|
||||
/* #endif */
|
||||
}
|
||||
&-icon {
|
||||
font-family: $prefix;
|
||||
font-size: $cascader-icon-size;
|
||||
color: $cascader-close-icon-color !important;
|
||||
}
|
||||
}
|
||||
&__content {
|
||||
// height: $cascader-content-height;
|
||||
}
|
||||
&__options {
|
||||
// flex: 1;
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
width: 100vw;
|
||||
/* #endif */
|
||||
/* #ifdef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
width: 750rpx;
|
||||
/* #endif */
|
||||
height: $cascader-height;
|
||||
&-container {
|
||||
/* #ifndef UNI-APP-X */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
transition-property: transform;
|
||||
transition-timing-function: ease;
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
&-title {
|
||||
color: $cascader-options-title-color;
|
||||
font-size: $font-size;
|
||||
line-height: 22px;
|
||||
padding-top: 16px;
|
||||
padding-left: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
&__cell {
|
||||
/* #ifndef UNI-APP-X */
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
/* #endif */
|
||||
// padding: $cascader-cell-padding;
|
||||
@include padding($cascader-cell-padding-y $cascader-cell-padding-x);
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: $cascader-cell-height;
|
||||
&--disabled {
|
||||
color: $cascader-disabled-color;
|
||||
}
|
||||
&-title {
|
||||
font-size: $cascader-cell-title-font-size;
|
||||
color: $cascader-cell-title-color;
|
||||
}
|
||||
&-icon {
|
||||
font-size: $cascader-icon-size;
|
||||
color: $cascader-icon-color;
|
||||
}
|
||||
}
|
||||
|
||||
// 多选模式下的checkbox样式
|
||||
&__checkbox {
|
||||
width: 100%;
|
||||
/* #ifndef UNI-APP-X */
|
||||
display: flex;
|
||||
/* #endif */
|
||||
align-items: center;
|
||||
// justify-content: space-between;
|
||||
|
||||
.l-checkbox {
|
||||
width: 100%;
|
||||
|
||||
.l-checkbox__label {
|
||||
flex: 1;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__footer {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,462 @@
|
||||
<template>
|
||||
<l-popup :visible="show" position="bottom" @click-overlay="onClose">
|
||||
<view class="l-cascader" :style="[styles]">
|
||||
<text class="l-cascader__title">{{title}}</text>
|
||||
<view class="l-cascader__close-btn" @click="onCloseBtn" v-if="closeable">
|
||||
<view class="l-cascader__close-icon" >
|
||||
<l-icon name="tdesign:close" size="24px"></l-icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="l-cascader__content">
|
||||
<l-tabs
|
||||
:list="steps"
|
||||
:value="stepIndex"
|
||||
:space-evenly="false" @change="onTabChange" size="large"
|
||||
:color="color"
|
||||
:visible="show"
|
||||
:activeColor="activeColor"
|
||||
:lineColor="activeColor"
|
||||
:bgColor="bgColor">
|
||||
</l-tabs>
|
||||
<template v-if="subTitles.length > 0 ">
|
||||
<text v-show="subTitles.length > stepIndex" class="l-cascader__options-title">
|
||||
{{subTitles.length > stepIndex ? subTitles[stepIndex]: ''}}
|
||||
</text>
|
||||
</template>
|
||||
|
||||
<view class="l-cascader__options-container" :style="[containerStyles]">
|
||||
<scroll-view
|
||||
class="l-cascader__options"
|
||||
v-for="(_options, index) in items"
|
||||
:key="index"
|
||||
:scroll-y="true">
|
||||
<view class="l-cascader__cell" v-for="(item, _index) in _options" :key="_index" @click="handleSelect(item[fieldKeys.value], index, true)">
|
||||
<text
|
||||
class="l-cascader__cell-title"
|
||||
:class="{'l-cascader__cell--disabled': item['disabled'] == true}"
|
||||
:style="[color != null ?'color:' + color: '']">{{item[fieldKeys.label]}}</text>
|
||||
<view class="l-cascader__cell-icon"
|
||||
v-if="selectedValue.length > index && selectedValue[index] == item[fieldKeys.value]">
|
||||
<l-icon
|
||||
:size="iconSize"
|
||||
:color="activeColor"
|
||||
name="check">
|
||||
</l-icon>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="l-cascader__footer" v-if="innerConfirmBtn">
|
||||
<l-button
|
||||
@click="handleConfirm"
|
||||
:color="innerConfirmBtn.color || props.color"
|
||||
:shape="innerConfirmBtn.shape || 'round'"
|
||||
:type="innerConfirmBtn.type || 'primary'">
|
||||
{{
|
||||
innerConfirmBtn.content
|
||||
}}
|
||||
</l-button>
|
||||
</view>
|
||||
</view>
|
||||
</l-popup>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Cascader 级联选择器组件
|
||||
* @description 支持多级联动的选择器,适用于地区选择、分类选择等场景
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-cascader
|
||||
*
|
||||
* @property {boolean} visible 控制组件显示/隐藏(必填)
|
||||
* @property {boolean} checkStrictly 父子节点选择是否关联(默认false联动选择)
|
||||
* @property {object[]} options 数据源(必填,符合CascaderOption结构)
|
||||
* @property {string} title 主标题文本
|
||||
* @property {object} keys 字段别名配置(例:{label:'name',value:'id'})
|
||||
* @property {string} value 当前选中值(支持v-model)
|
||||
* @property {string} defaultValue 默认选中值
|
||||
* @property {string} placeholder 未选择时的提示文字
|
||||
* @property {string[]} subTitles 各级副标题(如:['省份','城市','区县'])
|
||||
* @property {boolean} closeable 是否显示关闭按钮
|
||||
* @property {boolean} uniCloud 是否使用uniCloud数据源
|
||||
* @property {boolean} swipeable 是否支持手势滑动切换层级
|
||||
* @property {string} iconSize 图标尺寸(支持CSS单位)
|
||||
* @property {string} activeColor 选中状态主题色
|
||||
* @property {string} fontSize 文字字号(支持CSS单位)
|
||||
* @property {string} color 文字颜色
|
||||
* @property {string} bgColor 背景颜色
|
||||
* @event {Function} change 选项变化时触发(返回当前选中路径)
|
||||
* @event {Function} pick 选中时触发
|
||||
* @event {Function} close 关闭时触发
|
||||
* @event {Function} finish 点击完成时触发
|
||||
*/
|
||||
import { defineComponent, ref, computed, watch, reactive, onMounted ,onUnmounted, toRaw, watchEffect } from '@/uni_modules/lime-shared/vue';
|
||||
import cascaderProps from './props';
|
||||
import { CascaderOption, KeysType, ChildrenInfoType } from './type';
|
||||
import { getIndexesByValue, parseOptions, parseKeys, splitEveryTwo, getUniCloudArea, pickUniCloudArea, getIndexByValue, updateChildren } from './utils';
|
||||
|
||||
|
||||
export default defineComponent({
|
||||
name: 'l-cascader',
|
||||
emits: ['pick', 'change', 'close', 'finish', 'update:modelValue', 'input', 'update:visible'],
|
||||
props: cascaderProps,
|
||||
setup(props, {emit}) {
|
||||
const childrenInfo: ChildrenInfoType = {
|
||||
value: '',
|
||||
level: 0,
|
||||
};
|
||||
const fieldKeys = computed(():KeysType => parseKeys(props.keys, props.uniCloud))
|
||||
const cascaderValue = computed({
|
||||
set(value: string){
|
||||
// modelValue.value = value
|
||||
emit('update:modelValue', value)
|
||||
// #ifdef VUE2
|
||||
emit('input', value)
|
||||
// #endif
|
||||
},
|
||||
get():string {
|
||||
return props.value ?? props.modelValue
|
||||
}
|
||||
})
|
||||
const show = computed({
|
||||
set(value: boolean){
|
||||
emit('update:visible', value)
|
||||
},
|
||||
get():boolean {
|
||||
return props.visible
|
||||
}
|
||||
} as WritableComputedOptions<string>)
|
||||
|
||||
const cache = new Map<string, Record<string,any>[]>();
|
||||
const stepIndex = ref(0)
|
||||
const selectedIndexes = reactive<number[]>([]);
|
||||
const selectedValue = reactive<string[]>([]);
|
||||
const uniCloudColumns = ref<UTSJSONObject[]>([]);
|
||||
const realColumns = computed(():UTSJSONObject[] =>{
|
||||
if(props.uniCloud) {
|
||||
return uniCloudColumns.value
|
||||
}
|
||||
return props.options
|
||||
})
|
||||
const items = reactive<CascaderOption[][]>([realColumns.value]);
|
||||
const steps = reactive<CascaderOption[]>([{ label: props.placeholder }])
|
||||
const innerConfirmBtn = computed(():UTSJSONObject|null=> {
|
||||
if(props.confirmBtn == null) return null
|
||||
if(typeof props.confirmBtn == 'string') {
|
||||
return {
|
||||
content: props.confirmBtn as string
|
||||
}
|
||||
}
|
||||
return props.confirmBtn as UTSJSONObject
|
||||
})
|
||||
const styles = computed(() =>{
|
||||
const style:Record<string, any> = {}
|
||||
if(props.bgColor) {
|
||||
style['background'] = props.bgColor!
|
||||
}
|
||||
return style
|
||||
})
|
||||
const containerStyles = computed(()=>{
|
||||
const style:Record<string, any> = {}
|
||||
style['width'] = (items.length + 1) + '00%';
|
||||
style['transform'] = `translateX(${-stepIndex.value}00vw)`;
|
||||
return style
|
||||
})
|
||||
|
||||
const onTabChange = (index : number) => {
|
||||
stepIndex.value = index
|
||||
}
|
||||
// 监听选中索引变化
|
||||
const watchSelectedIndexes = () => {
|
||||
if (realColumns.value.length > 0) {
|
||||
items.splice(0, items.length, ...[parseOptions(realColumns.value, fieldKeys.value)]);
|
||||
let current = realColumns.value;
|
||||
for (let i = 0, size = selectedIndexes.length; i < size; i += 1) {
|
||||
const index = selectedIndexes[i];
|
||||
const next = current[index]
|
||||
const value = next?.[fieldKeys.value.value]
|
||||
const label = next?.[fieldKeys.value.label]
|
||||
const children = next?.[fieldKeys.value.children]
|
||||
if(value) {
|
||||
selectedValue.push(value);
|
||||
}
|
||||
if(label && steps[i]?.['label'] != label) {
|
||||
steps.push({label});
|
||||
}
|
||||
|
||||
if(children) {
|
||||
current = children
|
||||
items.push(parseOptions(children, fieldKeys.value));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (steps.length < items.length) {
|
||||
steps.push({ label: props.placeholder });
|
||||
}
|
||||
// stepIndex.value = items.length - 1;
|
||||
stepIndex.value = Math.max(Math.min(selectedIndexes.length - 1, items.length - 1), 0);
|
||||
}
|
||||
|
||||
let timer = 0
|
||||
const initWithValue = () => {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(()=>{
|
||||
if (cascaderValue.value) {
|
||||
steps.pop()
|
||||
const path = getIndexesByValue(realColumns.value, cascaderValue.value, fieldKeys.value)
|
||||
path?.forEach((e : number, index) => {
|
||||
// @ts-ignore
|
||||
if(selectedIndexes.length > index) {
|
||||
selectedIndexes[index] = e
|
||||
} else {
|
||||
// @ts-ignore
|
||||
selectedIndexes.push(e);
|
||||
}
|
||||
});
|
||||
watchSelectedIndexes();
|
||||
} else {
|
||||
selectedIndexes.length = 0;
|
||||
selectedValue.length = 0;
|
||||
steps.length = 1;
|
||||
steps[0] = { label: props.placeholder }
|
||||
items.length = 1;
|
||||
items[0] = parseOptions(realColumns.value, fieldKeys.value)
|
||||
stepIndex.value = 0;
|
||||
}
|
||||
},50)
|
||||
}
|
||||
// 设置级联选择器值
|
||||
const setCascaderValue = (value: string, selectedOptions: CascaderOption[]) => {
|
||||
cascaderValue.value = value;
|
||||
emit('change', value, [...selectedOptions])
|
||||
}
|
||||
const setSteps = (index: number, label: string) => {
|
||||
steps[index] = {label}
|
||||
}
|
||||
// 取消选择
|
||||
const cancelSelect = (value: string, level: number, index: number, item:CascaderOption) => {
|
||||
selectedIndexes[level] = index;
|
||||
selectedIndexes.length = level;
|
||||
selectedValue.length = level;
|
||||
// steps[level] = String(props.placeholder);
|
||||
// steps[level + 1] = props.placeholder;
|
||||
setSteps(level, props.placeholder)
|
||||
setSteps(level + 1, props.placeholder)
|
||||
steps.length = level + 1;
|
||||
|
||||
const children = item[fieldKeys.value.children] as CascaderOption[]|null
|
||||
if (children != null && children.length > 0) {
|
||||
items[level + 1] = item[fieldKeys.value.children];
|
||||
} else if (children != null && children.length == 0) {
|
||||
childrenInfo.value = value;
|
||||
childrenInfo.level = level;
|
||||
}
|
||||
}
|
||||
// 选择
|
||||
const chooseSelect = (value: string, level: number, index: number, item:CascaderOption) => {
|
||||
selectedIndexes[level] = index;
|
||||
selectedIndexes.length = level + 1;
|
||||
selectedValue[level] = String(value);
|
||||
selectedValue.length = level + 1;
|
||||
setSteps(level, `${item[fieldKeys.value.label] ?? props.placeholder}`)
|
||||
const children = item[fieldKeys.value.children] as CascaderOption[]|null
|
||||
if (children != null && children.length > 0) {
|
||||
// setItems(level + 1, children)
|
||||
items[level + 1] = children;
|
||||
items.length = level + 2;
|
||||
stepIndex.value += 1;
|
||||
setSteps(level + 1, props.placeholder)
|
||||
steps.length = level + 2;
|
||||
} else if (children != null && children.length == 0) {
|
||||
childrenInfo.value = value;
|
||||
childrenInfo.level = level;
|
||||
} else {
|
||||
// 如果存在确确按钮,则需要点按钮关闭
|
||||
if(props.confirmBtn != null) return
|
||||
items.length = level + 1
|
||||
steps.length = level + 1;
|
||||
setCascaderValue(
|
||||
`${item[fieldKeys.value.value] ?? ''}`,
|
||||
items.map((item, index):CascaderOption => toRaw(item[selectedIndexes[index]]))
|
||||
)
|
||||
emit('finish');
|
||||
show.value = false
|
||||
}
|
||||
}
|
||||
const handleSelect = (value: string, level: number, shouldEmit: boolean) => {
|
||||
const _value = value
|
||||
if(items.length <= level) return
|
||||
const index = items[level].findIndex((item: CascaderOption):boolean => item[fieldKeys.value.value] == _value);
|
||||
let item = selectedIndexes.slice(0, level).reduce((acc:CascaderOption[], item:number, index:number):CascaderOption[] => {
|
||||
if (index == 0) {
|
||||
return [acc[item]] as CascaderOption[];
|
||||
}
|
||||
const children = acc[0][fieldKeys.value.children] as CascaderOption[]|null
|
||||
return [children?.[item] ?? {}] as CascaderOption[];
|
||||
}, realColumns.value);
|
||||
|
||||
let cursor :CascaderOption|null
|
||||
if (level == 0) {
|
||||
cursor = item[index];
|
||||
} else {
|
||||
const children = item[0][fieldKeys.value.children] as CascaderOption[]|null
|
||||
cursor = children?.[index];
|
||||
}
|
||||
const disabled = cursor?.['disabled'] == true
|
||||
if (disabled || cursor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 反选条件:1:有确认按钮并且没有下级 2:允许各自取消(checkStrictly)
|
||||
const hasChildren = Boolean(cursor[fieldKeys.value.children]);
|
||||
const isSelected = selectedValue.includes(_value);
|
||||
const canCancel =
|
||||
(props.confirmBtn && !hasChildren) ||
|
||||
(props.checkStrictly);
|
||||
|
||||
if (canCancel && isSelected) {
|
||||
// if (props.checkStrictly && selectedValue.includes(_value)) {
|
||||
cancelSelect(_value, level, index, cursor);
|
||||
} else {
|
||||
chooseSelect(_value, level, index, cursor);
|
||||
}
|
||||
if(shouldEmit){
|
||||
const code = cursor[fieldKeys.value.value] as string
|
||||
emit('pick', level, index, code, toRaw([...selectedIndexes]))
|
||||
if(!props.uniCloud) return
|
||||
pickUniCloudArea(uniCloudColumns.value, level, code, toRaw([...selectedIndexes]), cache)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 更新级联选择器值
|
||||
const updateCascaderValue = () => {
|
||||
setCascaderValue(
|
||||
selectedValue[selectedValue.length - 1],
|
||||
items
|
||||
.filter((item, index):boolean => selectedIndexes.length > index)
|
||||
.map((item, index):CascaderOption => toRaw(item[selectedIndexes[index]]))
|
||||
);
|
||||
};
|
||||
const onClose = () => {
|
||||
emit('close');
|
||||
show.value = false
|
||||
}
|
||||
const onCloseBtn = () => {
|
||||
if (props.checkStrictly) {
|
||||
updateCascaderValue();
|
||||
onClose()
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = () => {
|
||||
// 1. 如果没有选中任何选项,直接关闭
|
||||
if (selectedValue.length === 0) {
|
||||
show.value = false;
|
||||
return;
|
||||
}
|
||||
// 2. 获取当前选中的完整路径(各级选项)
|
||||
const selectedOptions = items
|
||||
.filter((_, index) => selectedIndexes.length > index)
|
||||
.map((item, index) => toRaw(item[selectedIndexes[index]]));
|
||||
|
||||
|
||||
// 3. 更新级联选择器的值(如果是联动模式)
|
||||
if (!props.checkStrictly) {
|
||||
setCascaderValue(
|
||||
selectedValue[selectedValue.length - 1],
|
||||
selectedOptions
|
||||
);
|
||||
}
|
||||
|
||||
// 4. 触发 finish 事件,返回选中值和选项路径
|
||||
emit('finish');
|
||||
|
||||
// 5. 关闭弹窗
|
||||
show.value = false;
|
||||
}
|
||||
|
||||
const optionsWatch = watch(():CascaderOption[] => realColumns.value, (_:CascaderOption[]) => {
|
||||
watchSelectedIndexes()
|
||||
if(selectedIndexes.length == 0) {
|
||||
initWithValue()
|
||||
}
|
||||
if (show.value) {
|
||||
handleSelect(childrenInfo.value, childrenInfo.level, false);
|
||||
}
|
||||
},{ deep: true});
|
||||
|
||||
const placeholderWatch = watch(():string=> props.placeholder, (newValue: string, oldValue: string) => {
|
||||
const index = steps.indexOf({label : oldValue} as CascaderOption);
|
||||
if (index != -1) {
|
||||
setSteps(index, newValue)
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if(props.uniCloud) {
|
||||
const update = async () => {
|
||||
const provinces = await getUniCloudArea({type: 0}, cache)
|
||||
uniCloudColumns.value = provinces
|
||||
if(cascaderValue.value != '' && /^\d{6}$/.test(cascaderValue.value)) {
|
||||
const [province, citie, countie] = splitEveryTwo(cascaderValue.value );
|
||||
const provinceCode = province.padEnd(6, '0')
|
||||
const provinceIndex = getIndexByValue(uniCloudColumns.value, provinceCode)
|
||||
const cities = await getUniCloudArea({type: 1, parent_code: provinceCode}, cache)
|
||||
const citieCode = (province + citie).padEnd(6, '0')
|
||||
const citiesIndex = getIndexByValue(uniCloudColumns.value, citieCode)
|
||||
const counties = await getUniCloudArea({type: 2, parent_code: citieCode}, cache)
|
||||
|
||||
updateChildren(uniCloudColumns.value, cities, [provinceIndex])
|
||||
updateChildren(uniCloudColumns.value[provinceIndex]['children'] as UTSJSONObject[], counties, [citiesIndex])
|
||||
}
|
||||
initWithValue()
|
||||
}
|
||||
update()
|
||||
return
|
||||
}
|
||||
initWithValue()
|
||||
})
|
||||
|
||||
watchEffect(()=>{
|
||||
const value = cascaderValue.value
|
||||
if(realColumns.value.length > 0 && selectedIndexes.length == 0) {
|
||||
initWithValue()
|
||||
}
|
||||
})
|
||||
// 监听 cascaderValue 变化,当值被清空时重置状态
|
||||
const cascaderValueWatch = watch(():string => cascaderValue.value, (newValue: string, oldValue: string) => {
|
||||
if (newValue == '' && oldValue != '') {
|
||||
initWithValue()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(()=>{
|
||||
optionsWatch()
|
||||
placeholderWatch()
|
||||
cascaderValueWatch()
|
||||
})
|
||||
return {
|
||||
show,
|
||||
items,
|
||||
steps,
|
||||
stepIndex,
|
||||
selectedValue,
|
||||
styles,
|
||||
containerStyles,
|
||||
fieldKeys,
|
||||
handleSelect,
|
||||
onCloseBtn,
|
||||
onClose,
|
||||
onTabChange,
|
||||
handleConfirm,
|
||||
innerConfirmBtn
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
@@ -0,0 +1,102 @@
|
||||
export default {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* 父子节点选中状态不再关联,可各自选中或取消
|
||||
*/
|
||||
checkStrictly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
uniCloud: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [{ }]
|
||||
},
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
keys: {
|
||||
type: Object,
|
||||
default: () => { }
|
||||
},
|
||||
/**
|
||||
* 选项值
|
||||
*/
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* 选项值
|
||||
*/
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* 选项值,非受控属性
|
||||
*/
|
||||
defaultValue: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* 未选中时的提示文案
|
||||
*/
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '选择选项'
|
||||
},
|
||||
/**
|
||||
* 每级展示的次标题
|
||||
*/
|
||||
subTitles: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 关闭按钮
|
||||
*/
|
||||
closeable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
swipeable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
fontSize: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
bgColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconSize: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
activeColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
confirmBtn: {
|
||||
type: [String, Object],
|
||||
default: null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// @ts-nocheck
|
||||
// #ifndef UNI-APP-X
|
||||
type UTSJSONObject = Record<string, any>
|
||||
// #endif
|
||||
|
||||
export type ChildrenInfoType = {
|
||||
value: string;
|
||||
level: number;
|
||||
}
|
||||
export type KeysType = {
|
||||
label : string,
|
||||
value : string,
|
||||
children : string
|
||||
}
|
||||
export type CascaderOption = {
|
||||
children ?: Array<CascaderOption>;
|
||||
/** option label content */
|
||||
label ?: string;
|
||||
/** option search text */
|
||||
text ?: string;
|
||||
/** option value */
|
||||
value ?: string;
|
||||
/** option node content */
|
||||
content ?: string;
|
||||
color?: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
export interface CascaderProps {
|
||||
visible: boolean;
|
||||
/**
|
||||
* 父子节点选中状态不再关联,可各自选中或取消
|
||||
*/
|
||||
checkStrictly: boolean;
|
||||
/**
|
||||
* 是否支持多选
|
||||
*/
|
||||
multiple?: boolean;
|
||||
options : UTSJSONObject[];
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title ?: string;
|
||||
keys?: UTSJSONObject;
|
||||
/**
|
||||
* 选项值
|
||||
*/
|
||||
value ?: string;
|
||||
/**
|
||||
* 选项值,非受控属性
|
||||
*/
|
||||
defaultValue ?: string;
|
||||
/**
|
||||
* 未选中时的提示文案
|
||||
*/
|
||||
placeholder : string;
|
||||
/**
|
||||
* 每级展示的次标题
|
||||
*/
|
||||
subTitles : string[];
|
||||
/**
|
||||
* 关闭按钮
|
||||
*/
|
||||
closeable: boolean;
|
||||
uniCloud: boolean;
|
||||
swipeable ?: boolean;
|
||||
|
||||
|
||||
fontSize?: string;
|
||||
color?: string;
|
||||
bgColor?: string;
|
||||
// #ifdef APP
|
||||
iconSize: string;
|
||||
activeColor: string;
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
iconSize?: string;
|
||||
activeColor?: string;
|
||||
// #endif
|
||||
|
||||
|
||||
/**
|
||||
* 确认按钮。值为 null 则不显示确认按钮。值类型为字符串,则表示自定义按钮文本,值类型为 Object 则表示透传 Button 组件属性
|
||||
* @default ''
|
||||
*/
|
||||
confirmBtn ?: string | UTSJSONObject // string | ButtonProps | null
|
||||
/**
|
||||
* 确认按钮处于禁用状态时的文字
|
||||
*/
|
||||
confirmDisabledText ?: string;
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
// @ts-nocheck
|
||||
import type { CascaderOption, KeysType } from './type';
|
||||
// #ifndef UNI-APP-X
|
||||
type UTSJSONObject = Record<string, any>
|
||||
// #endif
|
||||
// #ifdef UNI-APP-X
|
||||
const Object = UTSJSONObject
|
||||
// #endif
|
||||
import { t } from '@/utils/i18n'
|
||||
|
||||
export function parseKeys(keys : UTSJSONObject | null, uniCloud:boolean) : KeysType {
|
||||
const _labelKey = uniCloud ? 'name' : `${keys?.['label'] ?? 'label'}`
|
||||
const _valueKey = uniCloud ? 'code' : `${keys?.['value'] ?? 'value'}`
|
||||
const _childrenKey = `${keys?.['children'] ?? 'children'}`
|
||||
return {
|
||||
label: _labelKey,
|
||||
value: _valueKey,
|
||||
children: _childrenKey,
|
||||
} as KeysType
|
||||
}
|
||||
|
||||
export function parseOptions(options : UTSJSONObject[], keys : KeysType) : UTSJSONObject[] {
|
||||
return options.map((item) : UTSJSONObject => {
|
||||
// #ifndef APP-ANDROID
|
||||
const obj = {
|
||||
[keys.label]: item[keys.label],
|
||||
[keys.value]: item[keys.value],
|
||||
[keys.children]: item[keys.children],
|
||||
}
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
const obj = {}
|
||||
item.toMap().forEach((v, key) => {
|
||||
if (key == keys.label || key == keys.value || key == keys.children) {
|
||||
obj[key] = v
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function getIndexesByValue(options : UTSJSONObject[], value : string | null, keys : KeysType) : number[] | null {
|
||||
if (value == null) return null
|
||||
for (let i = 0, size = options.length; i < size; i += 1) {
|
||||
const opt = options[i];
|
||||
if (opt[keys.value] == value) {
|
||||
return [i];
|
||||
}
|
||||
const children = opt[keys.children] as UTSJSONObject[] | null
|
||||
if (children != null) {
|
||||
const res = getIndexesByValue(children, value, keys)
|
||||
if (res != null) {
|
||||
return [i, ...res]
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 在数组的指定位置插入或更新值。
|
||||
* 如果指定的索引小于数组的长度,则更新该位置的值。
|
||||
* 如果指定的索引大于或等于数组的长度,则将值添加到数组的末尾。
|
||||
*
|
||||
* @param {number[]} arr - 要操作的数字数组。
|
||||
* @param {number} index - 要插入或更新值的索引位置。
|
||||
* @param {number} value - 要插入或更新的值。
|
||||
*/
|
||||
export function pushAt<T>(arr : T[], index : number, value : T) {
|
||||
// #ifdef APP-ANDROID
|
||||
if (index < arr.length) {
|
||||
arr[index] = value;
|
||||
} else {
|
||||
arr.push(value);
|
||||
}
|
||||
// #endif
|
||||
// #ifndef APP-ANDROID
|
||||
arr[index] = value;
|
||||
// #endif
|
||||
};
|
||||
|
||||
/**
|
||||
* 将给定的字符串按照每两个字符进行分割,返回分割后的字符串数组。
|
||||
*
|
||||
* @param {string} str - 需要被分割的原始字符串。
|
||||
* @returns {string[]} 返回一个数组,其中每个元素都是原始字符串中连续的两个字符组成的子串。
|
||||
*/
|
||||
export function splitEveryTwo(str : string) : string[] {
|
||||
const result : string[] = [];
|
||||
for (let i = 0; i < str.length; i += 2) {
|
||||
result.push(str.substring(i, i + 2));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
export function getIndexByValue(options : UTSJSONObject[], value : string) : number {
|
||||
return Math.max(options.findIndex(item => item['code'] == value), 0)
|
||||
}
|
||||
|
||||
export function updateChildren(parent : UTSJSONObject[], child : UTSJSONObject[], selectedIndexes : number[]) {
|
||||
if (selectedIndexes.length == 0 && parent.length == 0) {
|
||||
parent.concat(child)
|
||||
// parent = child
|
||||
} else if (selectedIndexes.length > 1) {
|
||||
const i = selectedIndexes.shift()!;
|
||||
updateChildren(parent[i]['children'] as UTSJSONObject[], child, selectedIndexes)
|
||||
} else if (selectedIndexes.length == 1) {
|
||||
const i = selectedIndexes.shift()!;
|
||||
// #ifdef APP-ANDROID || APP-IOS
|
||||
parent[i].set('children', child)
|
||||
// #endif
|
||||
// #ifndef APP-ANDROID || APP-IOS
|
||||
parent[i]['children'] = child
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
export function getUniCloudArea(where : UTSJSONObject, cache : Map<string, UTSJSONObject[]>) : Promise<UTSJSONObject[]> {
|
||||
return new Promise((resolve) => {
|
||||
const db = uniCloud.databaseForJQL()
|
||||
const collection = db.collection('opendb-city-china')
|
||||
const type = (where['type'] ?? 4) as number;
|
||||
const code = where['parent_code'] as string|null
|
||||
if (code != null && cache.has(code)) {
|
||||
resolve(cache.get(code)!)
|
||||
return
|
||||
}
|
||||
uni.showLoading({
|
||||
title: t('Mobile.Page.loading')
|
||||
})
|
||||
collection.where(where).get().then(res => {
|
||||
uni.hideLoading()
|
||||
// 省市 就加上children 县就直接返回
|
||||
const cursor = type > 1 ? res.data : res.data.map((item) : UTSJSONObject => {
|
||||
return Object.assign(item, { children: [] as UTSJSONObject[] })
|
||||
})
|
||||
if (code != null) {
|
||||
cache.set(code, cursor)
|
||||
}
|
||||
resolve(cursor)
|
||||
}).catch(err => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
icon: 'error',
|
||||
title: t('Mobile.Common.networkRetry')
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
export function pickUniCloudArea(columns : UTSJSONObject[], level : number, value : string, selectedIndexes : number[], cache:Map<string, UTSJSONObject[]>) {
|
||||
const first = selectedIndexes[0]
|
||||
// 获取当前项
|
||||
const item = selectedIndexes.reduce((p : UTSJSONObject | null, c : number, i : number) : UTSJSONObject | null => {
|
||||
// #ifdef APP-ANDROID || APP-IOS
|
||||
return i == 0 ? p : p?.getArray<UTSJSONObject>('children')?.[c]
|
||||
// #endif
|
||||
// #ifndef APP-ANDROID || APP-IOS
|
||||
return i == 0 ? p : p?.['children']?.[c]
|
||||
// #endif
|
||||
}, columns[first])
|
||||
|
||||
const code = item?.[`code`] as string | null
|
||||
const children = item?.[`children`] as UTSJSONObject[] | null
|
||||
|
||||
if (item != null && code == value && children != null && children.length == 0) {
|
||||
getUniCloudArea({ type: level + 1, parent_code: code }, cache).then(res => {
|
||||
updateChildren(columns, res, selectedIndexes)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
export const areaList = [
|
||||
{
|
||||
label: '北京市',
|
||||
value: '110000',
|
||||
children: [
|
||||
{
|
||||
value: '110100',
|
||||
label: '北京市',
|
||||
children: [
|
||||
{ value: '110101', label: '东城区' },
|
||||
{ value: '110102', label: '西城区' },
|
||||
{ value: '110105', label: '朝阳区' },
|
||||
{ value: '110106', label: '丰台区' },
|
||||
{ value: '110107', label: '石景山区' },
|
||||
{ value: '110108', label: '海淀区' },
|
||||
{ value: '110109', label: '门头沟区' },
|
||||
{ value: '110111', label: '房山区' },
|
||||
{ value: '110112', label: '通州区' },
|
||||
{ value: '110113', label: '顺义区' },
|
||||
{ value: '110114', label: '昌平区' },
|
||||
{ value: '110115', label: '大兴区' },
|
||||
{ value: '110116', label: '怀柔区' },
|
||||
{ value: '110117', label: '平谷区' },
|
||||
{ value: '110118', label: '密云区' },
|
||||
{ value: '110119', label: '延庆区' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '天津市',
|
||||
value: '120000',
|
||||
children: [
|
||||
{
|
||||
value: '120100',
|
||||
label: '天津市',
|
||||
children: [
|
||||
{ value: '120101', label: '和平区' },
|
||||
{ value: '120102', label: '河东区' },
|
||||
{ value: '120103', label: '河西区' },
|
||||
{ value: '120104', label: '南开区' },
|
||||
{ value: '120105', label: '河北区' },
|
||||
{ value: '120106', label: '红桥区' },
|
||||
{ value: '120110', label: '东丽区' },
|
||||
{ value: '120111', label: '西青区' },
|
||||
{ value: '120112', label: '津南区' },
|
||||
{ value: '120113', label: '北辰区' },
|
||||
{ value: '120114', label: '武清区' },
|
||||
{ value: '120115', label: '宝坻区' },
|
||||
{ value: '120116', label: '滨海新区' },
|
||||
{ value: '120117', label: '宁河区' },
|
||||
{ value: '120118', label: '静海区' },
|
||||
{ value: '120119', label: '蓟州区' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
export const areaList2 = [
|
||||
{
|
||||
name: '北京市',
|
||||
code: '110000',
|
||||
items: [
|
||||
{
|
||||
code: '110100',
|
||||
name: '北京市',
|
||||
items: [
|
||||
{ code: '110101', name: '东城区' },
|
||||
{ code: '110102', name: '西城区' },
|
||||
{ code: '110105', name: '朝阳区' },
|
||||
{ code: '110106', name: '丰台区' },
|
||||
{ code: '110107', name: '石景山区' },
|
||||
{ code: '110108', name: '海淀区' },
|
||||
{ code: '110109', name: '门头沟区' },
|
||||
{ code: '110111', name: '房山区' },
|
||||
{ code: '110112', name: '通州区' },
|
||||
{ code: '110113', name: '顺义区' },
|
||||
{ code: '110114', name: '昌平区' },
|
||||
{ code: '110115', name: '大兴区' },
|
||||
{ code: '110116', name: '怀柔区' },
|
||||
{ code: '110117', name: '平谷区' },
|
||||
{ code: '110118', name: '密云区' },
|
||||
{ code: '110119', name: '延庆区' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '天津市',
|
||||
code: '120000',
|
||||
items: [
|
||||
{
|
||||
code: '120100',
|
||||
name: '天津市',
|
||||
items: [
|
||||
{ code: '120101', name: '和平区' },
|
||||
{ code: '120102', name: '河东区' },
|
||||
{ code: '120103', name: '河西区' },
|
||||
{ code: '120104', name: '南开区' },
|
||||
{ code: '120105', name: '河北区' },
|
||||
{ code: '120106', name: '红桥区' },
|
||||
{ code: '120110', name: '东丽区' },
|
||||
{ code: '120111', name: '西青区' },
|
||||
{ code: '120112', name: '津南区' },
|
||||
{ code: '120113', name: '北辰区' },
|
||||
{ code: '120114', name: '武清区' },
|
||||
{ code: '120115', name: '宝坻区' },
|
||||
{ code: '120116', name: '滨海新区' },
|
||||
{ code: '120117', name: '宁河区' },
|
||||
{ code: '120118', name: '静海区' },
|
||||
{ code: '120119', name: '蓟州区' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
const makeOption = (
|
||||
label : string,
|
||||
value : string,
|
||||
children ?: UTSJSONObject[],
|
||||
) : UTSJSONObject => ({
|
||||
label,
|
||||
value,
|
||||
children,
|
||||
});
|
||||
|
||||
|
||||
|
||||
export function useCascaderAreaData() : Promise<UTSJSONObject[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef APP
|
||||
const manager = uni.getFileSystemManager();
|
||||
manager.readFile({
|
||||
filePath: 'static/city/city-china.json',
|
||||
encoding: 'utf-8',
|
||||
success: (res) => {
|
||||
|
||||
const areaList = JSON.parse<UTSJSONObject>(res.data as string)
|
||||
if(areaList == null) {
|
||||
reject('加载失败')
|
||||
}
|
||||
|
||||
|
||||
const city = areaList!['city_list'] as UTSJSONObject
|
||||
const county = areaList!['county_list'] as UTSJSONObject
|
||||
const province = areaList!['province_list'] as UTSJSONObject
|
||||
const provinceMap = new Map<string, UTSJSONObject>();
|
||||
UTSJSONObject.keys(province).forEach((code) => {
|
||||
provinceMap.set(code.slice(0, 2), makeOption(`${province[code]}`, code, []));
|
||||
});
|
||||
|
||||
const cityMap = new Map<string, UTSJSONObject>();
|
||||
|
||||
UTSJSONObject.keys(city).forEach((code) => {
|
||||
const option = makeOption(`${city[code]}`, code, []);
|
||||
cityMap.set(code.slice(0, 4), option);
|
||||
|
||||
const _province = provinceMap.get(code.slice(0, 2));
|
||||
if (_province != null) {
|
||||
(_province['children'] as UTSJSONObject[]).push(option)
|
||||
}
|
||||
});
|
||||
|
||||
UTSJSONObject.keys(county).forEach((code) => {
|
||||
const _city = cityMap.get(code.slice(0, 4));
|
||||
if (_city != null) {
|
||||
(_city['children'] as UTSJSONObject[]).push(makeOption(`${county[code]}`, code, null));
|
||||
}
|
||||
});
|
||||
|
||||
// #ifndef APP-ANDROID || APP-IOS || APP-HARMONY
|
||||
resolve(Array.from(provinceMap.values()))
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID || APP-IOS || APP-HARMONY
|
||||
const obj : UTSJSONObject[] = []
|
||||
provinceMap.forEach((value, code) => {
|
||||
obj.push(value)
|
||||
})
|
||||
resolve(obj)
|
||||
// #endif
|
||||
}
|
||||
} as ReadFileOptions);
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
112
qiming-mobile/uni_modules/lime-cascader/package.json
Normal file
112
qiming-mobile/uni_modules/lime-cascader/package.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"id": "lime-cascader",
|
||||
"displayName": "lime-cascader 级联选择",
|
||||
"version": "0.1.3",
|
||||
"description": "lime-cascader 级联选择 用于多层级数据选择,省市区等选择,主要为树形结构,可展示更多的数据。兼容uniapp/uniappx",
|
||||
"keywords": [
|
||||
"lime-cascader",
|
||||
"cascader",
|
||||
"省市区",
|
||||
"级联选择"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.28",
|
||||
"uni-app": "^4.75",
|
||||
"uni-app-x": "^4.75"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "√",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"lime-popup",
|
||||
"lime-style",
|
||||
"lime-icon",
|
||||
"lime-shared",
|
||||
"lime-tabs"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "√"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "√",
|
||||
"vue3": "√"
|
||||
},
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"vue": "√",
|
||||
"nvue": "-",
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√",
|
||||
"alipay": "-",
|
||||
"toutiao": "-",
|
||||
"baidu": "-",
|
||||
"kuaishou": "-",
|
||||
"jd": "-",
|
||||
"harmony": "-",
|
||||
"qq": "-",
|
||||
"lark": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "-",
|
||||
"union": "-"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
406
qiming-mobile/uni_modules/lime-cascader/readme_old.md
Normal file
406
qiming-mobile/uni_modules/lime-cascader/readme_old.md
Normal file
@@ -0,0 +1,406 @@
|
||||
# lime-cascader 级联选择
|
||||
- 级联选择 用于多层级数据选择,主要为树形结构,可展示更多的数据。兼容uniapp/uniappx
|
||||
- 插件依赖`lime-popup`,`lime-style`,`lime-shared`,`lime-tabs`,`lime-icon`,`lime-svg`,不喜勿下。
|
||||
|
||||
## 文档
|
||||
[cascader](https://limex.qcoon.cn/components/cascader.html)
|
||||
|
||||
## 安装
|
||||
插件市场导入即可,首次导入可能需要重新编译
|
||||
|
||||
**注意**
|
||||
* 🔔 本插件依赖的[lime-svg](https://ext.dcloud.net.cn/plugin?id=18519)在 uniapp x app中是原生插件,如果购买(收费为5元)则需要自定义基座,才能使用!uniapp可忽略。
|
||||
* 🔔 如果不需要[lime-svg](https://ext.dcloud.net.cn/plugin?id=18519)在lime-icon代码中注释掉即可
|
||||
|
||||
```html
|
||||
// lime-icon/components/l-icon.uvue 第4行 注释掉即可。
|
||||
<!-- <l-svg class="l-icon" :class="classes" :style="styles" :color="color" :src="iconUrl" v-else :web="web" @error="imageError" @load="imageload" @click="$emit('click')"></l-svg> -->
|
||||
```
|
||||
|
||||
|
||||
## 代码演示
|
||||
示例使用了`uts`及`vue3 setup`,`uniapp`可以把数据类型去掉即可。
|
||||
|
||||
### 基础使用
|
||||
级联选择组件通过设置`visible`弹出选择器。
|
||||
```html
|
||||
<view class="cell" @click="visible = true">
|
||||
<text>地址</text>
|
||||
<text v-show="fieldValue == null" style="color: #999;">请选择地址 ></text>
|
||||
<text v-show="fieldValue != null">{{fieldValue}}</text>
|
||||
</view>
|
||||
<l-cascader
|
||||
v-model:visible="visible"
|
||||
v-model="cascaderValue"
|
||||
:options="options"
|
||||
title="请选择所在地区"
|
||||
@change="onChange"/>
|
||||
```
|
||||
```js
|
||||
const visible = ref(false)
|
||||
// 设置默认值,如 110000
|
||||
const cascaderValue = ref('');
|
||||
const fieldValue = ref<string|null>(null);
|
||||
// 选项列表,children 代表子选项,支持多级嵌套
|
||||
const options = [
|
||||
{
|
||||
label: '北京市',
|
||||
value: '110000',
|
||||
children: [
|
||||
{
|
||||
value: '110100',
|
||||
label: '北京市',
|
||||
children: [
|
||||
{ value: '110101', label: '东城区' },
|
||||
{ value: '110102', label: '西城区' },
|
||||
{ value: '110105', label: '朝阳区' },
|
||||
{ value: '110106', label: '丰台区' },
|
||||
{ value: '110107', label: '石景山区' },
|
||||
{ value: '110108', label: '海淀区' },
|
||||
{ value: '110109', label: '门头沟区' },
|
||||
{ value: '110111', label: '房山区' },
|
||||
{ value: '110112', label: '通州区' },
|
||||
{ value: '110113', label: '顺义区' },
|
||||
{ value: '110114', label: '昌平区' },
|
||||
{ value: '110115', label: '大兴区' },
|
||||
{ value: '110116', label: '怀柔区' },
|
||||
{ value: '110117', label: '平谷区' },
|
||||
{ value: '110118', label: '密云区' },
|
||||
{ value: '110119', label: '延庆区' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '天津市',
|
||||
value: '120000',
|
||||
children: [
|
||||
{
|
||||
value: '120100',
|
||||
label: '天津市',
|
||||
children: [
|
||||
{ value: '120101', label: '和平区' },
|
||||
{ value: '120102', label: '河东区' },
|
||||
{ value: '120103', label: '河西区' },
|
||||
{ value: '120104', label: '南开区' },
|
||||
{ value: '120105', label: '河北区' },
|
||||
{ value: '120106', label: '红桥区' },
|
||||
{ value: '120110', label: '东丽区' },
|
||||
{ value: '120111', label: '西青区' },
|
||||
{ value: '120112', label: '津南区' },
|
||||
{ value: '120113', label: '北辰区' },
|
||||
{ value: '120114', label: '武清区' },
|
||||
{ value: '120115', label: '宝坻区' },
|
||||
{ value: '120116', label: '滨海新区' },
|
||||
{ value: '120117', label: '宁河区' },
|
||||
{ value: '120118', label: '静海区' },
|
||||
{ value: '120119', label: '蓟州区' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
// 全部选项选择后,会触发 change 事件
|
||||
const onChange = (value: string, options: UTSJSONObject[]) => {
|
||||
fieldValue.value = options.map((item: UTSJSONObject):any|null => (item['label'])).join('/');
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
### 带初始值
|
||||
通过设置`value`或`v-model`设置默认值,选择项的值
|
||||
|
||||
```html
|
||||
<view class="cell" @click="visible2 = true">
|
||||
<text>地址</text>
|
||||
<text v-show="fieldValue2 == null" style="color: #999;">请选择地址 ></text>
|
||||
<text v-show="fieldValue2 != null">{{fieldValue2}}</text>
|
||||
</view>
|
||||
<l-cascader
|
||||
v-model:visible="visible2"
|
||||
v-model="cascaderValue2"
|
||||
:options="options2"
|
||||
title="请选择所在地区"
|
||||
@change="onChange2"/>
|
||||
```
|
||||
```js
|
||||
const visible2 = ref(false)
|
||||
// 设置默认值
|
||||
const cascaderValue2 = ref('120119');
|
||||
const fieldValue2 = ref<string|null>(null);
|
||||
const options2 = areaList;
|
||||
const onChange2 = (value: string, options: UTSJSONObject[]) => {
|
||||
fieldValue2.value = options.map((item: UTSJSONObject):any|null => (item['label'])).join('/');
|
||||
};
|
||||
```
|
||||
|
||||
### 自定义字段名
|
||||
通过设置`keys`属性可以自定义 `options` 里的字段名称
|
||||
```html
|
||||
<view class="cell" @click="visible3 = true">
|
||||
<text>地址</text>
|
||||
<text v-show="fieldValue3 == null" style="color: #999;">请选择地址 ></text>
|
||||
<text v-show="fieldValue3 != null">{{fieldValue3}}</text>
|
||||
</view>
|
||||
<l-cascader
|
||||
v-model:visible="visible3"
|
||||
v-model="cascaderValue3"
|
||||
:options="options3"
|
||||
:keys="keys"
|
||||
title="请选择所在地区"
|
||||
@change="onChange3"/>
|
||||
```
|
||||
```js
|
||||
const visible3 = ref(false)
|
||||
const cascaderValue3 = ref('');
|
||||
const fieldValue3 = ref<string|null>(null);
|
||||
const options3 = [
|
||||
{
|
||||
name: '北京市',
|
||||
code: '110000',
|
||||
items: [
|
||||
{
|
||||
code: '110100',
|
||||
name: '北京市',
|
||||
items: [
|
||||
{ code: '110101', name: '东城区' },
|
||||
{ code: '110102', name: '西城区' },
|
||||
{ code: '110105', name: '朝阳区' },
|
||||
{ code: '110106', name: '丰台区' },
|
||||
{ code: '110107', name: '石景山区' },
|
||||
{ code: '110108', name: '海淀区' },
|
||||
{ code: '110109', name: '门头沟区' },
|
||||
{ code: '110111', name: '房山区' },
|
||||
{ code: '110112', name: '通州区' },
|
||||
{ code: '110113', name: '顺义区' },
|
||||
{ code: '110114', name: '昌平区' },
|
||||
{ code: '110115', name: '大兴区' },
|
||||
{ code: '110116', name: '怀柔区' },
|
||||
{ code: '110117', name: '平谷区' },
|
||||
{ code: '110118', name: '密云区' },
|
||||
{ code: '110119', name: '延庆区' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '天津市',
|
||||
code: '120000',
|
||||
items: [
|
||||
{
|
||||
code: '120100',
|
||||
name: '天津市',
|
||||
items: [
|
||||
{ code: '120101', name: '和平区' },
|
||||
{ code: '120102', name: '河东区' },
|
||||
{ code: '120103', name: '河西区' },
|
||||
{ code: '120104', name: '南开区' },
|
||||
{ code: '120105', name: '河北区' },
|
||||
{ code: '120106', name: '红桥区' },
|
||||
{ code: '120110', name: '东丽区' },
|
||||
{ code: '120111', name: '西青区' },
|
||||
{ code: '120112', name: '津南区' },
|
||||
{ code: '120113', name: '北辰区' },
|
||||
{ code: '120114', name: '武清区' },
|
||||
{ code: '120115', name: '宝坻区' },
|
||||
{ code: '120116', name: '滨海新区' },
|
||||
{ code: '120117', name: '宁河区' },
|
||||
{ code: '120118', name: '静海区' },
|
||||
{ code: '120119', name: '蓟州区' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const keys = {label: 'name', value: 'code', children: 'items'}
|
||||
const onChange3 = (value: string, options: UTSJSONObject[]) => {
|
||||
fieldValue3.value = options.map((item: UTSJSONObject):any|null => (item['name'])).join('/');
|
||||
};
|
||||
```
|
||||
|
||||
### 自定义选项上方内容
|
||||
通过设置`subTitles`属性每级展示的次标题
|
||||
```html
|
||||
<view class="cell" @click="visible4 = true">
|
||||
<text>地址</text>
|
||||
<text v-show="fieldValue4 == null" style="color: #999;">请选择地址 ></text>
|
||||
<text v-show="fieldValue4 != null">{{fieldValue4}}</text>
|
||||
</view>
|
||||
<l-cascader
|
||||
v-model:visible="visible4"
|
||||
v-model="cascaderValue4"
|
||||
:options="options4"
|
||||
:subTitles="subTitles"
|
||||
title="请选择所在地区"
|
||||
@change="onChange4"/>
|
||||
```
|
||||
```js
|
||||
const visible4 = ref(false)
|
||||
const cascaderValue4 = ref('');
|
||||
const fieldValue4 = ref<string|null>(null);
|
||||
const options4 = areaList;
|
||||
const subTitles = ['请选择省份', '请选择城市', '请选择区/县'];
|
||||
const onChange4 = (value: string, options: UTSJSONObject[]) => {
|
||||
fieldValue4.value = options.map((item: UTSJSONObject):any|null => (item['label'])).join('/');
|
||||
};
|
||||
```
|
||||
|
||||
### 自定义颜色
|
||||
通过设置`active-color`属性来设置选中状态的高亮颜色。
|
||||
```html
|
||||
<view class="cell" @click="visible5 = true">
|
||||
<text>地址</text>
|
||||
<text v-show="fieldValue5 == null" style="color: #999;">请选择地址 ></text>
|
||||
<text v-show="fieldValue5 != null">{{fieldValue5}}</text>
|
||||
</view>
|
||||
<l-cascader
|
||||
v-model:visible="visible5"
|
||||
v-model="cascaderValue5"
|
||||
:options="options5"
|
||||
active-color="#34c471"
|
||||
title="请选择所在地区"
|
||||
@change="onChange5"/>
|
||||
```
|
||||
|
||||
### 中国省市区数据
|
||||
`lime-shared/areaData`提供了一份中国省市区数据, 该数据来源于 `Vant`
|
||||
```js
|
||||
import { useCascaderAreaData } from '@/uni_modules/lime-shared/areaData'
|
||||
const options7 = useCascaderAreaData();
|
||||
const subTitles = ['请选择省份', '请选择城市', '请选择区/县'];
|
||||
const visible7 = ref(false)
|
||||
const cascaderValue7 = ref('');
|
||||
const fieldValue7 = ref<string|null>(null);
|
||||
const onChange7 = (value: string, options: UTSJSONObject[]) => {
|
||||
fieldValue7.value = options.map((item: UTSJSONObject):any|null => (item['label'])).join('/');
|
||||
};
|
||||
```
|
||||
```html
|
||||
<view class="cell" @click="visible7 = true">
|
||||
<text>地址</text>
|
||||
<text v-show="fieldValue7 == null" style="color: #999;">请选择地址 ></text>
|
||||
<text v-show="fieldValue7 != null">{{fieldValue7}}</text>
|
||||
</view>
|
||||
<l-cascader
|
||||
v-model:visible="visible7"
|
||||
v-model="cascaderValue7"
|
||||
:options="options7"
|
||||
:subTitles="subTitles"
|
||||
title="请选择所在地区"
|
||||
@change="onChange7"/>
|
||||
```
|
||||
|
||||
### uniCloud
|
||||
除了加载本地数据,还可以设置`uniCloud`,会加载`opendb-city-china`(中国城市省市区数据,含港澳台)表, 在[uniCloud](https://unicloud.dcloud.net.cn/)控制台使用opendb创建
|
||||
```html
|
||||
<view class="cell" @click="visible6 = true">
|
||||
<text>地址</text>
|
||||
<text v-show="fieldValue6 == null" style="color: #999;">请选择地址 ></text>
|
||||
<text v-show="fieldValue6 != null">{{fieldValue6}}</text>
|
||||
</view>
|
||||
<l-cascader
|
||||
v-model:visible="visible6"
|
||||
v-model="cascaderValue6"
|
||||
:options="options6"
|
||||
:subTitles="subTitles"
|
||||
:keys="{label: 'name', value: 'code'}"
|
||||
title="请选择所在地区"
|
||||
@pick="onPick6"
|
||||
@change="onChange6"/>
|
||||
```
|
||||
```js
|
||||
const visible6 = ref(false)
|
||||
const cascaderValue6 = ref('');
|
||||
const fieldValue6 = ref<string|null>(null);
|
||||
|
||||
const onChange6 = (value: string, options: UTSJSONObject[]) => {
|
||||
fieldValue6.value = options.map((item: UTSJSONObject):any|null => (item['name'])).join('/');
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
|
||||
### 查看示例
|
||||
- 导入后直接使用这个标签查看演示效果
|
||||
|
||||
```html
|
||||
<!-- // 代码位于 uni_modules/lime-cascader/compoents/lime-cascader -->
|
||||
<lime-cascader />
|
||||
```
|
||||
|
||||
|
||||
### 插件标签
|
||||
- 默认 l-cascader 为 component
|
||||
- 默认 lime-cascader 为 demo
|
||||
|
||||
### 关于vue2的使用方式
|
||||
- 插件使用了`composition-api`, 如果你希望在vue2中使用请按官方的教程[vue-composition-api](https://uniapp.dcloud.net.cn/tutorial/vue-composition-api.html)配置
|
||||
- 关键代码是: 在main.js中 在vue2部分加上这一段即可
|
||||
```js
|
||||
// vue2
|
||||
import Vue from 'vue'
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
Vue.use(VueCompositionAPI)
|
||||
```
|
||||
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| v-model:visible | 是否显示级联选择器 | _boolean_ | `false` |
|
||||
| v-model | 值 | _string_ | `-` |
|
||||
| subTitles | 每级展示的次标题,`Array<string>` | _string[]_ | `-` |
|
||||
| placeholder | 未选中时的提示文案 | _string_ | `选择选项` |
|
||||
| keys | 用来定义 `value / label` 在 `options` 中对应的字段别名。 | _{label,value,children}_ | `{}` |
|
||||
| title | 标题 | _string_ | `-` |
|
||||
| options | 可选项数据源 | _[]_ | `-` |
|
||||
| closeable | 关闭按钮 | _boolean_ | `-` |
|
||||
| bgColor | 背景色 | _string_ | `-` |
|
||||
| activeColor | 激活色 | _string_ | `-` |
|
||||
| iconSize | 图标尺寸 | _string_ | `-` |
|
||||
| color | 文本色 | _string_ | `-` |
|
||||
| fontSize | 字体大小 | _string_ | `-` |
|
||||
|
||||
### Events
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
| ---------------- | -------------------------- | ------------------- |
|
||||
| pick | 选择后触发 | `(level: number, index:number, value: string, selectedIndexes:number[])` |
|
||||
| change | 值发生变更时触发 | `(value: string, options: UTSJSONObject[])` |
|
||||
| close | 关闭时触发 | `` |
|
||||
| finish | 选择后触发 | `` |
|
||||
|
||||
|
||||
## 主题定制
|
||||
|
||||
### 样式变量
|
||||
|
||||
组件提供了下列 CSS 变量,可用于自定义样式,uvue app无效。
|
||||
|
||||
| 名称 | 默认值 | 描述 |
|
||||
| ------------------------------ | ------------------------------------ | ---- |
|
||||
| --l-cascader-title-color | _$text-color-1_ | - |
|
||||
| --l-cascader-icon-color | _$primary-color_ | - |
|
||||
| --l-cascader-icon-size | _24px_ | - |
|
||||
| --l-cascader-bg-color | _$bg-color-container_ | - |
|
||||
| --l-cascader-height | _320px_ | - |
|
||||
| --l-cascader-cell-height | _50px_ | - |
|
||||
| --l-cascader-cell-cell-padding | _14px 16px_ | - |
|
||||
| --l-cascader-cell-title-color | _$text-color-1_ | - |
|
||||
| --l-cascader-cell-title-font-size | _$font-size-md_ | - |
|
||||
| --l-cascader-disabled-color | _$text-color-3_ | - |
|
||||
| --l-cascader-title-height | _48px_ | - |
|
||||
| --l-cascader-title-font-size | _18px_ | - |
|
||||
| --l-cascader-options-title-color | _$text-color-3_ | - |
|
||||
|
||||
|
||||
|
||||
## 打赏
|
||||
|
||||
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
|
||||

|
||||

|
||||
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<view class="l-checkbox-group" :class="'l-checkbox-group--'+ direction">
|
||||
<slot />
|
||||
</view>
|
||||
</template>
|
||||
<script lang="uts" setup>
|
||||
/**
|
||||
* CheckboxGroup 复选框组容器
|
||||
* @description 用于管理多个 Checkbox 组件,支持整体禁用、最大选择和布局控制
|
||||
* <br> 插件类型:LCheckboxGroupComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-checkbox-group
|
||||
*
|
||||
* @property {Boolean} disabled 是否禁用组件
|
||||
* @property {Boolean} readonly 是否只读组件
|
||||
* @property {Number} max 支持最多选中的数量
|
||||
* @property {String|Number} name 唯一标识
|
||||
* @property {String|Number} value 选中值
|
||||
* @property {'small' | 'medium' | 'large'} size 组件统一尺寸
|
||||
* @value small
|
||||
* @value medium
|
||||
* @value large
|
||||
* @property {String} direction 布局方向
|
||||
* @value horizontal 水平
|
||||
* @value vertical 垂直
|
||||
* @property {String} icon = [square|round|circle] 形状
|
||||
* @value circle icon 圆形
|
||||
* @value line icon 线
|
||||
* @value rectangle icon 方形
|
||||
* @value dot icon 点状
|
||||
* @property {string} fontSize 文本统一字号
|
||||
* @property {string} iconSize 图标统一尺寸
|
||||
* @property {string} checkedColor 选中状态主题色
|
||||
* @property {string} iconBgColor 图标背景色
|
||||
* @property {string} iconBorderColor 图标边框色
|
||||
* @property {string} iconDisabledColor 禁用图标颜色
|
||||
* @property {string} iconDisabledBgColor 禁用背景色
|
||||
* @event {Function} change
|
||||
*/
|
||||
|
||||
import type { CheckboxGroupProps } from './type';
|
||||
import type { CheckboxChangeOptions } from '../l-checkbox/type';
|
||||
import { setCheckAllStatus } from './utils';
|
||||
|
||||
const emit = defineEmits(['update:value', 'update:modelValue', 'change']);
|
||||
const props = withDefaults(defineProps<CheckboxGroupProps>(), {
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
size: 'medium',
|
||||
direction: 'horizontal',
|
||||
icon: 'rectangle'
|
||||
})
|
||||
|
||||
const _innerValue = ref(props.defaultValue ?? [])
|
||||
const innerValue = computed({
|
||||
set(value: any[]){
|
||||
_innerValue.value = value
|
||||
emit('change', value)
|
||||
emit('update:value', value)
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
get(): any[]{
|
||||
return props.value ?? props.modelValue ?? _innerValue.value
|
||||
},
|
||||
} as WritableComputedOptions<any[]>)
|
||||
|
||||
const checkedSet = computed(():Set<any>=>{
|
||||
const set = new Set<any>()
|
||||
if (Array.isArray(innerValue.value)) {
|
||||
innerValue.value.forEach(item => {
|
||||
set.add(item)
|
||||
})
|
||||
}
|
||||
return set
|
||||
});
|
||||
const children = reactive<LCheckboxComponentPublicInstance[]>([]);
|
||||
// @ts-ignore
|
||||
const checkAllStatus = setCheckAllStatus(children, innerValue, checkedSet);
|
||||
const maxExceeded = computed(():boolean => {
|
||||
return props.max != null && innerValue.value.length == props.max;
|
||||
});
|
||||
|
||||
const manageChildInList = (child: LCheckboxComponentPublicInstance, shouldAdd: boolean) => {
|
||||
const index = children.indexOf(child);
|
||||
if(shouldAdd) {
|
||||
if(index != -1) return
|
||||
children.push(child)
|
||||
} else {
|
||||
if(index == -1) return
|
||||
children.splice(index, 1);
|
||||
}
|
||||
}
|
||||
const handleCheckboxChange = (item: CheckboxChangeOptions) => {
|
||||
const currentValue = item.value;
|
||||
if(Array.isArray(innerValue.value)) {
|
||||
if(currentValue == null) return;
|
||||
const val = [...innerValue.value];
|
||||
if (item.checked) {
|
||||
val.push(currentValue);
|
||||
} else {
|
||||
const i = val.indexOf(currentValue);
|
||||
val.splice(i, 1);
|
||||
}
|
||||
innerValue.value = val
|
||||
} else {
|
||||
console.warn(`CheckboxGroup Warn: \`value\` must be an array, instead of ${typeof innerValue.value}`);
|
||||
}
|
||||
}
|
||||
const getAllCheckboxValue = () : any[] => {
|
||||
const arr:any[] = []
|
||||
for (let i = 0, len = children.length; i < len; i++) {
|
||||
const item = children[i];
|
||||
const value = item.value ?? item.name //?? item.$.uid;
|
||||
if (item.checkAll) continue;
|
||||
if (value == null) continue;
|
||||
if(arr.includes(value)) continue;
|
||||
arr.push(value)
|
||||
if (maxExceeded.value) break;
|
||||
}
|
||||
return arr
|
||||
};
|
||||
const toggleAllCheckboxValues = () : any[] => {
|
||||
const arr:any[] = []
|
||||
|
||||
for (let i = 0, len = children.length; i < len; i++) {
|
||||
const item = children[i];
|
||||
const value = item.value ?? item.name;
|
||||
|
||||
if (item.checkAll) continue;
|
||||
if (value == null) continue;
|
||||
if(!checkedSet.value.has(value)) {
|
||||
arr.push(value)
|
||||
};
|
||||
if (maxExceeded.value) break;
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
const onCheckAllChange = (checked: boolean) => {
|
||||
const value = checked ? getAllCheckboxValue() : [];
|
||||
innerValue.value = value;
|
||||
}
|
||||
|
||||
const onCheckedChange = (item: CheckboxChangeOptions) => {
|
||||
if(item.checkAll) {
|
||||
onCheckAllChange(item.checked);
|
||||
} else {
|
||||
handleCheckboxChange(item);
|
||||
}
|
||||
}
|
||||
|
||||
const toggleAll = (checked : boolean) => {
|
||||
const value = checked ? getAllCheckboxValue() : toggleAllCheckboxValues();
|
||||
innerValue.value = value
|
||||
}
|
||||
|
||||
|
||||
defineExpose({
|
||||
toggleAll
|
||||
})
|
||||
|
||||
provide('limeCheckboxGroup', props)
|
||||
provide('limeCheckboxGroupValue', innerValue)
|
||||
provide('limeCheckboxGroupStatus', checkAllStatus)
|
||||
provide('limeCheckboxGroupCheckedSet', checkedSet)
|
||||
provide('limeCheckboxGroupManageChildInList', manageChildInList)
|
||||
provide('limeCheckboxGroupOnCheckedChange', onCheckedChange)
|
||||
// const optionList = getOptions(props, children);
|
||||
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.l-checkbox-group {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.l-checkbox-group--vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<view class="l-checkbox-group" :class="'l-checkbox-group--'+ direction">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* CheckboxGroup 复选框组容器
|
||||
* @description 用于管理多个 Checkbox 组件,支持整体禁用、最大选择和布局控制
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-checkbox-group
|
||||
*
|
||||
* @property {Boolean} disabled 是否禁用组件
|
||||
* @property {Boolean} readonly 是否只读组件
|
||||
* @property {Number} max 支持最多选中的数量
|
||||
* @property {String|Number} name 唯一标识
|
||||
* @property {String|Number} value 选中值
|
||||
* @property {'small' | 'medium' | 'large'} size 组件统一尺寸
|
||||
* @value small
|
||||
* @value medium
|
||||
* @value large
|
||||
* @property {String} direction 布局方向
|
||||
* @value horizontal 水平
|
||||
* @value vertical 垂直
|
||||
* @property {String} icon = [square|round|circle] 形状
|
||||
* @value circle icon 圆形
|
||||
* @value line icon 线
|
||||
* @value rectangle icon 方形
|
||||
* @value dot icon 点状
|
||||
* @property {string} fontSize 文本统一字号
|
||||
* @property {string} iconSize 图标统一尺寸
|
||||
* @property {string} checkedColor 选中状态主题色
|
||||
* @property {string} iconBgColor 图标背景色
|
||||
* @property {string} iconBorderColor 图标边框色
|
||||
* @property {string} iconDisabledColor 禁用图标颜色
|
||||
* @property {string} iconDisabledBgColor 禁用背景色
|
||||
* @event {Function} change
|
||||
*/
|
||||
|
||||
import { defineComponent, provide, ref, computed, reactive } from '@/uni_modules/lime-shared/vue';
|
||||
import checkboxGroupProps from './props'
|
||||
import type { CheckboxGroupProps } from './type';
|
||||
import type { CheckboxChangeOptions } from '../l-checkbox/type';
|
||||
import { setCheckAllStatus } from './utils';
|
||||
|
||||
const name = 'l-checkbox-group'
|
||||
export default defineComponent({
|
||||
name,
|
||||
props: checkboxGroupProps,
|
||||
emits: ['update:value', 'update:modelValue', 'change', 'input'],
|
||||
setup(props, { emit, expose }) {
|
||||
const _innerValue = ref(props.defaultValue ?? [])
|
||||
const innerValue = computed({
|
||||
set(value: any[]){
|
||||
_innerValue.value = value
|
||||
emit('change', value)
|
||||
emit('update:value', value)
|
||||
emit('update:modelValue', value)
|
||||
// #ifdef VUE2
|
||||
emit('input', value)
|
||||
// #endif
|
||||
},
|
||||
get(): any[]{
|
||||
return props.value ?? props.modelValue ?? _innerValue.value
|
||||
},
|
||||
} as WritableComputedOptions<any[]>)
|
||||
|
||||
const checkedSet = computed(():Set<any>=>{
|
||||
const set = new Set<any>()
|
||||
if (Array.isArray(innerValue.value)) {
|
||||
innerValue.value.forEach(item => {
|
||||
set.add(item)
|
||||
})
|
||||
}
|
||||
return set
|
||||
});
|
||||
const children = reactive<LCheckboxComponentPublicInstance[]>([]);
|
||||
// @ts-ignore
|
||||
const checkAllStatus = setCheckAllStatus(children, innerValue, checkedSet);
|
||||
const maxExceeded = computed(():boolean => {
|
||||
return props.max != null && innerValue.value.length == props.max;
|
||||
});
|
||||
|
||||
const manageChildInList = (child: LCheckboxComponentPublicInstance, shouldAdd: boolean) => {
|
||||
const index = children.indexOf(child);
|
||||
if(shouldAdd) {
|
||||
if(index != -1) return
|
||||
children.push(child)
|
||||
} else {
|
||||
if(index == -1) return
|
||||
children.splice(index, 1);
|
||||
}
|
||||
}
|
||||
const handleCheckboxChange = (item: CheckboxChangeOptions) => {
|
||||
const currentValue = item.value;
|
||||
if(Array.isArray(innerValue.value)) {
|
||||
if(currentValue == null) return;
|
||||
const val = [...innerValue.value];
|
||||
if (item.checked) {
|
||||
val.push(currentValue);
|
||||
} else {
|
||||
const i = val.indexOf(currentValue);
|
||||
val.splice(i, 1);
|
||||
}
|
||||
innerValue.value = val
|
||||
} else {
|
||||
console.warn(`CheckboxGroup Warn: \`value\` must be an array, instead of ${typeof innerValue.value}`);
|
||||
}
|
||||
}
|
||||
const getAllCheckboxValue = () : any[] => {
|
||||
const arr:any[] = []
|
||||
for (let i = 0, len = children.length; i < len; i++) {
|
||||
const item = children[i];
|
||||
const value = item.value ?? item.name //?? item.$.uid;
|
||||
if (item.checkAll) continue;
|
||||
if (value == null) continue;
|
||||
if(arr.includes(value)) continue;
|
||||
arr.push(value)
|
||||
if (maxExceeded.value) break;
|
||||
}
|
||||
return arr
|
||||
};
|
||||
const toggleAllCheckboxValues = () : any[] => {
|
||||
const arr:any[] = []
|
||||
|
||||
for (let i = 0, len = children.length; i < len; i++) {
|
||||
const item = children[i];
|
||||
const value = item.value ?? item.name;
|
||||
|
||||
if (item.checkAll) continue;
|
||||
if (value == null) continue;
|
||||
if(!checkedSet.value.has(value)) {
|
||||
arr.push(value)
|
||||
};
|
||||
if (maxExceeded.value) break;
|
||||
}
|
||||
return arr;
|
||||
};
|
||||
const onCheckAllChange = (checked: boolean) => {
|
||||
const value = checked ? getAllCheckboxValue() : [];
|
||||
innerValue.value = value;
|
||||
}
|
||||
|
||||
const onCheckedChange = (item: CheckboxChangeOptions) => {
|
||||
if(item.checkAll) {
|
||||
onCheckAllChange(item.checked);
|
||||
} else {
|
||||
handleCheckboxChange(item);
|
||||
}
|
||||
}
|
||||
|
||||
const toggleAll = (checked : boolean) => {
|
||||
const value = checked ? getAllCheckboxValue() : toggleAllCheckboxValues();
|
||||
innerValue.value = value
|
||||
}
|
||||
|
||||
// #ifdef VUE3
|
||||
expose({
|
||||
toggleAll
|
||||
})
|
||||
// #endif
|
||||
provide('limeCheckboxGroup', props)
|
||||
provide('limeCheckboxGroupValue', innerValue)
|
||||
provide('limeCheckboxGroupStatus', checkAllStatus)
|
||||
provide('limeCheckboxGroupCheckedSet', checkedSet)
|
||||
provide('limeCheckboxGroupManageChildInList', manageChildInList)
|
||||
provide('limeCheckboxGroupOnCheckedChange', onCheckedChange)
|
||||
|
||||
return {
|
||||
// #ifdef VUE2
|
||||
toggleAll
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.l-checkbox-group {
|
||||
// background-color: antiquewhite;
|
||||
}
|
||||
|
||||
.l-checkbox-group--vertical {
|
||||
:deep(.l-checkbox) {
|
||||
display: flex;
|
||||
// line-height: 64rpx;
|
||||
}
|
||||
|
||||
:deep(l-checkbox) {
|
||||
display: flex;
|
||||
// line-height: 64rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
// @ts-nocheck
|
||||
export default {
|
||||
/** 是否禁用组件,默认为 false。CheckboxGroup.disabled 优先级低于 Checkbox.disabled */
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 支持最多选中的数量 */
|
||||
max: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
/** 唯一标识 */
|
||||
name: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
/** 选中值 */
|
||||
value: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
/** 选中值,非受控属性 */
|
||||
defaultValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'horizontal'
|
||||
},
|
||||
// 未实现
|
||||
/** 以配置形式设置子元素。示例1:`['北京', '上海']` ,示例2: `[{ label: '全选', checkAll: true }, { label: '上海', value: 'shanghai' }]`。checkAll 值为 true 表示当前选项为「全选选项」 */
|
||||
options: {
|
||||
type: Array,
|
||||
},
|
||||
checkedColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconBgColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconBorderColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconDisabledColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconDisabledBgColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'rectangle'
|
||||
}, //?: 'circle' | 'line' | 'dot';
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium'
|
||||
}, //?: 'small' | 'medium' | 'large';
|
||||
iconSize: {
|
||||
type: String,
|
||||
defalut: null
|
||||
},
|
||||
fontSize: {
|
||||
type: String,
|
||||
defalut: null
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// @ts-nocheck
|
||||
export type CheckerDirection = 'horizontal' | 'vertical';
|
||||
export type CheckboxGroupValue = any[]//Array<string | number | boolean>;
|
||||
|
||||
export interface CheckboxGroupProps {
|
||||
/** 是否禁用组件,默认为 false。CheckboxGroup.disabled 优先级低于 Checkbox.disabled */
|
||||
disabled : boolean;
|
||||
readonly : boolean;
|
||||
/** 支持最多选中的数量 */
|
||||
max?: number;
|
||||
/** 唯一标识 */
|
||||
name?: any;//string;
|
||||
/** 选中值 */
|
||||
value?: any[];
|
||||
modelValue?: any[];
|
||||
/** 选中值,非受控属性 */
|
||||
defaultValue?: any[];
|
||||
|
||||
size: 'small' | 'medium' | 'large';
|
||||
direction : CheckerDirection;
|
||||
gap?: string;
|
||||
icon: 'circle' | 'line' | 'rectangle' | 'dot';
|
||||
|
||||
//未实现
|
||||
// options: Array<unknown>,
|
||||
fontSize?: string;
|
||||
iconSize?: string;
|
||||
checkedColor?: string;
|
||||
iconBgColor?: string;
|
||||
iconBorderColor?: string;
|
||||
iconDisabledColor?: string;
|
||||
iconDisabledBgColor?: string;
|
||||
}
|
||||
|
||||
// #ifndef APP-ANDROID || APP-IOS
|
||||
export type {ComputedRef} from 'vue';
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID || APP-IOS
|
||||
export type ComputedRef<T> = ComputedRefImpl<T>;
|
||||
// #endif
|
||||
@@ -0,0 +1,64 @@
|
||||
// @ts-nocheck
|
||||
import type { ComputedRef } from './type';
|
||||
import type { CheckboxStatus } from '../l-checkbox/type';
|
||||
// #ifndef UNI-APP-X
|
||||
import { computed } from '@/uni_modules/lime-shared/vue';
|
||||
// #endif
|
||||
|
||||
function intersection<T>(...arrays : T[][]): T[] {
|
||||
// 创建一个空数组来存储相交元素
|
||||
const result : T[] = [];
|
||||
|
||||
for (let i = 0; i < arrays[0].length; i++) {
|
||||
const item = arrays[0][i]
|
||||
// 检查该元素是否存在于所有其他数组中
|
||||
let isCommon = true;
|
||||
for (let j = 1; j < arrays.length; j++) {
|
||||
if (!arrays[j].includes(item)) {
|
||||
isCommon = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 如果该元素存在于所有数组中,并且尚未添加到结果数组中,则将其添加到结果数组中
|
||||
if (isCommon && !result.includes(item)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
// 返回包含相交元素的结果数组
|
||||
return result;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
export function setCheckAllStatus(
|
||||
children: LCheckboxComponentPublicInstance[],
|
||||
innerValue: ComputedRef<any[]>,
|
||||
checkedSet: ComputedRef<Set<any>>
|
||||
): ComputedRef<CheckboxStatus> {
|
||||
|
||||
const intersectionLen = computed(()=>{
|
||||
const values:any[] = []
|
||||
children.forEach(item => {
|
||||
const value = item.value ?? item.name;
|
||||
if(value == null) return
|
||||
values.push(value)
|
||||
})
|
||||
if (Array.isArray(innerValue.value)) {
|
||||
return intersection(innerValue.value, values).length;
|
||||
}
|
||||
return 0
|
||||
})
|
||||
const isAllChecked = computed((): boolean=>{
|
||||
if (checkedSet.value.size != children.length - 1) {
|
||||
return false;
|
||||
}
|
||||
return intersectionLen.value == children.length - 1;
|
||||
})
|
||||
const isIndeterminate = computed((): boolean=>{
|
||||
return !isAllChecked.value && intersectionLen.value < children.length && intersectionLen.value > 0;
|
||||
})
|
||||
return computed(():CheckboxStatus => {
|
||||
if (isAllChecked.value) return 'checked';
|
||||
if (isIndeterminate.value) return 'indeterminate';
|
||||
return 'uncheck';
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
@import '~@/uni_modules/lime-style/index.scss';
|
||||
@import '~@/uni_modules/lime-style/functions.scss';
|
||||
/* #ifdef uniVersion >= 4.75 */
|
||||
$use-css-var: true !default;
|
||||
/* #endif */
|
||||
|
||||
$checkbox: #{$prefix}-checkbox;
|
||||
$icon: #{$checkbox}__icon;
|
||||
|
||||
|
||||
$checkbox-icon-size: create-var(checkbox-icon-size, 20px);
|
||||
$checkbox-font-size: create-var(checkbox-font-size, 16px);
|
||||
|
||||
$checkbox-small-icon-size: create-var(checkbox-small-icon-size, 14px);
|
||||
$checkbox-small-font-size: create-var(checkbox-small-font-size, 15px);
|
||||
|
||||
$checkbox-large-icon-size: create-var(checkbox-large-icon-size, 22px);
|
||||
$checkbox-large-font-size: create-var(checkbox-large-font-size, 18px);
|
||||
|
||||
$checkbox-icon-border-width: create-var(checkbox-icon-border-width, 1px);
|
||||
// $checkbox-icon-border-color: var(--l-checkbox-border-color, $border-color);
|
||||
$checkbox-icon-border-radius: create-var(checkbox-icon-border-radius, 3px);
|
||||
|
||||
$checkbox-icon-bg-color: create-var(checkbox-icon-bg-color, $bg-color-container);
|
||||
$checkbox-icon-border-color: create-var(checkbox-border-icon-color, $gray-5);
|
||||
$checkbox-icon-disabled-color: create-var(checkbox-icon-disabled-color, $gray-5);
|
||||
$checkbox-icon-disabled-bg-color: create-var(checkbox-icon-disabled-bg-color, $gray-1);
|
||||
$checkbox-icon-checked-color: create-var(checkbox-icon-checked-color, $primary-color);
|
||||
$checkbox-text-color: create-var(checkbox-text-color, $text-color-1);
|
||||
$checkbox-icon-text-gap: create-var(checkbox-icon-text-gap, $spacer-xs);
|
||||
|
||||
/* #ifdef MP */
|
||||
:host {
|
||||
display: inline-flex;
|
||||
}
|
||||
/* #endif */
|
||||
.#{$checkbox} {
|
||||
/* #ifndef UNI-APP-X */
|
||||
display: inline-flex;
|
||||
/* #endif */
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
&__icon {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: $checkbox-icon-size;
|
||||
height: $checkbox-icon-size;
|
||||
align-self: center;
|
||||
transition-property: all;
|
||||
// #ifdef UNI-APP-X && APP
|
||||
// #ifndef APP-HARMONY || APP-ANDROID
|
||||
// 鸿蒙加上时间会导致无法描边 安卓会没有尺寸
|
||||
transition-duration: 200ms;
|
||||
/* #endif */
|
||||
/* #endif */
|
||||
transition-timing-function: cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
/* #ifndef UNI-APP-X && APP */
|
||||
&:after {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
opacity: 0;
|
||||
content: "";
|
||||
transition-property: all;
|
||||
transition-duration: 200ms;
|
||||
transition-timing-function: cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
}
|
||||
/* #endif */
|
||||
&--rectangle {
|
||||
// border-radius: $checkbox-icon-border-radius;
|
||||
@include border-radius($checkbox-icon-border-radius);
|
||||
}
|
||||
&--dot,
|
||||
&--circle {
|
||||
border-radius: 99px;
|
||||
}
|
||||
&--rectangle,
|
||||
&--dot,
|
||||
&--circle{
|
||||
background-color: $checkbox-icon-bg-color;
|
||||
border-width: $checkbox-icon-border-width;
|
||||
border-style: solid;
|
||||
border-color: $checkbox-icon-border-color;
|
||||
// border: $checkbox-icon-border-width solid $checkbox-icon-border-color;
|
||||
}
|
||||
&--rectangle,&--circle {
|
||||
/* #ifndef UNI-APP-X && APP */
|
||||
&:after {
|
||||
top: 48%;
|
||||
left: 24%;
|
||||
display: table;
|
||||
width: divide(100%, 20) * 7;
|
||||
height: divide(100%, 20) * 12;
|
||||
border: calc(#{$checkbox-icon-size} / 7) solid transparent;
|
||||
border-top: 0;
|
||||
border-inline-start: 0;
|
||||
transform: rotate(45deg) scale(0) translate(-50%,-50%);
|
||||
content: "";
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
&--rectangle#{&}--checked,
|
||||
&--circle#{&}--checked {
|
||||
background-color: $checkbox-icon-checked-color;
|
||||
border-color: $checkbox-icon-checked-color;
|
||||
/* #ifndef UNI-APP-X && APP */
|
||||
&:after {
|
||||
opacity: 1;
|
||||
transform: rotate(45deg) scale(1) translate(-50%,-50%);
|
||||
border-color: white;
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
&--rectangle#{&}--indeterminate,
|
||||
&--circle#{&}--indeterminate{
|
||||
background-color: $checkbox-icon-checked-color;
|
||||
border-color: $checkbox-icon-checked-color;
|
||||
/* #ifndef UNI-APP-X && APP */
|
||||
&:after {
|
||||
opacity: 1;
|
||||
height: 0;
|
||||
width: 50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: scale(1) translate(-50%,-50%);
|
||||
border-color: white;
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
&--dot {
|
||||
/* #ifndef UNI-APP-X && APP */
|
||||
&:after {
|
||||
background-color: white;
|
||||
border-radius: 99px;//$checkbox-icon-border-radius;
|
||||
transform: scale(0) translate(-50%,-50%);
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
&--dot#{&}--checked{
|
||||
background-color: $checkbox-icon-checked-color;
|
||||
border-color: $checkbox-icon-checked-color;
|
||||
/* #ifndef UNI-APP-X && APP */
|
||||
&:after{
|
||||
opacity: 1;
|
||||
width: 44%;
|
||||
height: 44%;
|
||||
transform: scale(1) translate(-50%,-50%);
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
&--dot#{&}--indeterminate {
|
||||
background-color: $checkbox-icon-checked-color;
|
||||
border-color: $checkbox-icon-checked-color;
|
||||
/* #ifndef UNI-APP-X && APP */
|
||||
&:after {
|
||||
opacity: 1;
|
||||
border-radius: 0;
|
||||
transform: scale(1) translate(-50%,-50%);
|
||||
width: 50%;
|
||||
height: calc(#{$checkbox-icon-size} / 7);
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
&--line {
|
||||
/* #ifndef UNI-APP-X && APP */
|
||||
&:after {
|
||||
top: 46%;
|
||||
left: 0%;
|
||||
inset-inline-start: 10%;
|
||||
display: table;
|
||||
width: divide(100%, 14) * 7;
|
||||
height: divide(100%, 14) * 12;
|
||||
border: calc(#{$checkbox-icon-size} / 7) solid transparent;
|
||||
border-top: 0;
|
||||
border-inline-start: 0;
|
||||
transform: rotate(45deg) scale(0) translate(-50%,-50%);
|
||||
content: "";
|
||||
transition-timing-function: cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
&--line#{&}--checked {
|
||||
/* #ifndef UNI-APP-X && APP */
|
||||
&:after {
|
||||
opacity: 1;
|
||||
transform: rotate(45deg) scale(1) translate(-50%,-50%);
|
||||
border-color: $checkbox-icon-checked-color;
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
&--line#{&}--indeterminate {
|
||||
/* #ifndef UNI-APP-X && APP */
|
||||
&:after {
|
||||
opacity: 1;
|
||||
height: 0;
|
||||
left: 50%;
|
||||
width: 70%;
|
||||
transform: scale(1) translate(-50%,-50%);
|
||||
border-color: $checkbox-icon-checked-color;
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
&--rectangle#{&}--disabled,
|
||||
&--circle#{&}--disabled,
|
||||
&--dot#{&}--disabled {
|
||||
border-color: $checkbox-icon-disabled-color;
|
||||
background-color: $checkbox-icon-disabled-bg-color;
|
||||
}
|
||||
&--rectangle#{&}--disabled#{&}--checked,
|
||||
&--circle#{&}--disabled#{&}--checked,
|
||||
&--dot#{&}--disabled#{&}--checked,
|
||||
&--rectangle#{&}--disabled#{&}--indeterminate,
|
||||
&--circle#{&}--disabled#{&}--indeterminate,
|
||||
&--dot#{&}--disabled#{&}--indeterminate {
|
||||
/* #ifndef UNI-APP-X && APP */
|
||||
&:after {
|
||||
border-color: $checkbox-icon-disabled-color;
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
&--dot#{&}--disabled#{&}--checked,
|
||||
&--dot#{&}--disabled#{&}--indeterminate {
|
||||
/* #ifndef UNI-APP-X && APP */
|
||||
&:after {
|
||||
background-color: $checkbox-icon-disabled-color;
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
&--line#{&}--disabled#{&}--checked,
|
||||
&--line#{&}--disabled#{&}--indeterminate{
|
||||
/* #ifndef UNI-APP-X && APP */
|
||||
&:after {
|
||||
border-color: $checkbox-icon-disabled-color;
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
}
|
||||
&__label {
|
||||
padding-left: $checkbox-icon-text-gap;//$spacer-xs;
|
||||
// padding-right: $spacer-xs;
|
||||
font-size: $checkbox-font-size;
|
||||
color: $checkbox-text-color;//$text-color-1;
|
||||
white-space: nowrap; //ios
|
||||
&--disabled {
|
||||
color: $checkbox-icon-disabled-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
<template>
|
||||
<view class="l-checkbox" :class="[rootCasses]" :style="[styles]" @click="handleChange">
|
||||
<slot name="checkbox" :checked="isChecked" :disabled="isDisabled">
|
||||
<slot name="icon" :checked="isChecked" :disabled="isDisabled">
|
||||
<view class="l-checkbox__icon" ref="iconRef"
|
||||
:class="[
|
||||
`l-checkbox__icon--${icon}`,
|
||||
{
|
||||
'l-checkbox__icon--checked': isChecked,
|
||||
'l-checkbox__icon--disabled': isDisabled,
|
||||
'l-checkbox__icon--indeterminate': isIndeterminate,
|
||||
}
|
||||
]" :style="[iconStyle]"></view>
|
||||
</slot>
|
||||
<text class="l-checkbox__label" :style="[labelStyles]" :class="labelClass" v-if="label!= null || $slots['default'] !=null">
|
||||
<slot>{{label}}</slot>
|
||||
</text>
|
||||
</slot>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* Checkbox 复选框组件
|
||||
* @description 用于在多个选项中进行选择的表单组件,支持单选、全选和不确定态
|
||||
* <br> 插件类型:LCheckboxComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-checkbox
|
||||
*
|
||||
* @property {boolean} defaultChecked 默认选中状态(非受控属性)
|
||||
* @property {string} label 显示文本(支持插槽)
|
||||
* @property {boolean} indeterminate 半选状态(优先级高于checked)
|
||||
* @property {boolean} disabled 禁用状态(覆盖Group设置)
|
||||
* @property {'small' | 'medium' | 'large'} size 组件尺寸
|
||||
* @value small
|
||||
* @value medium
|
||||
* @value large
|
||||
* @property {string} name 唯一标识符(表单提交使用)
|
||||
* @property {boolean} checkAll 标记为全选选项(需在Group中使用)
|
||||
* @property {string} value 选项值(Group模式下必填)
|
||||
* @property {'circle' | 'line' | 'rectangle' | 'dot'} icon 图标样式类型
|
||||
* @value circle 圆
|
||||
* @value line 线
|
||||
* @value rectangle 方
|
||||
* @value dot 点
|
||||
* @property {string} fontSize 文本字号(支持CSS单位)
|
||||
* @property {string} iconSize 图标尺寸(支持CSS单位)
|
||||
* @property {string} checkedColor 选中状态主题色
|
||||
* @property {string} iconBgColor 图标背景色
|
||||
* @property {string} iconBorderColor 图标边框色
|
||||
* @property {string} iconDisabledColor 禁用状态图标颜色
|
||||
* @property {string} iconDisabledBgColor 禁用状态背景色
|
||||
* @property {string|object} labelStyle label的样式
|
||||
* @event {Function} change 状态变化时触发(参数:CheckboxChangeOptions)
|
||||
*/
|
||||
|
||||
import type { CheckboxProps, ManageChildInList, CheckboxStatus, OnCheckedChange, CheckboxChangeOptions, ComputedRef } from './type';
|
||||
// import { isDarkMode, themeTokens } from '@/uni_modules/lime-style'
|
||||
const themeVars = inject('limeConfigProviderThemeVars', computed(()=> ({})))
|
||||
defineSlots<{
|
||||
checkbox(props : { checked : boolean, disabled: boolean}) : any,
|
||||
icon(props : { checked : boolean, disabled: boolean}) : any,
|
||||
default(props : { checked : boolean, disabled: boolean}) : any,
|
||||
}>()
|
||||
|
||||
|
||||
const name = 'l-checkbox';
|
||||
const emit = defineEmits(['update:modelValue', 'change']);
|
||||
|
||||
const props = withDefaults(defineProps<CheckboxProps>(), {
|
||||
defaultChecked: false,
|
||||
indeterminate: false,
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
// checked: null,
|
||||
// modelValue: null,
|
||||
size: 'medium',
|
||||
checkAll: false,
|
||||
icon: 'rectangle'
|
||||
})
|
||||
|
||||
defineOptions({
|
||||
mixins: [{
|
||||
props: {
|
||||
checked: {
|
||||
type: [null, Boolean],
|
||||
default: null,
|
||||
},
|
||||
modelValue: {
|
||||
type: [null, Boolean],
|
||||
default: null,
|
||||
},
|
||||
}
|
||||
}]
|
||||
})
|
||||
|
||||
|
||||
const instance = getCurrentInstance()!
|
||||
const formDisabled = inject<Ref<boolean|null>|null>('formDisabled', null)
|
||||
const formReadonly = inject<Ref<boolean|null>|null>('formReadonly', null)
|
||||
|
||||
const checkboxGroup = inject<LCheckboxGroupComponentPublicInstance|null>('limeCheckboxGroup', null);
|
||||
const checkboxGroupValue = inject<ComputedRef<any[]>|null>('limeCheckboxGroupValue', null);
|
||||
const checkboxGroupStatus = inject<ComputedRef<CheckboxStatus>|null>('limeCheckboxGroupStatus', null);
|
||||
const checkboxGroupCheckedSet = inject<ComputedRef<Set<any>>|null>('limeCheckboxGroupCheckedSet', null);
|
||||
const manageChildInList = inject<ManageChildInList|null>('limeCheckboxGroupManageChildInList', null);
|
||||
const onCheckedChange = inject<OnCheckedChange|null>('limeCheckboxGroupOnCheckedChange', null);
|
||||
if(manageChildInList != null) {
|
||||
manageChildInList(instance.proxy as LCheckboxComponentPublicInstance, true)
|
||||
}
|
||||
|
||||
const max = computed(():number => checkboxGroup?.max ?? -1)
|
||||
|
||||
const _innerChecked = ref(props.defaultChecked)
|
||||
const innerChecked = computed({
|
||||
set(value: boolean) {
|
||||
_innerChecked.value = value
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
},
|
||||
get():boolean {
|
||||
const value = (props.checked ?? props.modelValue)
|
||||
if(value != null) return value
|
||||
return _innerChecked.value
|
||||
}
|
||||
} as WritableComputedOptions<boolean>)
|
||||
|
||||
const isChecked = computed(():boolean=>{
|
||||
if (props.checkAll) {
|
||||
const checkAllStatus = checkboxGroupStatus?.value ?? 'uncheck';
|
||||
return checkAllStatus == 'checked' || checkAllStatus == 'indeterminate'
|
||||
}
|
||||
const value = props.value ?? props.name;
|
||||
if (checkboxGroupCheckedSet != null && value != null) {
|
||||
return checkboxGroupCheckedSet.value.has(value)
|
||||
}
|
||||
return innerChecked.value;
|
||||
})
|
||||
|
||||
const isDisabled = computed(():boolean=>{
|
||||
if(max.value > -1 && checkboxGroupValue != null) {
|
||||
return max.value <= checkboxGroupValue.value.length && !isChecked.value;
|
||||
}
|
||||
if (props.disabled) return props.disabled;
|
||||
return formDisabled?.value ?? checkboxGroup?.disabled ?? false;
|
||||
})
|
||||
|
||||
const isReadonly= computed(():boolean=>{
|
||||
if (props.readonly) return props.readonly;
|
||||
return formReadonly?.value ?? checkboxGroup?.readonly ?? false;
|
||||
})
|
||||
|
||||
const isIndeterminate = computed(():boolean=>{
|
||||
if (props.checkAll && checkboxGroupStatus != null) return checkboxGroupStatus.value == 'indeterminate';
|
||||
return props.indeterminate;
|
||||
})
|
||||
|
||||
const innerIcon = computed(():string => checkboxGroup?.icon ?? props.icon)
|
||||
const innerSize = computed(():string => checkboxGroup?.size ?? props.size)
|
||||
const innerIconSize = computed(():string|null => checkboxGroup?.iconSize ?? props.iconSize)
|
||||
const innerFontSize = computed(():string|null => checkboxGroup?.fontSize ?? props.fontSize)
|
||||
const innerCheckedColor = computed(():string|null => checkboxGroup?.checkedColor ?? props.checkedColor)
|
||||
const innerIconBgColor = computed(():string|null => props.iconBgColor ?? checkboxGroup?.iconBgColor)
|
||||
const innerIconBorderColor = computed(():string|null => props.iconBorderColor ?? checkboxGroup?.iconBorderColor )
|
||||
const innerIconDisabledColor = computed(():string|null => props.iconDisabledColor ?? checkboxGroup?.iconDisabledColor )
|
||||
const innerIconDisabledBgColor = computed(():string|null => props.iconDisabledBgColor ?? checkboxGroup?.iconDisabledBgColor)
|
||||
|
||||
|
||||
const rootCasses = computed(():Map<string, boolean>=>{
|
||||
const cls = new Map<string, boolean>()
|
||||
cls.set(`${name}--${props.size}`, true)
|
||||
cls.set(`${name}--disabled`, isDisabled.value)
|
||||
return cls
|
||||
})
|
||||
|
||||
const iconClasses = computed(():Map<string, boolean>=>{
|
||||
const cls = new Map<string, boolean>()
|
||||
cls.set(`${name}__icon--disabled`, isDisabled.value)
|
||||
cls.set(`${name}__icon--${props.icon}`, true)
|
||||
// #ifdef APP
|
||||
cls.set(`${name}__icon--checked`, isChecked.value && innerCheckedColor.value == null)
|
||||
cls.set(`${name}__icon--indeterminate`, isIndeterminate.value && innerCheckedColor.value == null)
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
cls.set(`${name}__icon--checked`, isChecked.value)
|
||||
cls.set(`${name}__icon--indeterminate`, isIndeterminate.value)
|
||||
// #endif
|
||||
return cls
|
||||
})
|
||||
|
||||
const labelClass = computed(():Map<string, boolean>=>{
|
||||
const cls = new Map<string, boolean>();
|
||||
cls.set(`${name}__label--disabled`, isDisabled.value)
|
||||
return cls
|
||||
})
|
||||
|
||||
const styles = computed(():Map<string, any>=>{
|
||||
const style = new Map<string, any>();
|
||||
if(checkboxGroup != null && checkboxGroup.gap != null) {
|
||||
style.set(checkboxGroup.direction == 'horizontal' ? 'margin-right' : 'margin-bottom', checkboxGroup.gap!)
|
||||
}
|
||||
// #ifndef APP
|
||||
if(innerCheckedColor.value != null) {
|
||||
style.set('--l-checkbox-icon-checked-color', innerCheckedColor.value!)
|
||||
}
|
||||
if(innerIconBorderColor.value != null) {
|
||||
style.set('--l-checkbox-icon-border-color', innerIconBorderColor.value!)
|
||||
}
|
||||
if(innerIconDisabledColor.value != null) {
|
||||
style.set('--l-checkbox-icon-disabled-color', innerIconDisabledColor.value!)
|
||||
}
|
||||
if(innerIconDisabledBgColor.value != null) {
|
||||
style.set('--l-checkbox-icon-disabled-bg-color', innerIconDisabledBgColor.value!)
|
||||
}
|
||||
if(innerFontSize.value != null) {
|
||||
style.set('--l-checkbox-font-size', innerFontSize.value!)
|
||||
}
|
||||
// #endif
|
||||
return style
|
||||
})
|
||||
|
||||
const iconStyle = computed(():Map<string, any>=>{
|
||||
const style = new Map<string, any>();
|
||||
if(innerIconSize.value != null) {
|
||||
style.set('width', innerIconSize.value!)
|
||||
style.set('height', innerIconSize.value!)
|
||||
// #ifndef APP
|
||||
style.set('--l-checkbox-icon-size', innerIconSize.value!)
|
||||
// #endif
|
||||
}
|
||||
|
||||
if(innerCheckedColor.value != null) {
|
||||
// #ifndef APP
|
||||
style.set('--l-checkbox-icon-checked-color', innerCheckedColor.value!)
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
|
||||
if(!isDisabled.value && !isChecked.value && ['dot', 'circle', 'rectangle'].includes(innerIcon.value)) {
|
||||
if(innerIconBgColor.value!=null) {
|
||||
style.set('background-color', innerIconBgColor.value!)
|
||||
}
|
||||
if(innerIconBorderColor.value!=null) {
|
||||
style.set('border-color', innerIconBorderColor.value!)
|
||||
}
|
||||
}
|
||||
if(isDisabled.value && ['dot', 'circle', 'rectangle'].includes(innerIcon.value)) {
|
||||
if(innerIconDisabledBgColor.value!=null) {
|
||||
style.set('background-color', innerIconDisabledBgColor.value!)
|
||||
}
|
||||
if(innerIconDisabledColor.value!=null) {
|
||||
style.set('border-color', innerIconDisabledColor.value!)
|
||||
}
|
||||
|
||||
}
|
||||
if(isChecked.value && ['dot', 'circle', 'rectangle'].includes(innerIcon.value)) {
|
||||
style.set('background-color', innerCheckedColor.value!)
|
||||
style.set('border-color', innerCheckedColor.value!)
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
return style
|
||||
})
|
||||
|
||||
// const labelStyle = computed(():Map<string, any>=>{
|
||||
// const style = new Map<string, any>();
|
||||
// const fontSize = props.fontSize ?? checkboxGroup?.fontSize
|
||||
// if(fontSize != null) {
|
||||
// style.set('font-size', fontSize)
|
||||
// }
|
||||
// return style
|
||||
// })
|
||||
|
||||
const labelStyles = computed(():any => {
|
||||
if(typeof props.labelStyle == 'string') {
|
||||
return `${props.labelStyle};` + (innerIconSize.value != null ? `font-size: ${innerIconSize.value}`: '')
|
||||
}
|
||||
if(typeof props.labelStyle == 'object') {
|
||||
return UTSJSONObject.assign({}, (props.labelStyle??{}) as UTSJSONObject, innerFontSize.value != null ? {'font-size': innerFontSize.value}: {})
|
||||
}
|
||||
return {}
|
||||
})
|
||||
const handleChange = (e: UniPointerEvent) => {
|
||||
if (isDisabled.value || isReadonly.value) return;
|
||||
const value = !isChecked.value;
|
||||
innerChecked.value = value;
|
||||
|
||||
if(onCheckedChange != null) {
|
||||
onCheckedChange({
|
||||
checked: value,
|
||||
checkAll: props.checkAll,
|
||||
value: props.value ?? props.name //?? instance.uid
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// #ifdef APP
|
||||
const iconRef = ref<UniElement|null>(null)
|
||||
|
||||
const update = () => {
|
||||
if(iconRef.value == null) return
|
||||
const ctx = iconRef.value!.getDrawableContext()!;
|
||||
const rect = iconRef.value!.getBoundingClientRect();
|
||||
// #ifndef APP-HARMONY
|
||||
const iconSize = rect.width
|
||||
// #endif
|
||||
// #ifdef APP-HARMONY
|
||||
const iconSize = rect.width - 1.5
|
||||
// #endif
|
||||
|
||||
const x = iconSize / 2;
|
||||
const y = iconSize / 2;
|
||||
|
||||
|
||||
const primaryColor = `${themeVars.value['checkboxIconCheckedColor'] ?? '#3283ff'}`
|
||||
const disabledColor = `${themeVars.value['checkboxIconDisabledColor'] ?? '#c5c5c5'}`
|
||||
|
||||
ctx.reset();
|
||||
const drawIndeterminate = () => {
|
||||
ctx.strokeStyle = isDisabled.value
|
||||
? innerIconDisabledColor.value ?? disabledColor//themes.get(isDarkMode.value ? 'dark' :'light')!
|
||||
: innerIcon.value == 'line'
|
||||
? props.checkedColor ?? primaryColor //'#3283ff'
|
||||
: 'white';
|
||||
ctx.lineWidth = innerIcon.value == 'line' ? iconSize * 0.16 : iconSize * 0.12;
|
||||
const width = innerIcon.value == 'line' ? iconSize * 0.8 : iconSize * 0.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo((iconSize - width) / 2, iconSize * 0.5);
|
||||
ctx.lineTo((iconSize - width) / 2 + width, iconSize * 0.5);
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
const drawCheckedIcon = () => {
|
||||
if (isDisabled.value) {
|
||||
ctx.strokeStyle = innerIconDisabledColor.value ?? disabledColor;
|
||||
ctx.fillStyle = innerIconDisabledColor.value ?? disabledColor;
|
||||
} else if (innerIcon.value == 'line') {
|
||||
ctx.strokeStyle = props.checkedColor ?? primaryColor //'#3283ff';
|
||||
} else {
|
||||
ctx.strokeStyle = 'white';
|
||||
ctx.fillStyle = 'white';
|
||||
}
|
||||
ctx.lineWidth = innerIcon.value == 'line' ? iconSize * 0.16 : iconSize * 0.12;
|
||||
ctx.lineCap = 'round';
|
||||
|
||||
if (innerIcon.value == 'circle' || innerIcon.value == 'rectangle') {
|
||||
ctx.beginPath();
|
||||
// #ifndef APP-HARMONY
|
||||
ctx.moveTo(iconSize * 0.2967, iconSize * 0.53);
|
||||
ctx.lineTo(iconSize * 0.4342, iconSize * 0.6675);
|
||||
ctx.lineTo(iconSize * 0.7092, iconSize * 0.3925);
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-HARMONY
|
||||
// 鸿蒙不支持lineCap
|
||||
ctx.moveTo(iconSize * 0.23, iconSize * 0.45);
|
||||
ctx.lineTo(iconSize * 0.44, iconSize * 0.6675);
|
||||
ctx.lineTo(iconSize * 0.73, iconSize * 0.3);
|
||||
// #endif
|
||||
|
||||
ctx.stroke();
|
||||
} else if (innerIcon.value == 'line') {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(iconSize * 0.10, iconSize * 0.5466);
|
||||
ctx.lineTo(iconSize * 0.3666, iconSize * 0.8134);
|
||||
ctx.lineTo(iconSize * 0.90, iconSize * 0.28);
|
||||
ctx.stroke();
|
||||
} else if (innerIcon.value == 'dot') {
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, iconSize * 0.22, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
};
|
||||
|
||||
if (isIndeterminate.value) {
|
||||
drawIndeterminate();
|
||||
}else if (isChecked.value) {
|
||||
drawCheckedIcon();
|
||||
}
|
||||
ctx.update();
|
||||
}
|
||||
|
||||
|
||||
const resizeObserver = new UniResizeObserver((entries : Array<UniResizeObserverEntry>) => {
|
||||
update()
|
||||
})
|
||||
|
||||
const stopWatch = watch(():UniElement|null => iconRef.value, (el:UniElement|null) => {
|
||||
if(el== null) return
|
||||
resizeObserver.observe(el)
|
||||
})
|
||||
|
||||
let init = ref(false)
|
||||
// 在list-view里一开始没有尺寸
|
||||
watchEffect(()=>{
|
||||
if(iconRef.value == null) return
|
||||
if(init.value) {
|
||||
update()
|
||||
return
|
||||
}
|
||||
iconRef.value?.getBoundingClientRectAsync()?.then(res=>{
|
||||
requestAnimationFrame(update)
|
||||
init.value = true
|
||||
});
|
||||
})
|
||||
|
||||
onBeforeUnmount(()=>{
|
||||
try {
|
||||
// 鸿蒙在hbx4.66 由导航返回时会报错
|
||||
stopWatch()
|
||||
resizeObserver.disconnect()
|
||||
} catch(err) {
|
||||
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
onBeforeUnmount(()=>{
|
||||
if(manageChildInList != null) {
|
||||
manageChildInList(instance.proxy as LCheckboxComponentPublicInstance, false)
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import './index-u';
|
||||
</style>
|
||||
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<view class="l-checkbox" :class="[rootCasses]" :style="[styles]" @click="handleChange">
|
||||
<slot name="checkbox" :checked="isChecked" :disabled="isDisabled">
|
||||
<slot name="icon" :checked="isChecked" :disabled="isDisabled">
|
||||
<view class="l-checkbox__icon" ref="iconRef" :class="iconClasses" :style="[iconStyle]"></view>
|
||||
</slot>
|
||||
<text class="l-checkbox__label" :style="[labelStyle]" :class="labelClass" v-if="label || $slots['default']">
|
||||
<slot>{{label}}</slot>
|
||||
</text>
|
||||
</slot>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Checkbox 复选框组件
|
||||
* @description 用于在多个选项中进行选择的表单组件,支持单选、全选和不确定态
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-checkbox
|
||||
*
|
||||
* @property {boolean} defaultChecked 默认选中状态(非受控属性)
|
||||
* @property {string} label 显示文本(支持插槽)
|
||||
* @property {boolean} indeterminate 半选状态(优先级高于checked)
|
||||
* @property {boolean} disabled 禁用状态(覆盖Group设置)
|
||||
* @property {'small' | 'medium' | 'large'} size 组件尺寸
|
||||
* @value small
|
||||
* @value medium
|
||||
* @value large
|
||||
* @property {string} name 唯一标识符(表单提交使用)
|
||||
* @property {boolean} checkAll 标记为全选选项(需在Group中使用)
|
||||
* @property {string} value 选项值(Group模式下必填)
|
||||
* @property {'circle' | 'line' | 'rectangle' | 'dot'} icon 图标样式类型
|
||||
* @value circle 圆
|
||||
* @value line 线
|
||||
* @value rectangle 方
|
||||
* @value dot 点
|
||||
* @property {string} fontSize 文本字号(支持CSS单位)
|
||||
* @property {string} iconSize 图标尺寸(支持CSS单位)
|
||||
* @property {string} checkedColor 选中状态主题色
|
||||
* @property {string} iconBgColor 图标背景色
|
||||
* @property {string} iconBorderColor 图标边框色
|
||||
* @property {string} iconDisabledColor 禁用状态图标颜色
|
||||
* @property {string} iconDisabledBgColor 禁用状态背景色
|
||||
* @property {string|object} labelStyle label的样式
|
||||
* @event {Function} change 状态变化时触发(参数:CheckboxChangeOptions)
|
||||
*/
|
||||
import { type ManageChildInList, type CheckboxStatus, type OnCheckedChange, type CheckboxChangeOptions, type ComputedRef } from './type';
|
||||
import { computed, defineComponent, ref, inject, getCurrentInstance, onBeforeUnmount } from '@/uni_modules/lime-shared/vue'
|
||||
import checkboxProps from './props'
|
||||
const name = 'l-checkbox';
|
||||
export default defineComponent({
|
||||
name,
|
||||
props: checkboxProps,
|
||||
emits: ['update:modelValue', 'input', 'change'],
|
||||
setup(props, { emit }) {
|
||||
const instance = getCurrentInstance()!
|
||||
const formDisabled = inject<Ref<boolean|null>|null>('formDisabled', null)
|
||||
const formReadonly = inject<Ref<boolean|null>|null>('formReadonly', null)
|
||||
const checkboxGroup = inject<LCheckboxGroupComponentPublicInstance|null>('limeCheckboxGroup', null);
|
||||
const checkboxGroupValue = inject<ComputedRef<any[]>|null>('limeCheckboxGroupValue', null);
|
||||
const checkboxGroupStatus = inject<ComputedRef<CheckboxStatus>|null>('limeCheckboxGroupStatus', null);
|
||||
const checkboxGroupCheckedSet = inject<ComputedRef<Set<any>>|null>('limeCheckboxGroupCheckedSet', null);
|
||||
const manageChildInList = inject<ManageChildInList|null>('limeCheckboxGroupManageChildInList', null);
|
||||
const onCheckedChange = inject<OnCheckedChange|null>('limeCheckboxGroupOnCheckedChange', null);
|
||||
if(manageChildInList != null) {
|
||||
manageChildInList(instance.proxy as LCheckboxComponentPublicInstance, true)
|
||||
}
|
||||
|
||||
const max = computed(():number => checkboxGroup?.max ?? -1)
|
||||
|
||||
const _innerChecked = ref(props.checked || props.modelValue || props.value || props.defaultChecked)
|
||||
const innerChecked = computed({
|
||||
set(value: boolean) {
|
||||
_innerChecked.value = value
|
||||
emit('update:modelValue', value)
|
||||
emit('change', value)
|
||||
// #ifdef VUE2
|
||||
emit('input', value)
|
||||
// #endif
|
||||
},
|
||||
get():boolean {
|
||||
const value = (props.checked ?? props.modelValue ?? props.value)
|
||||
if(value != null) return value
|
||||
return _innerChecked.value
|
||||
|
||||
// return props.checked || props.modelValue || props.value || _innerChecked.value
|
||||
}
|
||||
} as WritableComputedOptions<boolean>)
|
||||
|
||||
const isChecked = computed(():boolean=>{
|
||||
if (props.checkAll) {
|
||||
const checkAllStatus = checkboxGroupStatus?.value || 'uncheck';
|
||||
return checkAllStatus == 'checked' || checkAllStatus == 'indeterminate'
|
||||
}
|
||||
const value = props.value || props.name;
|
||||
if (checkboxGroupCheckedSet && value) {
|
||||
return checkboxGroupCheckedSet.value.has(value)
|
||||
}
|
||||
return innerChecked.value;
|
||||
})
|
||||
|
||||
const isDisabled = computed(():boolean=>{
|
||||
if(max.value > -1 && checkboxGroupValue) {
|
||||
return max.value <= checkboxGroupValue.value.length && !isChecked.value;
|
||||
}
|
||||
if (props.disabled) return props.disabled;
|
||||
// return checkboxGroup?.disabled || false;
|
||||
return formDisabled?.value || checkboxGroup?.disabled || false;
|
||||
})
|
||||
|
||||
const isReadonly= computed(():boolean=>{
|
||||
if (props.readonly) return props.readonly;
|
||||
return formReadonly?.value ?? checkboxGroup?.readonly ?? false;
|
||||
})
|
||||
|
||||
const isIndeterminate = computed(():boolean=>{
|
||||
if (props.checkAll && checkboxGroupStatus) return checkboxGroupStatus.value == 'indeterminate';
|
||||
return props.indeterminate;
|
||||
})
|
||||
|
||||
const innerIcon = computed(():string => checkboxGroup?.icon || props.icon)
|
||||
const innerSize = computed(():string => checkboxGroup?.size || props.size)
|
||||
const innerIconSize = computed(():string|null => checkboxGroup?.iconSize || props.iconSize)
|
||||
const innerFontSize = computed(():string|null => checkboxGroup?.fontSize || props.fontSize)
|
||||
const innerCheckedColor = computed(():string|null => checkboxGroup?.checkedColor || props.checkedColor)
|
||||
const innerIconBgColor = computed(():string => props.iconBgColor || checkboxGroup?.iconBgColor)
|
||||
const innerIconBorderColor = computed(():string => props.iconBorderColor || checkboxGroup?.iconBorderColor)
|
||||
const innerIconDisabledColor = computed(():string => props.iconDisabledColor || checkboxGroup?.iconDisabledColor)
|
||||
const innerIconDisabledBgColor = computed(():string => props.iconDisabledBgColor || checkboxGroup?.iconDisabledBgColor)
|
||||
|
||||
|
||||
const rootCasses = computed(()=>{
|
||||
const cls = [`${name}--${props.size}`]
|
||||
if(isDisabled.value) {
|
||||
cls.push(`${name}--disabled`)
|
||||
}
|
||||
return cls.join(' ')
|
||||
})
|
||||
|
||||
const iconClasses = computed(()=>{
|
||||
const cls = [`${name}__icon--${props.icon}`]
|
||||
if(isChecked.value) {
|
||||
cls.push(`${name}__icon--checked`)
|
||||
}
|
||||
if(isDisabled.value) {
|
||||
cls.push(`${name}__icon--disabled`)
|
||||
}
|
||||
if(isIndeterminate.value) {
|
||||
cls.push(`${name}__icon--indeterminate`)
|
||||
}
|
||||
return cls.join(' ')
|
||||
})
|
||||
|
||||
const labelClass = computed(()=>{
|
||||
const cls = [];
|
||||
if(isDisabled.value) {
|
||||
cls.push(`${name}__label--disabled`)
|
||||
}
|
||||
return cls.join(' ')
|
||||
})
|
||||
|
||||
const styles = computed(()=>{
|
||||
const style:Record<string, any> = {};
|
||||
if(checkboxGroup && checkboxGroup.gap) {
|
||||
style[checkboxGroup.direction == 'horizontal' ? 'margin-right' : 'margin-bottom'] = checkboxGroup.gap!
|
||||
}
|
||||
if(innerCheckedColor.value) {
|
||||
style['--l-checkbox-icon-checked-color'] = innerCheckedColor.value!
|
||||
}
|
||||
if(innerIconBorderColor.value) {
|
||||
style['--l-checkbox-icon-border-color'] = innerIconBorderColor.value!
|
||||
}
|
||||
if(innerIconDisabledColor.value) {
|
||||
style['--l-checkbox-icon-disabled-color'] = innerIconDisabledColor.value!
|
||||
}
|
||||
if(innerIconDisabledBgColor.value) {
|
||||
style['--l-checkbox-icon-disabled-bg-color'] = innerIconDisabledBgColor.value!
|
||||
}
|
||||
if(innerFontSize.value) {
|
||||
style['--l-checkbox-font-size'] = innerFontSize.value!
|
||||
}
|
||||
return style
|
||||
})
|
||||
|
||||
const iconStyle = computed(()=>{
|
||||
const style:Record<string, any> = {}
|
||||
if(innerIconSize.value) {
|
||||
style['width'] = innerIconSize.value!
|
||||
style['height'] = innerIconSize.value!
|
||||
style['--l-checkbox-icon-size'] = innerIconSize.value!
|
||||
}
|
||||
return style
|
||||
})
|
||||
|
||||
// const labelStyle = computed(()=>{
|
||||
// const style:Record<string, any> = {}
|
||||
// const fontSize = props.fontSize || checkboxGroup?.fontSize
|
||||
// if(fontSize) {
|
||||
// style['font-size'] = fontSize
|
||||
// }
|
||||
// return style
|
||||
// })
|
||||
|
||||
const handleChange = (e: UniPointerEvent) => {
|
||||
if (isDisabled.value || isReadonly.value) return;
|
||||
const value = !isChecked.value;
|
||||
innerChecked.value = value;
|
||||
|
||||
if(onCheckedChange) {
|
||||
onCheckedChange({
|
||||
checked: value,
|
||||
checkAll: props.checkAll,
|
||||
value: props.value || props.name //?? instance.uid
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(()=>{
|
||||
if(manageChildInList) {
|
||||
manageChildInList(instance.proxy as LCheckboxComponentPublicInstance, false)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return {
|
||||
isChecked,
|
||||
isDisabled,
|
||||
rootCasses,
|
||||
iconClasses,
|
||||
labelClass,
|
||||
styles,
|
||||
iconStyle,
|
||||
// labelStyle,
|
||||
handleChange
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import './index-u';
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
// @ts-nocheck
|
||||
export default {
|
||||
checked: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
/** 是否选中,非受控属性 */
|
||||
defaultChecked: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 是否禁用组件。如果父组件存在 CheckboxGroup,默认值由 CheckboxGroup.disabled 控制。Checkbox.disabled 优先级高于 CheckboxGroup.disabled */
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 主文案 */
|
||||
label: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
indeterminate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
/** 用于标识是否为「全选选项」。单独使用无效,需在 CheckboxGroup 中使用 */
|
||||
checkAll: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/** 多选框的值 */
|
||||
value: {
|
||||
type: [String, Number, Boolean],
|
||||
default: null
|
||||
},
|
||||
checkedColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconBgColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconBorderColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconDisabledColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
iconDisabledBgColor: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'rectangle'
|
||||
}, //?: 'circle' | 'line' | 'dot';
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium'
|
||||
}, //?: 'small' | 'medium' | 'large';
|
||||
iconSize: {
|
||||
type: String,
|
||||
defalut: null
|
||||
},
|
||||
fontSize: {
|
||||
type: String,
|
||||
defalut: null
|
||||
},
|
||||
labelStyle: {
|
||||
type: [String, Object]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// @ts-nocheck
|
||||
export type CheckboxProps = {
|
||||
/**
|
||||
* 是否选中
|
||||
*/
|
||||
// checked ?: boolean;
|
||||
/**
|
||||
* 是否选中,非受控属性
|
||||
*/
|
||||
defaultChecked: boolean;
|
||||
/**
|
||||
* 是否选中
|
||||
*/
|
||||
// modelValue ?: boolean;
|
||||
/**
|
||||
* 主文案
|
||||
*/
|
||||
label ?: string;
|
||||
/**
|
||||
* 是否为半选
|
||||
*/
|
||||
indeterminate: boolean;
|
||||
/**
|
||||
* 是否禁用组件。如果父组件存在 CheckboxGroup,默认值由 CheckboxGroup.disabled 控制。Checkbox.disabled 优先级高于 CheckboxGroup.disabled
|
||||
*/
|
||||
disabled : boolean;
|
||||
/**
|
||||
* 只读
|
||||
*/
|
||||
readonly : boolean;
|
||||
size : 'small' | 'medium' | 'large'
|
||||
/**
|
||||
* 标识符,通常为一个唯一的字符串或数字
|
||||
*/
|
||||
name ?: any //string,
|
||||
/**
|
||||
* 用于标识是否为「全选选项」。单独使用无效,需在 CheckboxGroup 中使用
|
||||
*/
|
||||
checkAll : boolean
|
||||
/**
|
||||
* 多选框的值
|
||||
*/
|
||||
value?: any; // string | number
|
||||
icon: 'circle' | 'line' | 'rectangle' | 'dot';
|
||||
|
||||
fontSize?: string;
|
||||
iconSize?: string;
|
||||
checkedColor?: string;
|
||||
iconBgColor?: string;
|
||||
iconBorderColor?: string;
|
||||
iconDisabledColor?: string;
|
||||
iconDisabledBgColor?: string;
|
||||
|
||||
labelStyle?: string | UTSJSONObject
|
||||
}
|
||||
|
||||
export type ManageChildInList = (child: LCheckboxComponentPublicInstance, shouldAdd: boolean) => void;
|
||||
export type CheckboxStatus = 'checked' | 'uncheck' | 'indeterminate';
|
||||
export type CheckboxChangeOptions = {
|
||||
checked : boolean,
|
||||
checkAll : boolean,
|
||||
value?: any
|
||||
};
|
||||
|
||||
export type OnCheckedChange = (options: CheckboxChangeOptions) => void
|
||||
// #ifndef APP-ANDROID || APP-IOS
|
||||
export type {ComputedRef} from 'vue';
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID || APP-IOS
|
||||
export type ComputedRef<T> = ComputedRefImpl<T>;
|
||||
// export type WritableComputedRef<T> = ComputedRefImpl<T>;
|
||||
// #endif
|
||||
109
qiming-mobile/uni_modules/lime-checkbox/package.json
Normal file
109
qiming-mobile/uni_modules/lime-checkbox/package.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"id": "lime-checkbox",
|
||||
"displayName": "lime-checkbox 复选框",
|
||||
"version": "0.1.0",
|
||||
"description": "lime-checkbox 复选框,支持v-model双向数据绑定,支持自定义样式,支持半选,兼容uniapp/uniappx",
|
||||
"keywords": [
|
||||
"lime-checkbox",
|
||||
"checkbox",
|
||||
"复选框",
|
||||
"半选"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.34",
|
||||
"uni-app": "^4.73",
|
||||
"uni-app-x": "^4.75"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "√",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"lime-shared",
|
||||
"lime-style"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "√"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "√",
|
||||
"vue3": "√"
|
||||
},
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"vue": "√",
|
||||
"nvue": "-",
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√",
|
||||
"alipay": "-",
|
||||
"toutiao": "-",
|
||||
"baidu": "-",
|
||||
"kuaishou": "-",
|
||||
"jd": "-",
|
||||
"harmony": "-",
|
||||
"qq": "-",
|
||||
"lark": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "-",
|
||||
"union": "-"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
258
qiming-mobile/uni_modules/lime-checkbox/readme_old.md
Normal file
258
qiming-mobile/uni_modules/lime-checkbox/readme_old.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# lime-checkbox 复选框
|
||||
复选框,支持v-model双向数据绑定,支持自定义样式,兼容uniapp/uniappx
|
||||
|
||||
## 文档
|
||||
🚀 [checkbox【站点1】](https://limex.qcoon.cn/components/checkbox.html)
|
||||
🌍 [checkbox【站点2】](https://limeui.netlify.app/components/checkbox.html)
|
||||
🔥 [checkbox【站点3】](https://limeui.familyzone.top/components/checkbox.html)
|
||||
|
||||
|
||||
|
||||
## 安装
|
||||
插件市场导入即可,首次导入可能需要重新编译
|
||||
|
||||
## 代码演示
|
||||
### 基础演示
|
||||
通过 `v-model` 绑定复选框的勾选状态。
|
||||
```html
|
||||
<l-checkbox v-model="checked">复选框</l-checkbox>
|
||||
```
|
||||
```js
|
||||
const checked = ref(false)
|
||||
```
|
||||
|
||||
|
||||
### 选项组
|
||||
通过 v-model 绑定值当前选中项的 `value`或`name`。
|
||||
```html
|
||||
<l-checkbox-group v-model="checked" @change="onChange">
|
||||
<l-checkbox value="Beijing" label="北京" />
|
||||
<l-checkbox value="Shanghai" label="上海" />
|
||||
<l-checkbox value="Guangzhou" label="广州" />
|
||||
<l-checkbox value="Shenzen" label="深圳" />
|
||||
</l-checkbox-group>
|
||||
```
|
||||
```js
|
||||
const checked = ref(['Beijing']);
|
||||
|
||||
const onChange = (e: string[]) => {
|
||||
console.log('onChange', e)
|
||||
}
|
||||
```
|
||||
|
||||
### 禁用
|
||||
通过 `disabled` 属性禁止选项切换,在 checkbox 上设置 `disabled` 可以禁用单个选项。
|
||||
```html
|
||||
<l-checkbox-group v-model="checked" disabled>
|
||||
<l-checkbox value="Beijing" label="北京" />
|
||||
<l-checkbox value="Shanghai" label="上海" />
|
||||
<l-checkbox value="Guangzhou" label="广州" />
|
||||
<l-checkbox value="Shenzen" label="深圳" />
|
||||
</l-checkbox-group>
|
||||
```
|
||||
|
||||
### 样式
|
||||
`icon` 属性可选值为`circle`(圆) `line`(线) `dot`(点),复选框形状。
|
||||
```html
|
||||
<l-checkbox-group icon="dot">
|
||||
<l-checkbox value="Beijing" label="北京" />
|
||||
<l-checkbox value="Guangzhou" label="广州" />
|
||||
<l-checkbox value="Shenzen" label="深圳" />
|
||||
</l-checkbox-group>
|
||||
<l-checkbox-group icon="circle">
|
||||
<l-checkbox value="Beijing" label="北京" />
|
||||
<l-checkbox value="Guangzhou" label="广州" />
|
||||
<l-checkbox value="Shenzen" label="深圳" />
|
||||
</l-checkbox-group>
|
||||
<l-checkbox-group icon="line">
|
||||
<l-checkbox value="Beijing" label="北京" />
|
||||
<l-checkbox value="Guangzhou" label="广州" />
|
||||
<l-checkbox value="Shenzen" label="深圳" />
|
||||
</l-checkbox-group>
|
||||
```
|
||||
|
||||
### 自定义颜色
|
||||
通过 `checked-color` 属性设置选中状态的图标颜色。
|
||||
```html
|
||||
<l-checkbox-group checked-color="#ee0a24">
|
||||
<l-checkbox value="Beijing" label="北京" />
|
||||
<l-checkbox value="Guangzhou" label="广州" />
|
||||
<l-checkbox value="Shenzen" label="深圳" />
|
||||
</l-checkbox-group>
|
||||
```
|
||||
|
||||
### 自定义大小
|
||||
通过 `icon-size` 属性可以自定义图标的大小。
|
||||
```html
|
||||
<l-checkbox-group>
|
||||
<l-checkbox icon-size="44px" value="Beijing" label="北京" />
|
||||
<l-checkbox icon-size="34px" value="Guangzhou" label="广州" />
|
||||
<l-checkbox icon-size="24px" value="Shenzen" label="深圳" />
|
||||
</l-checkbox-group>
|
||||
```
|
||||
|
||||
### 自定义图标
|
||||
|
||||
通过 `icon` 插槽自定义图标,并通过 `slotProps` 判断是否为选中状态。
|
||||
```html
|
||||
<l-checkbox-group>
|
||||
<l-checkbox value="Beijing" label="北京">
|
||||
<template #icon="{checked}">
|
||||
<image v-show="checked" style="width:20px; height:20px" src="https://fastly.jsdelivr.net/npm/@vant/assets/user-active.png"></image>
|
||||
<image v-show="!checked" style="width:20px; height:20px" src="https://fastly.jsdelivr.net/npm/@vant/assets/user-inactive.png"></image>
|
||||
</template>
|
||||
</l-checkbox>
|
||||
<l-checkbox value="Guangzhou" label="广州">
|
||||
<template #icon="{checked}">
|
||||
<image v-show="checked" style="width:20px; height:20px" src="https://fastly.jsdelivr.net/npm/@vant/assets/user-active.png"></image>
|
||||
<image v-show="!checked" style="width:20px; height:20px" src="https://fastly.jsdelivr.net/npm/@vant/assets/user-inactive.png"></image>
|
||||
</template>
|
||||
</l-checkbox>
|
||||
<l-checkbox value="Shenzen" label="深圳">
|
||||
<template #icon="{checked}">
|
||||
<image v-show="checked" style="width:20px; height:20px" src="https://fastly.jsdelivr.net/npm/@vant/assets/user-active.png"></image>
|
||||
<image v-show="!checked" style="width:20px; height:20px" src="https://fastly.jsdelivr.net/npm/@vant/assets/user-inactive.png"></image>
|
||||
</template>
|
||||
</l-checkbox>
|
||||
</l-checkbox-group>
|
||||
```
|
||||
|
||||
|
||||
### 限制最大可选数
|
||||
通过 `max` 属性可以限制复选框组的最大可选数。
|
||||
```html
|
||||
<l-checkbox-group :max="3">
|
||||
<l-checkbox value="Beijing" label="北京" />
|
||||
<l-checkbox value="Shanghai" label="上海" />
|
||||
<l-checkbox value="Guangzhou" label="广州" />
|
||||
<l-checkbox value="Shenzen" label="深圳" />
|
||||
</l-checkbox-group>
|
||||
```
|
||||
|
||||
### 全选与反选
|
||||
通过 `CheckboxGroup` 实例上的 `toggleAll` 方法可以实现全选与反选。
|
||||
```html
|
||||
<l-checkbox-group ref="checkboxGroupRef" @change="onChange" direction="vertical">
|
||||
<l-checkbox value="all" checkAll label="全选" />
|
||||
<l-checkbox value="Beijing" label="北京" />
|
||||
<l-checkbox value="Shanghai" label="上海" />
|
||||
<l-checkbox value="Guangzhou" label="广州" />
|
||||
<l-checkbox value="Shenzen" label="深圳" />
|
||||
</l-checkbox-group>
|
||||
<button type="primary" @click="checkAll">全选</button>
|
||||
<button @click="toggleAll">反选</button>
|
||||
```
|
||||
```js
|
||||
const checkboxGroupRef = ref<LCheckboxGroupComponentPublicInstance|null>(null);
|
||||
const onChange = (e: string[]) => {
|
||||
console.log('onChange', e)
|
||||
}
|
||||
const checkAll = () => {
|
||||
if(checkboxGroupRef.value == null) return
|
||||
checkboxGroupRef.value!.toggleAll(true)
|
||||
}
|
||||
const toggleAll = () => {
|
||||
if(checkboxGroupRef.value == null) return
|
||||
checkboxGroupRef.value!.toggleAll(false)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 插件标签
|
||||
`l-checkbox` 为 component
|
||||
`lime-checkbox` 为 demo
|
||||
|
||||
### Vue2使用
|
||||
插件使用了`composition-api`, 如果你希望在vue2中使用请按官方的教程[vue-composition-api](https://uniapp.dcloud.net.cn/tutorial/vue-composition-api.html)配置
|
||||
关键代码是: 在main.js中 在vue2部分加上这一段即可
|
||||
|
||||
```js
|
||||
// vue2
|
||||
import Vue from 'vue'
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
Vue.use(VueCompositionAPI)
|
||||
```
|
||||
|
||||
|
||||
## API
|
||||
|
||||
### Checkbox Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| name | 标识符,通常为一个唯一的字符串或数字 | _string\|number_ | - |
|
||||
| value | 复选按钮的值 | _any_ | - |
|
||||
| v-model | 是否选中 | _any_ | - |
|
||||
| indeterminate | 是否为半选 | _boolean_ | `false` |
|
||||
| checked | 是否选中 | _boolean_ | `false` |
|
||||
| disabled | 是否为禁用态 | _boolean_ | `false` |
|
||||
| readonly | 是否为只读 | _boolean_ | `false` |
|
||||
| checkAll | 用于标识是否为「全选选项」。复独使用无效 | _boolean_ | `false` |
|
||||
| icon | 自定义选中图标和非选中图标可选值`'rectangle' | 'circle' | 'line' | 'dot'` | _string_ | `rectangle` |
|
||||
| label | 主文案 | _string_ | `` |
|
||||
| fontSize | 文本大小 | _string_ | `` |
|
||||
| iconSize | 图标大小 | _string_ | `` |
|
||||
| checkedColor | 选中状态颜色 | _string_ | `` |
|
||||
| iconBgColor | 图标背景颜色 | _string_ | `` |
|
||||
| iconBorderColor | 图标描边颜色 | _string_ | `` |
|
||||
| iconDisabledColor | 图标禁用颜色 | _string_ | `` |
|
||||
| iconDisabledBgColor | 图标禁用背景颜色 | _string_ | `` |
|
||||
| labelStyle | label的样式 | _string\|object | `` |
|
||||
|
||||
### CheckboxGroup Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| v-model | 标识符,通常为一个唯一的字符串或数字 | _string\|number[]_ | - |
|
||||
| name | 标识符,通常为一个唯一的字符串或数字 | _string\|number_ | - |
|
||||
| value | 复选按钮的值 | _any[]_ | - |
|
||||
| indeterminate | 是否为半选 | _boolean_ | `false` |
|
||||
| disabled | 是否为禁用态 | _boolean_ | `false` |
|
||||
| direction | 排列方向,可选值为`vertical` | _string_ | `horizontal` |
|
||||
| icon | 自定义选中图标和非选中图标可选值`'rectangle' | 'circle' | 'line' | 'dot'` | _string_ | `rectangle` |
|
||||
| fontSize | 文本大小 | _string_ | `` |
|
||||
| iconSize | 图标大小 | _string_ | `` |
|
||||
| checkedColor | 选中状态颜色 | _string_ | `` |
|
||||
| iconBgColor | 图标背景颜色 | _string_ | `` |
|
||||
| iconBorderColor | 图标描边颜色 | _string_ | `` |
|
||||
| iconDisabledColor | 图标禁用颜色 | _string_ | `` |
|
||||
| iconDisabledBgColor | 图标禁用背景颜色 | _string_ | `` |
|
||||
|
||||
### Events
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
| ------ | ------------------------ | ---------------------- |
|
||||
| change | 当绑定值变化时触发的事件 | _currentValue: any_ |
|
||||
|
||||
|
||||
### Radio Slots
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
| ------ | ------------------------ | ---------------------- |
|
||||
| default | 自定义文本 | _{ checked: boolean, disabled: boolean }_ |
|
||||
| icon | 自定义图标 | _{ checked: boolean, disabled: boolean }_ |
|
||||
|
||||
|
||||
## 主题定制
|
||||
|
||||
### 样式变量
|
||||
|
||||
组件提供了下列 CSS 变量,可用于自定义样式,uvue app无效。
|
||||
|
||||
| 名称 | 默认值 | 描述 |
|
||||
| ------------------------ | -------------------- | ---- |
|
||||
| --l-checkbox-icon-size | _40rpx_ | - |
|
||||
| --l-checkbox-font-size | _32rpx_ | - |
|
||||
| --l-checkbox-icon-bg-color | _white_ | - |
|
||||
| --l-checkbox-border-icon-color | _$gray-5_ | - |
|
||||
| --l-checkbox-icon-disabled-color | _$gray-5_ | - |
|
||||
| --l-checkbox-icon-disabled-bg-color | _$gray-1_ | - |
|
||||
| --l-checkbox-icon-checked-color | _$primary-color_ | - |
|
||||
|
||||
|
||||
## 打赏
|
||||
|
||||
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
|
||||

|
||||

|
||||
681
qiming-mobile/uni_modules/lime-color/common/color.uts
Normal file
681
qiming-mobile/uni_modules/lime-color/common/color.uts
Normal file
@@ -0,0 +1,681 @@
|
||||
// @ts-nocheck
|
||||
import { numberInputToObject, rgbaToHex, rgbToHex, rgbToHsl, rgbToHsv } from './conversion';
|
||||
import { names } from './css-color-names';
|
||||
import { inputToRGB } from './format-input';
|
||||
import { HSL, HSLA, HSV, HSVA, HSBA, RGB, RGBA, RGBAString, LColorInfo, LColorFormats, LColorOptions, LColorInput } from '../utssdk/interface.uts';
|
||||
import { bound01, boundAlpha, clamp01, toBoolean, isNumber } from './util';
|
||||
|
||||
export class TinyColor {
|
||||
r : number;
|
||||
g : number;
|
||||
b : number;
|
||||
a : number;
|
||||
/** 用于创建 limeColor 实例的格式 */
|
||||
format ?: LColorFormats;
|
||||
/** 传递给构造函数以创建 limeColor 实例的输入 */
|
||||
originalInput : LColorInput;
|
||||
/** 颜色已被成功解析 */
|
||||
isValid : boolean;
|
||||
|
||||
gradientType ?: string;
|
||||
|
||||
/** rounded alpha */
|
||||
roundA : number;
|
||||
// #ifdef APP
|
||||
reversedNames : Map<string, string>;
|
||||
// #endif
|
||||
constructor(color : LColorInput = '', opts : LColorOptions = {} as LColorOptions) {
|
||||
let _color : any = color
|
||||
// if(color instanceof TinyColor){
|
||||
// return color as TinyColor
|
||||
// }
|
||||
if (isNumber(color)) {
|
||||
_color = numberInputToObject(color as number);
|
||||
}
|
||||
this.originalInput = _color;
|
||||
const rgb = inputToRGB(_color);
|
||||
this.r = rgb.r;
|
||||
this.g = rgb.g;
|
||||
this.b = rgb.b;
|
||||
this.a = rgb.a;
|
||||
this.roundA = Math.round(100 * this.a) / 100;
|
||||
this.format = opts.format ?? rgb.format;
|
||||
this.gradientType = opts.gradientType;
|
||||
|
||||
// 不要让范围在 [0,255] 中的值返回成 [0,1]。
|
||||
// 这里可能会失去一些精度,但可以解决原来
|
||||
// .5 被解释为总数的半数,而不是1的一半的问题
|
||||
// 如果本来应该是128,那么这个已经在 inputToRgb 中处理过了 if (this.r < 1) {
|
||||
if (this.r < 1) {
|
||||
this.r = Math.round(this.r);
|
||||
}
|
||||
|
||||
if (this.g < 1) {
|
||||
this.g = Math.round(this.g);
|
||||
}
|
||||
|
||||
if (this.b < 1) {
|
||||
this.b = Math.round(this.b);
|
||||
}
|
||||
|
||||
this.isValid = rgb.ok ?? false;
|
||||
|
||||
// #ifdef APP
|
||||
this.reversedNames = new Map<string, string>()
|
||||
names.forEach((value : string, key : string) => {
|
||||
this.reversedNames.set(value, key)
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
/**
|
||||
* 判断当前颜色是否为暗色。
|
||||
* @returns 一个布尔值,表示当前颜色是否为暗色。
|
||||
*/
|
||||
isDark() : boolean {
|
||||
return this.getBrightness() < 128;
|
||||
}
|
||||
/**
|
||||
* 判断当前颜色是否为亮色。
|
||||
* @returns 一个布尔值,表示当前颜色是否为亮色。
|
||||
*/
|
||||
isLight() : boolean {
|
||||
return !this.isDark();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算当前颜色的亮度值。
|
||||
* 亮度值是根据 RGB 颜色空间中的红、绿、蓝三个通道的值计算得出的,计算公式为:(r * 299 + g * 587 + b * 114) / 1000。
|
||||
* @returns 返回颜色的感知亮度,范围从0-255。
|
||||
*/
|
||||
getBrightness() : number {
|
||||
// http://www.w3.org/TR/AERT#color-contrast
|
||||
const rgb = this.toRgb();
|
||||
return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
|
||||
}
|
||||
/**
|
||||
* 计算当前颜色的相对亮度值。
|
||||
* 相对亮度值是根据 RGB 颜色空间中的红、绿、蓝三个通道的值计算得出的,计算公式为:0.2126 * R + 0.7152 * G + 0.0722 * B。
|
||||
* @returns 返回颜色的感知亮度,范围从0-1。
|
||||
*/
|
||||
getLuminance() : number {
|
||||
// http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
|
||||
const rgb = this.toRgb();
|
||||
let R : number;
|
||||
let G : number;
|
||||
let B : number;
|
||||
const RsRGB : number = rgb.r / 255;
|
||||
const GsRGB : number = rgb.g / 255;
|
||||
const BsRGB : number = rgb.b / 255;
|
||||
|
||||
if (RsRGB <= 0.03928) {
|
||||
R = RsRGB / 12.92;
|
||||
} else {
|
||||
// eslint-disable-next-line prefer-exponentiation-operator
|
||||
R = Math.pow((RsRGB + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
if (GsRGB <= 0.03928) {
|
||||
G = GsRGB / 12.92;
|
||||
} else {
|
||||
// eslint-disable-next-line prefer-exponentiation-operator
|
||||
G = Math.pow((GsRGB + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
if (BsRGB <= 0.03928) {
|
||||
B = BsRGB / 12.92;
|
||||
} else {
|
||||
// eslint-disable-next-line prefer-exponentiation-operator
|
||||
B = Math.pow((BsRGB + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前颜色的透明度值。
|
||||
* 透明度值的范围是 0 到 1,其中 0 表示完全透明,1 表示完全不透明。
|
||||
* @returns 一个数字,表示当前颜色的透明度值。
|
||||
*/
|
||||
getAlpha() : number {
|
||||
return this.a;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前颜色的透明度值。
|
||||
* @param alpha - 要设置的透明度值。透明度值的范围是 0 到 1,其中 0 表示完全透明,1 表示完全不透明。
|
||||
* @returns 一个 `TinyColor` 对象,表示设置透明度后的颜色。
|
||||
*/
|
||||
setAlpha(alpha ?: string) : TinyColor
|
||||
setAlpha(alpha ?: number) : TinyColor
|
||||
setAlpha(alpha ?: any) : TinyColor {
|
||||
this.a = boundAlpha(alpha);
|
||||
this.roundA = Math.round(100 * this.a) / 100;
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* 判断当前颜色是否为单色。
|
||||
* 单色是指颜色的饱和度(S)为 0 的颜色,这些颜色只有明度(L)变化,没有颜色变化。
|
||||
* @returns 一个布尔值,表示当前颜色是否为单色。
|
||||
*/
|
||||
isMonochrome() : boolean {
|
||||
const { s } = this.toHsl();
|
||||
return s == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前颜色转换为 HSV(色相、饱和度、亮度)颜色空间。
|
||||
* @returns 一个对象,包含四个属性:`h`(色相)、`s`(饱和度)、`v`(亮度)和 `a`(透明度)。
|
||||
*/
|
||||
toHsv() : HSVA {
|
||||
const hsv = rgbToHsv(this.r, this.g, this.b);
|
||||
|
||||
return { h: Math.round(hsv.h * 360), s: hsv.s, v: hsv.v, a: this.a } as HSVA;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前颜色转换为 HSV(色相、饱和度、亮度)颜色空间的字符串表示。
|
||||
* @returns 一个字符串,表示当前颜色的 HSV 或 HSVA 格式 hsva(xxx, xxx, xxx, xx)。
|
||||
*/
|
||||
toHsvString() : string {
|
||||
const hsv = rgbToHsv(this.r, this.g, this.b);
|
||||
const h = Math.round(hsv.h * 360);
|
||||
const s = Math.round(hsv.s * 100);
|
||||
const v = Math.round(hsv.v * 100);
|
||||
return this.a == 1 ? `hsv(${h}, ${s}%, ${v}%)` : `hsva(${h}, ${s}%, ${v}%, ${this.roundA})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前颜色对象转换为HSBA颜色空间,即Hue(色相)、Saturation(饱和度)、Brightness(亮度)和Alpha(透明度
|
||||
* @returns {HSBA} 返回一个HSBA对象,表示当前颜色对象在HSBA颜色空间中的值
|
||||
*/
|
||||
toHsb() : HSBA {
|
||||
const hsv = rgbToHsv(this.r, this.g, this.b);
|
||||
return { h: Math.round(hsv.h * 360), s: hsv.s, b: hsv.v, a: this.a } as HSBA;
|
||||
}
|
||||
/**
|
||||
* 将当前颜色对象转换为CSS风格的HSB或HSVA字符串
|
||||
* @returns {string} 返回一个CSS风格的HSB或HSVA字符串,表示当前颜色对象的颜色值
|
||||
*/
|
||||
toHsbString() : string {
|
||||
const hsb = this.toHsb();
|
||||
const h = Math.round(hsb.h);
|
||||
const s = Math.round(hsb.s * 100);
|
||||
const b = Math.round(hsb.b * 100);
|
||||
return this.a == 1
|
||||
? `hsb(${h}, ${s}%, ${b}%)`
|
||||
: `hsba(${h}, ${s}%, ${b}%, ${this.roundA})`;
|
||||
}
|
||||
/**
|
||||
* 将当前颜色转换为 HSL(色相、饱和度、明度)颜色空间。
|
||||
* @returns 一个对象,包含四个属性:`h`(色相)、`s`(饱和度)、`l`(明度)和 `a`(透明度)。
|
||||
*/
|
||||
toHsl() : HSLA {
|
||||
const hsl = rgbToHsl(this.r, this.g, this.b);
|
||||
return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this.a } as HSLA;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前颜色转换为 HSL(色相、饱和度、明度)颜色空间的字符串表示。
|
||||
* @returns 一个字符串,表示当前颜色的 HSL 或 HSLA 格式 hsla(xxx, xxx, xxx, xx)。
|
||||
*/
|
||||
toHslString() : string {
|
||||
const hsl = rgbToHsl(this.r, this.g, this.b);
|
||||
const h = Math.round(hsl.h * 360);
|
||||
const s = Math.round(hsl.s * 100);
|
||||
const l = Math.round(hsl.l * 100);
|
||||
return this.a == 1 ? `hsl(${h}, ${s}%, ${l}%)` : `hsla(${h}, ${s}%, ${l}%, ${this.roundA})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前颜色转换为十六进制颜色表示。
|
||||
* @param allow3Char 是否允许返回简写的十六进制颜色表示(如果可能)。默认值为 `false`。
|
||||
* @returns 一个字符串,表示当前颜色的十六进制格式。
|
||||
*/
|
||||
toHex(allow3Char = false) : string {
|
||||
return rgbToHex(this.r, this.g, this.b, allow3Char);
|
||||
}
|
||||
/**
|
||||
* 将当前颜色转换为带有井号(`#`)前缀的十六进制颜色表示。
|
||||
* @param allow3Char 是否允许返回简写的十六进制颜色表示(如果可能)。默认值为 `false`。
|
||||
* @returns 一个字符串,表示当前颜色的带有井号前缀的十六进制格式。
|
||||
*/
|
||||
toHexString(allow3Char = false) : string {
|
||||
return '#' + this.toHex(allow3Char);
|
||||
}
|
||||
/**
|
||||
* 返回颜色的八位十六进制值.
|
||||
* @param allow4Char 如果可能的话,将十六进制值缩短为4个字符
|
||||
*/
|
||||
toHex8(allow4Char = false) : string {
|
||||
return rgbaToHex(this.r, this.g, this.b, this.a, allow4Char);
|
||||
}
|
||||
/**
|
||||
* 返回颜色的八位十六进制值,并且值前面带有#符号.
|
||||
* @param allow4Char 如果可能的话,将十六进制值缩短为4个字符
|
||||
*/
|
||||
toHex8String(allow4Char = false) : string {
|
||||
return '#' + this.toHex8(allow4Char);
|
||||
}
|
||||
/**
|
||||
* 根据颜色的透明度(Alpha值)返回较短的十六进制值,并且值前面带有#符号。
|
||||
* @param allowShortChar 如果可能的话,将十六进制值缩短至3个或4个字符
|
||||
*/
|
||||
toHexShortString(allowShortChar = false) : string {
|
||||
return this.a == 1 ? this.toHexString(allowShortChar) : this.toHex8String(allowShortChar);
|
||||
}
|
||||
/**
|
||||
* 将当前颜色转换为 RGB(红、绿、蓝)颜色空间的对象表示。
|
||||
* @returns 一个包含 `r`、`g`、`b` 和 `a` 属性的对象,表示当前颜色的 RGB 格式。
|
||||
*/
|
||||
toRgb() : RGBA {
|
||||
return {
|
||||
r: Math.round(this.r),
|
||||
g: Math.round(this.g),
|
||||
b: Math.round(this.b),
|
||||
a: this.a,
|
||||
} as RGBA;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将当前颜色对象转换为CSS风格的RGB或RGBA字符串
|
||||
* @returns {string} 返回一个CSS风格的RGB或RGBA字符串,表示当前颜色对象的颜色值
|
||||
*/
|
||||
toRgbString() : string {
|
||||
const r = Math.round(this.r);
|
||||
const g = Math.round(this.g);
|
||||
const b = Math.round(this.b);
|
||||
return this.a == 1 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${this.roundA})`;
|
||||
}
|
||||
/**
|
||||
* 将当前颜色转换为百分比表示的 RGB(红、绿、蓝)颜色空间的对象表示。
|
||||
* @returns 一个包含 `r`、`g`、`b` 和 `a` 属性的对象,表示当前颜色的百分比表示的 RGB 格式。
|
||||
*/
|
||||
toPercentageRgb() : RGBAString {
|
||||
// 定义一个格式化函数,将颜色值转换为百分比表示
|
||||
const fmt = (x : number) : string => `${Math.round(bound01(x, 255) * 100)}%`;
|
||||
// 返回一个RGBA对象,其中颜色值已转换为百分比表示
|
||||
return {
|
||||
r: fmt(this.r),
|
||||
g: fmt(this.g),
|
||||
b: fmt(this.b),
|
||||
a: this.a,
|
||||
} as RGBAString;
|
||||
}
|
||||
/**
|
||||
* 将RGBA相对值插值为一个字符串,颜色值以百分比表示。
|
||||
*/
|
||||
toPercentageRgbString() : string {
|
||||
// 定义一个四舍五入函数,将颜色值转换为百分比表示的整数
|
||||
const rnd = (x : number) : number => Math.round(bound01(x, 255) * 100);
|
||||
// 根据alpha值返回不同的字符串表示
|
||||
return this.a == 1
|
||||
? `rgb(${rnd(this.r)}%, ${rnd(this.g)}%, ${rnd(this.b)}%)`
|
||||
: `rgba(${rnd(this.r)}%, ${rnd(this.g)}%, ${rnd(this.b)}%, ${this.roundA})`;
|
||||
}
|
||||
/**
|
||||
* 返回这个颜色的'真实'名称,不存在返回null
|
||||
*/
|
||||
toName() : string | null {
|
||||
if (this.a == 0) {
|
||||
return 'transparent';
|
||||
}
|
||||
|
||||
if (this.a < 1) {
|
||||
return null;
|
||||
}
|
||||
const hex = this.toHexString()//'#' + rgbToHex(this.r, this.g, this.b, false);
|
||||
|
||||
// #ifndef APP
|
||||
const _names = Array.from(names.entries())
|
||||
for (let [key, value] of _names) {
|
||||
if (hex == value) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
// #endif
|
||||
|
||||
// #ifdef APP
|
||||
return this.reversedNames.get(hex)
|
||||
// #endif
|
||||
}
|
||||
/**
|
||||
* 将颜色转换为字符串表示。
|
||||
*
|
||||
* @param format - 用于显示字符串表示的格式。
|
||||
*/
|
||||
// toString<T extends 'name'>(format : T) : string;
|
||||
// toString<T extends LColorFormats>(format ?: T) : string;
|
||||
|
||||
// #ifdef APP-ANDROID
|
||||
override toString() : string {
|
||||
return this.toString(null)
|
||||
}
|
||||
// #endif
|
||||
toString(format ?: LColorFormats) : string {
|
||||
const formatSet = toBoolean(format);
|
||||
let _format = format ?? this.format;
|
||||
|
||||
let formattedString : string | null = null;
|
||||
const hasAlpha = this.a < 1 && this.a >= 0;
|
||||
const needsAlphaFormat = !formatSet && hasAlpha && (_format != null && _format.startsWith('hex') || _format == 'name');
|
||||
|
||||
if (needsAlphaFormat) {
|
||||
// 特殊情况:透明度,所有其他非透明度格式都会在有透明度时返回rgba。
|
||||
// 当透明度为0时,返回"transparent"。
|
||||
if (_format == 'name' && this.a == 0) {
|
||||
return this.toName() ?? 'transparent';
|
||||
}
|
||||
return this.toRgbString();
|
||||
}
|
||||
|
||||
if (_format == 'rgb') {
|
||||
formattedString = this.toRgbString();
|
||||
}
|
||||
|
||||
if (_format == 'prgb') {
|
||||
formattedString = this.toPercentageRgbString();
|
||||
}
|
||||
|
||||
if (_format == 'hex' || _format == 'hex6') {
|
||||
formattedString = this.toHexString();
|
||||
}
|
||||
|
||||
if (_format == 'hex3') {
|
||||
formattedString = this.toHexString(true);
|
||||
}
|
||||
|
||||
if (_format == 'hex4') {
|
||||
formattedString = this.toHex8String(true);
|
||||
}
|
||||
|
||||
if (_format == 'hex8') {
|
||||
formattedString = this.toHex8String();
|
||||
}
|
||||
|
||||
if (_format == 'name') {
|
||||
formattedString = this.toName();
|
||||
}
|
||||
|
||||
if (_format == 'hsl') {
|
||||
formattedString = this.toHslString();
|
||||
}
|
||||
|
||||
if (_format == 'hsv') {
|
||||
formattedString = this.toHsvString();
|
||||
}
|
||||
if (_format == 'hsb') {
|
||||
formattedString = this.toHsbString();
|
||||
}
|
||||
return formattedString ?? this.toHexString();
|
||||
}
|
||||
toNumber() : number {
|
||||
return (Math.round(this.r) << 16) + (Math.round(this.g) << 8) + Math.round(this.b);
|
||||
}
|
||||
clone() : TinyColor {
|
||||
return new TinyColor(this.toString());
|
||||
}
|
||||
/**
|
||||
* 将颜色变浅指定的量。提供100将始终返回白色。
|
||||
* @param amount - 有效值介于1-100之间
|
||||
*/
|
||||
lighten(amount = 10) : TinyColor {
|
||||
const hsl = this.toHsl();
|
||||
hsl.l += amount / 100;
|
||||
hsl.l = clamp01(hsl.l);
|
||||
return new TinyColor(hsl, { format: this.format } as LColorOptions);
|
||||
}
|
||||
/**
|
||||
* 将颜色变亮一定的量,范围从0到100。
|
||||
* @param amount - 有效值在1-100之间
|
||||
*/
|
||||
brighten(amount = 10) : TinyColor {
|
||||
const rgb = this.toRgb();
|
||||
rgb.r = Math.max(0, Math.min(255, rgb.r - Math.round(255 * -(amount / 100))));
|
||||
rgb.g = Math.max(0, Math.min(255, rgb.g - Math.round(255 * -(amount / 100))));
|
||||
rgb.b = Math.max(0, Math.min(255, rgb.b - Math.round(255 * -(amount / 100))));
|
||||
return new TinyColor(rgb, { format: this.format } as LColorOptions);
|
||||
}
|
||||
/**
|
||||
* 将颜色变暗一定的量,范围从0到100。
|
||||
* 提供100将始终返回黑色。
|
||||
* @param amount - 有效值在1-100之间
|
||||
*/
|
||||
darken(amount = 10) : TinyColor {
|
||||
const hsl = this.toHsl();
|
||||
hsl.l -= amount / 100;
|
||||
hsl.l = clamp01(hsl.l);
|
||||
return new TinyColor(hsl, { format: this.format } as LColorOptions);
|
||||
}
|
||||
/**
|
||||
* 将颜色与纯白色混合,范围从0到100。
|
||||
* 提供0将什么都不做,提供100将始终返回白色。
|
||||
* @param amount - 有效值在1-100之间
|
||||
*/
|
||||
tint(amount = 10) : TinyColor {
|
||||
return this.mix('white', amount);
|
||||
}
|
||||
/**
|
||||
* 将颜色与纯黑色混合,范围从0到100。
|
||||
* 提供0将什么都不做,提供100将始终返回黑色。
|
||||
* @param amount - 有效值在1-100之间
|
||||
*/
|
||||
shade(amount = 10) : TinyColor {
|
||||
return this.mix('black', amount);
|
||||
}
|
||||
/**
|
||||
* 将颜色的饱和度降低一定的量,范围从0到100。
|
||||
* 提供100与调用greyscale相同
|
||||
* @param amount - 有效值在1-100之间
|
||||
*/
|
||||
desaturate(amount = 10) : TinyColor {
|
||||
const hsl = this.toHsl();
|
||||
hsl.s -= amount / 100;
|
||||
hsl.s = clamp01(hsl.s);
|
||||
return new TinyColor(hsl, { format: this.format } as LColorOptions);
|
||||
}
|
||||
/**
|
||||
* 将颜色饱和度提高一定数量,范围从 0 到 100。
|
||||
* @param amount - 有效值介于 1 到 100 之间。
|
||||
*/
|
||||
saturate(amount = 10) : TinyColor {
|
||||
const hsl = this.toHsl();
|
||||
hsl.s += amount / 100;
|
||||
hsl.s = clamp01(hsl.s);
|
||||
return new TinyColor(hsl, { format: this.format } as LColorOptions);
|
||||
}
|
||||
/**
|
||||
* 将颜色完全去饱和为灰度。
|
||||
* 等同于调用 `desaturate(100)`。
|
||||
*/
|
||||
greyscale() : TinyColor {
|
||||
return this.desaturate(100);
|
||||
}
|
||||
/**
|
||||
* spin 方法接收一个正数或负数作为参数,表示色相的变化量,变化范围在 [-360, 360] 之间。
|
||||
* 如果提供的值超出此范围,它将被限制在此范围内。
|
||||
*/
|
||||
spin(amount : number) : TinyColor {
|
||||
const hsl = this.toHsl();
|
||||
const hue = (hsl.h + amount) % 360;
|
||||
hsl.h = hue < 0 ? 360 + hue : hue;
|
||||
return new TinyColor(hsl, { format: this.format } as LColorOptions);
|
||||
}
|
||||
/**
|
||||
* 将当前颜色与另一种颜色按给定的比例混合,范围从0到100。
|
||||
* 0表示不混合(返回当前颜色)
|
||||
*/
|
||||
mix(color : LColorInput, amount = 50) : TinyColor {
|
||||
const rgb1 = this.toRgb();
|
||||
const rgb2 = new TinyColor(color).toRgb();
|
||||
|
||||
const p = amount / 100;
|
||||
const rgba = {
|
||||
r: (rgb2.r - rgb1.r) * p + rgb1.r,
|
||||
g: (rgb2.g - rgb1.g) * p + rgb1.g,
|
||||
b: (rgb2.b - rgb1.b) * p + rgb1.b,
|
||||
a: (rgb2.a - rgb1.a) * p + rgb1.a,
|
||||
};
|
||||
|
||||
return new TinyColor(rgba, { format: this.format } as LColorOptions);
|
||||
}
|
||||
/**
|
||||
* 生成一组与当前颜色相似的颜色。
|
||||
* 这些颜色在色相环上是相邻的,形成一个类似于彩虹的颜色序列。
|
||||
* @param results - 要生成的相似颜色的数量,默认值为 6。
|
||||
* @param slices - 将色相环划分为多少个部分,默认值为 30。
|
||||
* @returns 一个包含当前颜色及其相似颜色的 TinyColor 对象数组。
|
||||
*/
|
||||
analogous(results = 6, slices = 30) : TinyColor[] {
|
||||
const hsl = this.toHsl();
|
||||
const part = 360 / slices;
|
||||
const ret : TinyColor[] = [this];
|
||||
let _results = results
|
||||
// for (hsl.h = (hsl.h - ((part * results) >> 1) + 720) % 360; --results;) {
|
||||
// hsl.h = (hsl.h + part) % 360;
|
||||
// ret.push(new TinyColor(hsl));
|
||||
// }
|
||||
hsl.h = (hsl.h - ((part * _results) >> 1) + 720) % 360;
|
||||
while (_results > 0) {
|
||||
hsl.h = (hsl.h + part) % 360;
|
||||
ret.push(new TinyColor(hsl));
|
||||
_results--;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* 计算当前颜色的补色。
|
||||
* 补色是指在色相环上相对位置的颜色,它们的色相差为 180°。
|
||||
* taken from https://github.com/infusion/jQuery-xcolor/blob/master/jquery.xcolor.js
|
||||
* @returns 一个 TinyColor 对象,表示当前颜色的补色。
|
||||
*/
|
||||
complement() : TinyColor {
|
||||
const hsl = this.toHsl();
|
||||
hsl.h = (hsl.h + 180) % 360;
|
||||
return new TinyColor(hsl, { format: this.format } as LColorOptions);
|
||||
}
|
||||
/**
|
||||
* 生成一组与当前颜色具有相同色相和饱和度的颜色。
|
||||
* 这些颜色的亮度值不同,形成一个单色调的颜色序列。
|
||||
* @param results - 要生成的单色调颜色的数量,默认值为 6。
|
||||
* @returns 一个包含当前颜色及其单色调颜色的 TinyColor 对象数组。
|
||||
*/
|
||||
monochromatic(results = 6) : TinyColor[] {
|
||||
const hsv = this.toHsv();
|
||||
const { h } = hsv;
|
||||
const { s } = hsv;
|
||||
let { v } = hsv;
|
||||
const res : TinyColor[] = [];
|
||||
const modification = 1 / results;
|
||||
let _results = results
|
||||
// while (results--) {
|
||||
// res.push(new TinyColor({ h, s, v }));
|
||||
// v = (v + modification) % 1;
|
||||
// }
|
||||
while (_results > 0) {
|
||||
res.push(new TinyColor({ h, s, v }));
|
||||
v = (v + modification) % 1;
|
||||
_results--
|
||||
}
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* 生成当前颜色的分裂补色。
|
||||
* 分裂补色是指在色相环上位于当前颜色的两侧的颜色,它们的色相差为 180°。
|
||||
* @returns 一个包含当前颜色及其分裂补色的 TinyColor 对象数组。
|
||||
*/
|
||||
splitcomplement() : TinyColor[] {
|
||||
const hsl = this.toHsl();
|
||||
const { h } = hsl;
|
||||
return [
|
||||
this,
|
||||
new TinyColor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l }),
|
||||
new TinyColor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l }),
|
||||
] as TinyColor[];
|
||||
}
|
||||
/**
|
||||
* 计算当前颜色在给定背景颜色上的显示效果。
|
||||
* @param background - 背景颜色,可以是任何 LColorInput 类型的值。
|
||||
* @returns 一个 TinyColor 对象,表示当前颜色在给定背景颜色上的显示效果。
|
||||
*/
|
||||
onBackground(background : LColorInput) : TinyColor {
|
||||
const fg = this.toRgb();
|
||||
const bg = new TinyColor(background).toRgb();
|
||||
const alpha = fg.a + bg.a * (1 - fg.a);
|
||||
|
||||
return new TinyColor({
|
||||
r: (fg.r * fg.a + bg.r * bg.a * (1 - fg.a)) / alpha,
|
||||
g: (fg.g * fg.a + bg.g * bg.a * (1 - fg.a)) / alpha,
|
||||
b: (fg.b * fg.a + bg.b * bg.a * (1 - fg.a)) / alpha,
|
||||
a: alpha,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 生成当前颜色的三色调。
|
||||
* 三色调是指在色相环上位于当前颜色的两侧的颜色,它们的色相差为 120°。
|
||||
* 这是 `polyad(3)` 方法的别名。
|
||||
* @returns 一个包含当前颜色及其三色调颜色的 TinyColor 对象数组。
|
||||
*/
|
||||
triad() : TinyColor[] {
|
||||
return this.polyad(3);
|
||||
}
|
||||
/**
|
||||
* 生成当前颜色的四色调。
|
||||
* 四色调是指在色相环上位于当前颜色的两侧的颜色,它们的色相差为 90°。
|
||||
* 这是 `polyad(4)` 方法的别名。
|
||||
* @returns 一个包含当前颜色及其四色调颜色的 TinyColor 对象数组。
|
||||
*/
|
||||
tetrad() : TinyColor[] {
|
||||
return this.polyad(4);
|
||||
}
|
||||
/**
|
||||
* 生成当前颜色的 n 色调。
|
||||
* n 色调是指在色相环上位于当前颜色的两侧的颜色,它们的色相差为 360° / n。
|
||||
* Get polyad colors, like (for 1, 2, 3, 4, 5, 6, 7, 8, etc...)
|
||||
* monad, dyad, triad, tetrad, pentad, hexad, heptad, octad, etc...
|
||||
* @param n - 一个整数,表示要生成的色调数量。
|
||||
* @returns 一个包含当前颜色及其 n 色调颜色的 TinyColor 对象数组。
|
||||
*/
|
||||
polyad(n : number) : TinyColor[] {
|
||||
const hsl = this.toHsl();
|
||||
const { h } = hsl;
|
||||
|
||||
const result : TinyColor[] = [this];
|
||||
const increment = 360 / n;
|
||||
for (let i = 1; i < n; i++) {
|
||||
result.push(new TinyColor({ h: (h + i * increment) % 360, s: hsl.s, l: hsl.l }));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* 比较当前颜色与给定颜色是否相等。
|
||||
* @param color - 一个 LColorInput 类型的值,表示要比较的颜色。
|
||||
* @returns 一个布尔值,表示当前颜色与给定颜色是否相等。
|
||||
*/
|
||||
|
||||
// #ifndef APP-ANDROID
|
||||
equals(other ?: LColorInput) : boolean {
|
||||
if (other == null) {
|
||||
return false
|
||||
} else if (other instanceof TinyColor) {
|
||||
return this.toRgbString() == (other as TinyColor).toRgbString()
|
||||
}
|
||||
return this.toRgbString() == new TinyColor(other).toRgbString();
|
||||
}
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
override equals(other ?: LColorInput) : boolean {
|
||||
if (other == null) {
|
||||
return false
|
||||
} else if (other instanceof TinyColor) {
|
||||
return this.toRgbString() == (other as TinyColor).toRgbString()
|
||||
}
|
||||
return this.toRgbString() == new TinyColor(other).toRgbString();
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
export function tinyColor(color : LColorInput = '', opts : LColorOptions = {} as LColorOptions) : TinyColor {
|
||||
return new TinyColor(color, opts);
|
||||
}
|
||||
306
qiming-mobile/uni_modules/lime-color/common/conversion.uts
Normal file
306
qiming-mobile/uni_modules/lime-color/common/conversion.uts
Normal file
@@ -0,0 +1,306 @@
|
||||
import { RGB,HSL,HSV } from '../utssdk/interface.uts';
|
||||
import { bound01, pad2 } from './util';
|
||||
|
||||
|
||||
/**
|
||||
* Handle bounds / percentage checking to conform to CSS color spec
|
||||
* 处理边界/百分比检查以符合 CSS 颜色规范
|
||||
* <http://www.w3.org/TR/css3-color/>
|
||||
* *Assumes:* r, g, b in [0, 255] or [0, 1]
|
||||
* *Returns:* { r, g, b } in [0, 255]
|
||||
*/
|
||||
function rgbToRgb(r: string, g: string, b: string):RGB;
|
||||
function rgbToRgb(r: number, g: string, b: string):RGB;
|
||||
function rgbToRgb(r: number, g: number, b: string):RGB;
|
||||
function rgbToRgb(r: number, g: number, b: number):RGB;
|
||||
function rgbToRgb(r: any, g: any, b: any) : RGB {
|
||||
return {
|
||||
r: bound01(r, 255) * 255,
|
||||
g: bound01(g, 255) * 255,
|
||||
b: bound01(b, 255) * 255,
|
||||
} as RGB;
|
||||
}
|
||||
export {
|
||||
rgbToRgb
|
||||
}
|
||||
/**
|
||||
* Converts an RGB color value to HSL.
|
||||
* 将 RGB 颜色值转换为 HSL。
|
||||
* *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
|
||||
* *Returns:* { h, s, l } in [0,1]
|
||||
*/
|
||||
function rgbToHsl(r: string, g: string, b: string):HSL;
|
||||
function rgbToHsl(r: number, g: string, b: string):HSL;
|
||||
function rgbToHsl(r: number, g: number, b: string):HSL;
|
||||
function rgbToHsl(r: number, g: number, b: number):HSL;
|
||||
function rgbToHsl(r : any, g : any, b : any) : HSL {
|
||||
r = bound01(r, 255);
|
||||
g = bound01(g, 255);
|
||||
b = bound01(b, 255);
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h = 0;
|
||||
let s:number// = 0;
|
||||
const l = (max + min) / 2;
|
||||
|
||||
if (max == min) {
|
||||
s = 0;
|
||||
h = 0; // achromatic
|
||||
} else {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
default:
|
||||
console.log('h')
|
||||
break;
|
||||
}
|
||||
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return { h, s, l } as HSL;
|
||||
}
|
||||
|
||||
export {
|
||||
rgbToHsl
|
||||
}
|
||||
|
||||
export function hue2rgb(p : number, q : number, t : number) : number {
|
||||
let _t = t
|
||||
if (_t < 0) {
|
||||
_t += 1;
|
||||
}
|
||||
|
||||
if (_t > 1) {
|
||||
_t -= 1;
|
||||
}
|
||||
|
||||
if (_t < 1 / 6) {
|
||||
return p + (q - p) * (6 * _t);
|
||||
}
|
||||
|
||||
if (_t < 1 / 2) {
|
||||
return q;
|
||||
}
|
||||
|
||||
if (_t < 2 / 3) {
|
||||
return p + (q - p) * (2 / 3 - _t) * 6;
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HSL color value to RGB.
|
||||
* 将 HSL 颜色值转换为 RGB。
|
||||
* *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
|
||||
* *Returns:* { r, g, b } in the set [0, 255]
|
||||
*/
|
||||
function hslToRgb(h : string,s : string,l : string) : RGB
|
||||
function hslToRgb(h : number,s : string,l : string) : RGB
|
||||
function hslToRgb(h : number,s : number,l : string) : RGB
|
||||
function hslToRgb(h : number,s : number,l : number) : RGB
|
||||
function hslToRgb(h : any,s : any,l : any) : RGB {
|
||||
let r : number;
|
||||
let g : number;
|
||||
let b : number;
|
||||
h = bound01(h, 360);
|
||||
s = bound01(s, 100);
|
||||
l = bound01(l, 100);
|
||||
|
||||
if (s == 0) {
|
||||
// achromatic
|
||||
g = l;
|
||||
b = l;
|
||||
r = l;
|
||||
} else {
|
||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
const p = 2 * l - q;
|
||||
r = hue2rgb(p, q, h + 1 / 3);
|
||||
g = hue2rgb(p, q, h);
|
||||
b = hue2rgb(p, q, h - 1 / 3);
|
||||
}
|
||||
|
||||
return { r: r * 255, g: g * 255, b: b * 255 } as RGB;
|
||||
}
|
||||
export {
|
||||
hslToRgb
|
||||
}
|
||||
/**
|
||||
* Converts an RGB color value to HSV
|
||||
* 将RGB颜色值转换为HSV颜色值
|
||||
* *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
|
||||
* *Returns:* { h, s, v } in [0,1]
|
||||
*/
|
||||
export function rgbToHsv(r : number, g : number, b : number) : HSV {
|
||||
r = bound01(r, 255);
|
||||
g = bound01(g, 255);
|
||||
b = bound01(b, 255);
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h = 0;
|
||||
const v = max;
|
||||
const d = max - min;
|
||||
const s = max == 0 ? 0 : d / max;
|
||||
|
||||
if (max == min) {
|
||||
h = 0; // achromatic
|
||||
} else {
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
default:
|
||||
console.log('1')
|
||||
break;
|
||||
}
|
||||
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return { h, s, v } as HSV;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HSV color value to RGB.
|
||||
* 将HSV颜色值转换为RGB。
|
||||
* *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
|
||||
* *Returns:* { r, g, b } in the set [0, 255]
|
||||
*/
|
||||
function hsvToRgb( h : string, s : string, v : string) : RGB
|
||||
function hsvToRgb( h : number, s : string, v : string) : RGB
|
||||
function hsvToRgb( h : number, s : number, v : string) : RGB
|
||||
function hsvToRgb( h : number, s : number, v : number) : RGB
|
||||
function hsvToRgb( h : any, s : any, v : any) : RGB {
|
||||
h = bound01(h, 360) * 6;
|
||||
s = bound01(s, 100);
|
||||
v = bound01(v, 100);
|
||||
|
||||
const i = Math.floor(h);
|
||||
const f = h - i;
|
||||
const p = v * (1 - s);
|
||||
const q = v * (1 - f * s);
|
||||
const t = v * (1 - (1 - f) * s);
|
||||
const mod = i % 6;
|
||||
const r = [v, q, p, p, t, v][mod];
|
||||
const g = [t, v, v, q, p, p][mod];
|
||||
const b = [p, p, t, v, v, q][mod];
|
||||
|
||||
return { r: r * 255, g: g * 255, b: b * 255 } as RGB;
|
||||
}
|
||||
export {
|
||||
hsvToRgb
|
||||
}
|
||||
/**
|
||||
* Converts an RGB color to hex
|
||||
* 将RGB颜色转换为十六进制。
|
||||
* Assumes r, g, and b are contained in the set [0, 255]
|
||||
* Returns a 3 or 6 character hex
|
||||
*/
|
||||
export function rgbToHex(r : number, g : number, b : number, allow3Char : boolean = false) : string {
|
||||
const hex = [
|
||||
pad2(Math.round(r).toString(16)),
|
||||
pad2(Math.round(g).toString(16)),
|
||||
pad2(Math.round(b).toString(16)),
|
||||
];
|
||||
|
||||
// Return a 3 character hex if possible
|
||||
if (
|
||||
allow3Char &&
|
||||
hex[0].startsWith(hex[0].charAt(1)) &&
|
||||
hex[1].startsWith(hex[1].charAt(1)) &&
|
||||
hex[2].startsWith(hex[2].charAt(1))
|
||||
) {
|
||||
return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
|
||||
}
|
||||
|
||||
return hex.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an RGBA color plus alpha transparency to hex
|
||||
* 将带有透明度的RGBA颜色转换为十六进制。
|
||||
* Assumes r, g, b are contained in the set [0, 255] and
|
||||
* a in [0, 1]. Returns a 4 or 8 character rgba hex
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
export function rgbaToHex(r : number, g : number, b : number, a : number, allow4Char : boolean = false) : string {
|
||||
const hex = [
|
||||
pad2(Math.round(r).toString(16)),
|
||||
pad2(Math.round(g).toString(16)),
|
||||
pad2(Math.round(b).toString(16)),
|
||||
pad2(convertDecimalToHex(a)),
|
||||
];
|
||||
|
||||
// Return a 4 character hex if possible
|
||||
if (
|
||||
allow4Char &&
|
||||
hex[0].startsWith(hex[0].charAt(1)) &&
|
||||
hex[1].startsWith(hex[1].charAt(1)) &&
|
||||
hex[2].startsWith(hex[2].charAt(1)) &&
|
||||
hex[3].startsWith(hex[3].charAt(1))
|
||||
) {
|
||||
return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0) + hex[3].charAt(0);
|
||||
}
|
||||
|
||||
return hex.join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an RGBA color to an ARGB Hex8 string
|
||||
* 将RGBA颜色转换为ARGB十六进制字符串。
|
||||
* Rarely used, but required for "toFilter()"
|
||||
*/
|
||||
export function rgbaToArgbHex(r : number, g : number, b : number, a : number) : string {
|
||||
const hex = [
|
||||
pad2(convertDecimalToHex(a)),
|
||||
pad2(Math.round(r).toString(16)),
|
||||
pad2(Math.round(g).toString(16)),
|
||||
pad2(Math.round(b).toString(16)),
|
||||
];
|
||||
|
||||
return hex.join('');
|
||||
}
|
||||
|
||||
/** Converts a decimal to a hex value */
|
||||
/** 将十进制转换为十六进制值。 */
|
||||
function convertDecimalToHex(d : number) : string
|
||||
function convertDecimalToHex(d : string) : string
|
||||
function convertDecimalToHex(d : any) : string {
|
||||
return Math.round(parseFloat(`${d}`) * 255).toString(16);
|
||||
}
|
||||
export {convertDecimalToHex}
|
||||
/** Converts a hex value to a decimal */
|
||||
export function convertHexToDecimal(h : string) : number {
|
||||
return parseIntFromHex(h) / 255;
|
||||
}
|
||||
|
||||
/** Parse a base-16 hex value into a base-10 integer */
|
||||
export function parseIntFromHex(val : string) : number {
|
||||
return parseInt(val, 16);
|
||||
}
|
||||
|
||||
export function numberInputToObject(color : number) : RGB {
|
||||
return {
|
||||
r: color >> 16,
|
||||
g: (color & 0xff00) >> 8,
|
||||
b: color & 0xff,
|
||||
} as RGB;
|
||||
}
|
||||
155
qiming-mobile/uni_modules/lime-color/common/css-color-names.uts
Normal file
155
qiming-mobile/uni_modules/lime-color/common/css-color-names.uts
Normal file
@@ -0,0 +1,155 @@
|
||||
// @ts-nocheck
|
||||
// https://github.com/bahamas10/css-color-names/blob/master/css-color-names.json
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
export const names: Map<string, string> = new Map<string, string>([
|
||||
['aliceblue', '#f0f8ff'],
|
||||
['antiquewhite', '#faebd7'],
|
||||
['aqua', '#00ffff'],
|
||||
['aquamarine', '#7fffd4'],
|
||||
['azure', '#f0ffff'],
|
||||
['beige', '#f5f5dc'],
|
||||
['bisque', '#ffe4c4'],
|
||||
['black', '#000000'],
|
||||
['blanchedalmond', '#ffebcd'],
|
||||
['blue', '#0000ff'],
|
||||
['blueviolet', '#8a2be2'],
|
||||
['brown', '#a52a2a'],
|
||||
['burlywood', '#deb887'],
|
||||
['cadetblue', '#5f9ea0'],
|
||||
['chartreuse', '#7fff00'],
|
||||
['chocolate', '#d2691e'],
|
||||
['coral', '#ff7f50'],
|
||||
['cornflowerblue', '#6495ed'],
|
||||
['cornsilk', '#fff8dc'],
|
||||
['crimson', '#dc143c'],
|
||||
['cyan', '#00ffff'],
|
||||
['darkblue', '#00008b'],
|
||||
['darkcyan', '#008b8b'],
|
||||
['darkgoldenrod', '#b8860b'],
|
||||
['darkgray', '#a9a9a9'],
|
||||
['darkgreen', '#006400'],
|
||||
['darkgrey', '#a9a9a9'],
|
||||
['darkkhaki', '#bdb76b'],
|
||||
['darkmagenta', '#8b008b'],
|
||||
['darkolivegreen', '#556b2f'],
|
||||
['darkorange', '#ff8c00'],
|
||||
['darkorchid', '#9932cc'],
|
||||
['darkred', '#8b0000'],
|
||||
['darksalmon', '#e9967a'],
|
||||
['darkseagreen', '#8fbc8f'],
|
||||
['darkslateblue', '#483d8b'],
|
||||
['darkslategray', '#2f4f4f'],
|
||||
['darkslategrey', '#2f4f4f'],
|
||||
['darkturquoise', '#00ced1'],
|
||||
['darkviolet', '#9400d3'],
|
||||
['deeppink', '#ff1493'],
|
||||
['deepskyblue', '#00bfff'],
|
||||
['dimgray', '#696969'],
|
||||
['dimgrey', '#696969'],
|
||||
['dodgerblue', '#1e90ff'],
|
||||
['firebrick', '#b22222'],
|
||||
['floralwhite', '#fffaf0'],
|
||||
['forestgreen', '#228b22'],
|
||||
['fuchsia', '#ff00ff'],
|
||||
['gainsboro', '#dcdcdc'],
|
||||
['ghostwhite', '#f8f8ff'],
|
||||
['goldenrod', '#daa520'],
|
||||
['gold', '#ffd700'],
|
||||
['gray', '#808080'],
|
||||
['green', '#008000'],
|
||||
['greenyellow', '#adff2f'],
|
||||
['grey', '#808080'],
|
||||
['honeydew', '#f0fff0'],
|
||||
['hotpink', '#ff69b4'],
|
||||
['indianred', '#cd5c5c'],
|
||||
['indigo', '#4b0082'],
|
||||
['ivory', '#fffff0'],
|
||||
['khaki', '#f0e68c'],
|
||||
['lavenderblush', '#fff0f5'],
|
||||
['lavender', '#e6e6fa'],
|
||||
['lawngreen', '#7cfc00'],
|
||||
['lemonchiffon', '#fffacd'],
|
||||
['lightblue', '#add8e6'],
|
||||
['lightcoral', '#f08080'],
|
||||
['lightcyan', '#e0ffff'],
|
||||
['lightgoldenrodyellow', '#fafad2'],
|
||||
['lightgray', '#d3d3d3'],
|
||||
['lightgreen', '#90ee90'],
|
||||
['lightgrey', '#d3d3d3'],
|
||||
['lightpink', '#ffb6c1'],
|
||||
['lightsalmon', '#ffa07a'],
|
||||
['lightseagreen', '#20b2aa'],
|
||||
['lightskyblue', '#87cefa'],
|
||||
['lightslategray', '#778899'],
|
||||
['lightslategrey', '#778899'],
|
||||
['lightsteelblue', '#b0c4de'],
|
||||
['lightyellow', '#ffffe0'],
|
||||
['lime', '#00ff00'],
|
||||
['limegreen', '#32cd32'],
|
||||
['linen', '#faf0e6'],
|
||||
['magenta', '#ff00ff'],
|
||||
['maroon', '#800000'],
|
||||
['mediumaquamarine', '#66cdaa'],
|
||||
['mediumblue', '#0000cd'],
|
||||
['mediumorchid', '#ba55d3'],
|
||||
['mediumpurple', '#9370db'],
|
||||
['mediumseagreen', '#3cb371'],
|
||||
['mediumslateblue', '#7b68ee'],
|
||||
['mediumspringgreen', '#00fa9a'],
|
||||
['mediumturquoise', '#48d1cc'],
|
||||
['mediumvioletred', '#c71585'],
|
||||
['midnightblue', '#191970'],
|
||||
['mintcream', '#f5fffa'],
|
||||
['mistyrose', '#ffe4e1'],
|
||||
['moccasin', '#ffe4b5'],
|
||||
['navajowhite', '#ffdead'],
|
||||
['navy', '#000080'],
|
||||
['oldlace', '#fdf5e6'],
|
||||
['olive', '#808000'],
|
||||
['olivedrab', '#6b8e23'],
|
||||
['orange', '#ffa500'],
|
||||
['orangered', '#ff4500'],
|
||||
['orchid', '#da70d6'],
|
||||
['palegoldenrod', '#eee8aa'],
|
||||
['palegreen', '#98fb98'],
|
||||
['paleturquoise', '#afeeee'],
|
||||
['palevioletred', '#db7093'],
|
||||
['papayawhip', '#ffefd5'],
|
||||
['peachpuff', '#ffdab9'],
|
||||
['peru', '#cd853f'],
|
||||
['pink', '#ffc0cb'],
|
||||
['plum', '#dda0dd'],
|
||||
['powderblue', '#b0e0e6'],
|
||||
['purple', '#800080'],
|
||||
['rebeccapurple', '#663399'],
|
||||
['red', '#ff0000'],
|
||||
['rosybrown', '#bc8f8f'],
|
||||
['royalblue', '#4169e1'],
|
||||
['saddlebrown', '#8b4513'],
|
||||
['salmon', '#fa8072'],
|
||||
['sandybrown', '#f4a460'],
|
||||
['seagreen', '#2e8b57'],
|
||||
['seashell', '#fff5ee'],
|
||||
['sienna', '#a0522d'],
|
||||
['silver', '#c0c0c0'],
|
||||
['skyblue', '#87ceeb'],
|
||||
['slateblue', '#6a5acd'],
|
||||
['slategray', '#708090'],
|
||||
['slategrey', '#708090'],
|
||||
['snow', '#fffafa'],
|
||||
['springgreen', '#00ff7f'],
|
||||
['steelblue', '#4682b4'],
|
||||
['tan', '#d2b48c'],
|
||||
['teal', '#008080'],
|
||||
['thistle', '#d8bfd8'],
|
||||
['tomato', '#ff6347'],
|
||||
['turquoise', '#40e0d0'],
|
||||
['violet', '#ee82ee'],
|
||||
['wheat', '#f5deb3'],
|
||||
['white', '#ffffff'],
|
||||
['whitesmoke', '#f5f5f5'],
|
||||
['yellow', '#ffff00'],
|
||||
['yellowgreen', '#9acd32'],
|
||||
]);
|
||||
356
qiming-mobile/uni_modules/lime-color/common/format-input.uts
Normal file
356
qiming-mobile/uni_modules/lime-color/common/format-input.uts
Normal file
@@ -0,0 +1,356 @@
|
||||
// @ts-nocheck
|
||||
import { HSL, HSLA, HSV, HSVA, HSB, HSBA,RGB, RGBA, LColorInfo, LColorFormats } from '../utssdk/interface.uts';
|
||||
import { convertHexToDecimal, hslToRgb, hsvToRgb, parseIntFromHex, rgbToRgb } from './conversion';
|
||||
import { names } from './css-color-names';
|
||||
import { boundAlpha, convertToPercentage, toBoolean } from './util';
|
||||
type ColorMatchers = {
|
||||
CSS_UNIT : RegExp,
|
||||
rgb : RegExp,
|
||||
rgba : RegExp,
|
||||
hsl : RegExp,
|
||||
hsla : RegExp,
|
||||
hsv : RegExp,
|
||||
hsva : RegExp,
|
||||
hsb : RegExp,
|
||||
hsba : RegExp,
|
||||
hex3 : RegExp,
|
||||
hex6 : RegExp,
|
||||
hex4 : RegExp,
|
||||
hex8 : RegExp,
|
||||
}
|
||||
|
||||
// #ifndef UNI-APP-X
|
||||
type UTSJSONObject = object
|
||||
// #endif
|
||||
|
||||
// <http://www.w3.org/TR/css3-values/#integers>
|
||||
const CSS_INTEGER = '[-\\+]?\\d+%?';
|
||||
|
||||
// <http://www.w3.org/TR/css3-values/#number-value>
|
||||
const CSS_NUMBER = '[-\\+]?\\d*\\.\\d+%?';
|
||||
|
||||
// Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
|
||||
// 允许正负整数/数字。不要捕获要么/或者,只需捕获整个结果。
|
||||
const CSS_UNIT = `(?:${CSS_NUMBER})|(?:${CSS_INTEGER})`;
|
||||
|
||||
// Actual matching.
|
||||
// Parentheses and commas are optional, but not required.
|
||||
// Whitespace can take the place of commas or opening paren
|
||||
// 实际匹配。
|
||||
// 圆括号和逗号是可选的,但不是必需的。
|
||||
// 空格可以代替逗号或左括号
|
||||
const PERMISSIVE_MATCH3 = '[\\s|\\(]+(' +
|
||||
CSS_UNIT +
|
||||
')[,|\\s]+(' +
|
||||
CSS_UNIT +
|
||||
')[,|\\s]+(' +
|
||||
CSS_UNIT +
|
||||
')\\s*\\)?';;
|
||||
// const PERMISSIVE_MATCH3 = `[\\s|\\(]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})\\s*\\)?`;
|
||||
const PERMISSIVE_MATCH4 = '[\\s|\\(]+(' +
|
||||
CSS_UNIT +
|
||||
')[,|\\s]+(' +
|
||||
CSS_UNIT +
|
||||
')[,|\\s]+(' +
|
||||
CSS_UNIT +
|
||||
')[,|\\s]+(' +
|
||||
CSS_UNIT +
|
||||
')\\s*\\)?';
|
||||
// const PERMISSIVE_MATCH4 = `[\\s|\\(]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})[,|\\s]+(${CSS_UNIT})\\s*\\)?`;
|
||||
|
||||
export const matchers = {
|
||||
CSS_UNIT: new RegExp(CSS_UNIT),
|
||||
rgb: new RegExp('rgb' + PERMISSIVE_MATCH3),
|
||||
rgba: new RegExp('rgba' + PERMISSIVE_MATCH4),
|
||||
hsl: new RegExp('hsl' + PERMISSIVE_MATCH3),
|
||||
hsla: new RegExp('hsla' + PERMISSIVE_MATCH4),
|
||||
hsv: new RegExp('hsv' + PERMISSIVE_MATCH3),
|
||||
hsva: new RegExp('hsva' + PERMISSIVE_MATCH4),
|
||||
hsb: new RegExp('hsb' + PERMISSIVE_MATCH3),
|
||||
hsba: new RegExp('hsba' + PERMISSIVE_MATCH4),
|
||||
hex3: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
|
||||
hex6: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
|
||||
hex4: /^#?([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
|
||||
hex8: /^#?([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
|
||||
} as ColorMatchers;
|
||||
|
||||
/**
|
||||
* Check to see if it looks like a CSS unit
|
||||
* 检查它是否看起来像一个 CSS 单位
|
||||
* (see `matchers` above for definition).
|
||||
*/
|
||||
function isValidCSSUnit(color : string) : boolean
|
||||
function isValidCSSUnit(color : number) : boolean
|
||||
// #ifndef APP
|
||||
function isValidCSSUnit(color : any|null) : boolean
|
||||
// #endif
|
||||
function isValidCSSUnit(color : any|null) : boolean {
|
||||
return toBoolean(matchers.CSS_UNIT.exec(`${color}`));
|
||||
}
|
||||
export { isValidCSSUnit }
|
||||
|
||||
function inputToRGB(color : string) : LColorInfo
|
||||
function inputToRGB(color : RGB) : LColorInfo
|
||||
function inputToRGB(color : RGBA) : LColorInfo
|
||||
function inputToRGB(color : HSL) : LColorInfo
|
||||
function inputToRGB(color : HSLA) : LColorInfo
|
||||
function inputToRGB(color : HSV) : LColorInfo
|
||||
function inputToRGB(color : HSVA) : LColorInfo
|
||||
function inputToRGB(color : HSB) : LColorInfo
|
||||
function inputToRGB(color : HSBA) : LColorInfo
|
||||
function inputToRGB(color : any) : LColorInfo {
|
||||
let _color: UTSJSONObject|null = null
|
||||
let rgb = { r: 0, g: 0, b: 0 } as RGB;
|
||||
let a:any = 1;
|
||||
let s: any | null;
|
||||
let v: any | null;
|
||||
let l: any | null;
|
||||
let ok = false;
|
||||
let format: LColorFormats | null = null;
|
||||
|
||||
if (typeof color == 'string') {
|
||||
_color = stringInputToObject(color as string);
|
||||
} else if(typeof color == 'object'){
|
||||
_color = JSON.parse<UTSJSONObject>(JSON.stringify(color)) as UTSJSONObject
|
||||
} else {
|
||||
// _color = {} as UTSJSONObject
|
||||
}
|
||||
if(_color != null){
|
||||
if (isValidCSSUnit(_color['r']) && isValidCSSUnit(_color['g']) && isValidCSSUnit(_color['b'])){
|
||||
rgb = rgbToRgb(_color['r']!, _color['g']!, _color['b']!);
|
||||
ok = true;
|
||||
format = `${_color['r']}`.endsWith('%') ? 'prgb' : 'rgb';
|
||||
} else if(isValidCSSUnit(_color['h']) && isValidCSSUnit(_color['s']) && (isValidCSSUnit(_color['v']) || isValidCSSUnit(_color['b'])) ){
|
||||
const isHSV = _color['v'] != null
|
||||
s = convertToPercentage(_color['s']!);
|
||||
v = isHSV ? convertToPercentage(_color['v']!) : convertToPercentage(_color['b']!);
|
||||
rgb = hsvToRgb(_color['h']!, s, v);
|
||||
ok = true;
|
||||
format = isHSV ? 'hsv' : 'hsb';
|
||||
} else if(isValidCSSUnit(_color['h']) && isValidCSSUnit(_color['s']) && isValidCSSUnit(_color['l'])){
|
||||
s = convertToPercentage(_color['s']!);
|
||||
l = convertToPercentage(_color['l']!);
|
||||
|
||||
rgb = hslToRgb(_color['h']!, s, l);
|
||||
ok = true;
|
||||
format = 'hsl';
|
||||
}
|
||||
if(_color['a']!=null){
|
||||
a = _color['a']!;
|
||||
}
|
||||
}
|
||||
a = boundAlpha(a);
|
||||
return {
|
||||
ok,
|
||||
format: _color?.['format'] as (string | null) ?? format,
|
||||
r: Math.min(255, Math.max(rgb.r, 0)),
|
||||
g: Math.min(255, Math.max(rgb.g, 0)),
|
||||
b: Math.min(255, Math.max(rgb.b, 0)),
|
||||
a,
|
||||
} as LColorInfo
|
||||
}
|
||||
|
||||
export {
|
||||
inputToRGB
|
||||
}
|
||||
|
||||
/**
|
||||
* Permissive string parsing. Take in a number of formats, and output an object
|
||||
* based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
|
||||
*/
|
||||
|
||||
export function stringInputToObject(color : string) : UTSJSONObject | null {
|
||||
let _color = color.trim().toLowerCase();
|
||||
if (_color.length == 0) {
|
||||
return null;
|
||||
}
|
||||
let named = false;
|
||||
if (names.get(_color) != null) {
|
||||
_color = names.get(_color)!;
|
||||
named = true;
|
||||
} else if (_color == 'transparent') {
|
||||
return { r: 0, g: 0, b: 0, a: 0, format: 'name' } as UTSJSONObject;
|
||||
// return new Map([
|
||||
// ['r', 0],
|
||||
// ['g', 0],
|
||||
// ['b', 0],
|
||||
// ['a', 0],
|
||||
// ['format', 'name'],
|
||||
// ])
|
||||
}
|
||||
// Try to match string input using regular expressions.
|
||||
// Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
|
||||
// Just return an object and let the conversion functions handle that.
|
||||
// This way the result will be the same whether the tinycolor is initialized with string or object.
|
||||
|
||||
let match = matchers.rgb.exec(_color);
|
||||
if (match != null) {
|
||||
const r = match[1]
|
||||
const g = match[2]
|
||||
const b = match[3]
|
||||
|
||||
return { r, g, b } as UTSJSONObject;
|
||||
// return new Map([
|
||||
// ['r', match[1]],
|
||||
// ['g', match[2]],
|
||||
// ['b', match[3]],
|
||||
// ])
|
||||
}
|
||||
|
||||
match = matchers.rgba.exec(_color);
|
||||
if (match != null) {
|
||||
const r = match[1]
|
||||
const g = match[2]
|
||||
const b = match[3]
|
||||
const a = match[4]
|
||||
return { r, g, b, a } as UTSJSONObject;
|
||||
// return new Map([
|
||||
// ['r', match[1]],
|
||||
// ['g', match[2]],
|
||||
// ['b', match[3]],
|
||||
// ['a', match[4]],
|
||||
// ])
|
||||
}
|
||||
|
||||
match = matchers.hsl.exec(_color);
|
||||
if (match != null) {
|
||||
const h = match[1]
|
||||
const s = match[2]
|
||||
const l = match[3]
|
||||
return { h, s, l } as UTSJSONObject;
|
||||
// return new Map([
|
||||
// ['h', match[1]],
|
||||
// ['s', match[2]],
|
||||
// ['l', match[3]],
|
||||
// ])
|
||||
}
|
||||
|
||||
match = matchers.hsla.exec(_color);
|
||||
if (match != null) {
|
||||
const h = match[1]
|
||||
const s = match[2]
|
||||
const l = match[3]
|
||||
const a = match[4]
|
||||
return { h, s, l, a } as UTSJSONObject;
|
||||
// return new Map([
|
||||
// ['h', match[1]],
|
||||
// ['s', match[2]],
|
||||
// ['l', match[3]],
|
||||
// ['a', match[4]],
|
||||
// ])
|
||||
}
|
||||
|
||||
match = matchers.hsv.exec(_color);
|
||||
if (match != null) {
|
||||
const h = match[1]
|
||||
const s = match[2]
|
||||
const v = match[3]
|
||||
return { h, s, v } as UTSJSONObject;
|
||||
// return new Map([
|
||||
// ['h', match[1]],
|
||||
// ['s', match[2]],
|
||||
// ['v', match[3]],
|
||||
// ])
|
||||
}
|
||||
|
||||
match = matchers.hsva.exec(_color);
|
||||
if (match != null) {
|
||||
const h = match[1]
|
||||
const s = match[2]
|
||||
const v = match[3]
|
||||
const a = match[4]
|
||||
return { h, s, v, a } as UTSJSONObject;
|
||||
// return new Map([
|
||||
// ['h', match[1]],
|
||||
// ['s', match[2]],
|
||||
// ['v', match[3]],
|
||||
// ['a', match[4]],
|
||||
// ])
|
||||
}
|
||||
|
||||
match = matchers.hex8.exec(_color);
|
||||
if (match != null) {
|
||||
const r = parseIntFromHex(match[1]!)
|
||||
const g = parseIntFromHex(match[2]!)
|
||||
const b = parseIntFromHex(match[3]!)
|
||||
const a = convertHexToDecimal(match[4]!)
|
||||
return {
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
a,
|
||||
format: named ? 'name' : 'hex8',
|
||||
} as UTSJSONObject;
|
||||
// return new Map([
|
||||
// ['r', parseIntFromHex(match[1])],
|
||||
// ['g', parseIntFromHex(match[2])],
|
||||
// ['b', parseIntFromHex(match[3])],
|
||||
// ['a', convertHexToDecimal(match[4])],
|
||||
// ['format', named ? 'name' : 'hex8'],
|
||||
// ])
|
||||
}
|
||||
|
||||
match = matchers.hex6.exec(_color);
|
||||
if (match != null) {
|
||||
const r = parseIntFromHex(match[1]!)
|
||||
const g = parseIntFromHex(match[2]!)
|
||||
const b = parseIntFromHex(match[3]!)
|
||||
// const a = parseIntFromHex(match[4])
|
||||
return {
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
format: named ? 'name' : 'hex',
|
||||
} as UTSJSONObject;
|
||||
// return new Map([
|
||||
// ['r', parseIntFromHex(match[1])],
|
||||
// ['g', parseIntFromHex(match[2])],
|
||||
// ['b', parseIntFromHex(match[3])],
|
||||
// ['format', named ? 'name' : 'hex'],
|
||||
// ])
|
||||
}
|
||||
|
||||
match = matchers.hex4.exec(_color);
|
||||
if (match != null) {
|
||||
const r = parseIntFromHex((match[1] + match[1]))
|
||||
const g = parseIntFromHex((match[2] + match[2]))
|
||||
const b = parseIntFromHex((match[3] + match[3]))
|
||||
const a = convertHexToDecimal((match[4] + match[4]))
|
||||
return {
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
a,
|
||||
format: named ? 'name' : 'hex8',
|
||||
} as UTSJSONObject;
|
||||
// return new Map([
|
||||
// ['r', parseIntFromHex(match[1] + match[1])],
|
||||
// ['g', parseIntFromHex(match[2] + match[2])],
|
||||
// ['b', parseIntFromHex(match[3] + match[3])],
|
||||
// ['a', convertHexToDecimal(match[4] + match[4])],
|
||||
// ['format', named ? 'name' : 'hex8'],
|
||||
// ])
|
||||
}
|
||||
|
||||
match = matchers.hex3.exec(_color);
|
||||
if (match != null) {
|
||||
const r = parseIntFromHex((match[1] + match[1]))
|
||||
const g = parseIntFromHex((match[2] + match[2]))
|
||||
const b = parseIntFromHex((match[3] + match[3]))
|
||||
return {
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
format: named ? 'name' : 'hex',
|
||||
} as UTSJSONObject;
|
||||
// return new Map([
|
||||
// ['r', parseIntFromHex(match[1] + match[1])],
|
||||
// ['g', parseIntFromHex(match[2] + match[2])],
|
||||
// ['b', parseIntFromHex(match[3] + match[3])],
|
||||
// ['format', named ? 'name' : 'hex'],
|
||||
// ])
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
202
qiming-mobile/uni_modules/lime-color/common/generate.uts
Normal file
202
qiming-mobile/uni_modules/lime-color/common/generate.uts
Normal file
@@ -0,0 +1,202 @@
|
||||
// https://github.com/ant-design/ant-design-colors/blob/main/src/generate.ts
|
||||
import { inputToRGB } from './format-input';
|
||||
import { rgbToHex, rgbToHsv } from './conversion';
|
||||
import { HSV, LColorInfo, LGenerateOptions} from '../utssdk/interface.uts';
|
||||
|
||||
type DarkColorMapItem = {
|
||||
index : number;
|
||||
opacity : number;
|
||||
};
|
||||
const hueStep = 2; // 色相阶梯
|
||||
const saturationStep = 0.16; // 饱和度阶梯,浅色部分
|
||||
const saturationStep2 = 0.05; // 饱和度阶梯,深色部分
|
||||
const brightnessStep1 = 0.05; // 亮度阶梯,浅色部分
|
||||
const brightnessStep2 = 0.15; // 亮度阶梯,深色部分
|
||||
const lightColorCount = 5; // 浅色数量,主色上
|
||||
const darkColorCount = 4; // 深色数量,主色下
|
||||
// 暗色主题颜色映射关系表
|
||||
const darkColorMap = [
|
||||
{ index: 7, opacity: 0.15 },
|
||||
{ index: 6, opacity: 0.25 },
|
||||
{ index: 5, opacity: 0.3 },
|
||||
{ index: 5, opacity: 0.45 },
|
||||
{ index: 5, opacity: 0.65 },
|
||||
{ index: 5, opacity: 0.85 },
|
||||
{ index: 4, opacity: 0.9 },
|
||||
{ index: 3, opacity: 0.95 },
|
||||
{ index: 2, opacity: 0.97 },
|
||||
{ index: 1, opacity: 0.98 },
|
||||
] as DarkColorMapItem[];
|
||||
|
||||
|
||||
// 从 TinyColor.toHsv 移植的包装函数
|
||||
// 保留这里,因为有 `hsv.h * 360`
|
||||
function toHsv({ r, g, b } : LColorInfo) : HSV {
|
||||
// 将 RGB 值转换为 HSV 值
|
||||
const hsv = rgbToHsv(r, g, b);
|
||||
// 返回一个 HsvObject,其中 h 值乘以 360
|
||||
return { h: hsv.h * 360, s: hsv.s, v: hsv.v } as HSV;
|
||||
}
|
||||
|
||||
// 从 TinyColor.toHexString 移植的包装函数
|
||||
// 保留这里,因为有前缀 `#`
|
||||
function toHex({ r, g, b }: LColorInfo) : string {
|
||||
// 将 RGB 值转换为十六进制字符串,并添加前缀 `#`
|
||||
return `#${rgbToHex(r, g, b, false)}`;
|
||||
}
|
||||
|
||||
|
||||
// 从 TinyColor.mix 移植的包装函数,无法进行 tree-shaking
|
||||
// 数量范围为 [0, 1]
|
||||
// 假设 color1 和 color2 没有透明度,因为以下源代码也是如此
|
||||
function mix(rgb1 : LColorInfo, rgb2 : LColorInfo, amount : number) : LColorInfo {
|
||||
// 将 amount 除以 100,得到一个范围为 [0, 1] 的值
|
||||
const p = amount / 100;
|
||||
// 计算混合后的 RGB 值
|
||||
const rgb = {
|
||||
r: (rgb2.r - rgb1.r) * p + rgb1.r,
|
||||
g: (rgb2.g - rgb1.g) * p + rgb1.g,
|
||||
b: (rgb2.b - rgb1.b) * p + rgb1.b,
|
||||
a: 1
|
||||
} as LColorInfo;
|
||||
// 返回混合后的 RGB 对象
|
||||
return rgb;
|
||||
}
|
||||
|
||||
// 根据给定的 HSV 对象和索引值计算新的色相值
|
||||
// 如果 light 参数为 true,则色相向左转动;否则向右转动
|
||||
function getHue(hsv : HSV, i : number, light : boolean = false) : number {
|
||||
let hue : number;
|
||||
// 根据色相不同,色相转向不同
|
||||
if (Math.round(hsv.h) >= 60 && Math.round(hsv.h) <= 240) {
|
||||
// 如果色相在 60 到 240 之间,向左转动
|
||||
hue = light ? Math.round(hsv.h) - hueStep * i : Math.round(hsv.h) + hueStep * i;
|
||||
} else {
|
||||
hue = light ? Math.round(hsv.h) + hueStep * i : Math.round(hsv.h) - hueStep * i;
|
||||
}
|
||||
|
||||
if (hue < 0) {
|
||||
// 如果新的色相值小于 0,则加上 360
|
||||
hue += 360;
|
||||
} else if (hue >= 360) {
|
||||
// 如果新的色相值大于等于 360,则减去 360
|
||||
hue -= 360;
|
||||
}
|
||||
return hue;
|
||||
}
|
||||
|
||||
|
||||
// 根据给定的 HSV 对象和索引值计算新的饱和度值
|
||||
// 如果 light 参数为 true,则饱和度减小;否则增加
|
||||
function getSaturation(hsv : HSV, i : number, light : boolean = false) : number {
|
||||
// grey color don't change saturation
|
||||
// 如果颜色是灰色(色相和饱和度都为 0),则饱和度不变
|
||||
if (hsv.h == 0 && hsv.s == 0) {
|
||||
return hsv.s;
|
||||
}
|
||||
let saturation : number;
|
||||
// 如果 light 参数为 true,则饱和度减小
|
||||
if (light) {
|
||||
saturation = hsv.s - saturationStep * i;
|
||||
}
|
||||
// 如果 i 等于 darkColorCount,则饱和度增加
|
||||
else if (i == darkColorCount) {
|
||||
saturation = hsv.s + saturationStep;
|
||||
}
|
||||
// 否则,饱和度增加
|
||||
else {
|
||||
saturation = hsv.s + saturationStep2 * i;
|
||||
}
|
||||
// 边界值修正
|
||||
if (saturation > 1) {
|
||||
saturation = 1;
|
||||
}
|
||||
// 第一格的 s 限制在 0.06-0.1 之间
|
||||
if (light && i == lightColorCount && saturation > 0.1) {
|
||||
saturation = 0.1;
|
||||
}
|
||||
if (saturation < 0.06) {
|
||||
saturation = 0.06;
|
||||
}
|
||||
return parseFloat(saturation.toFixed(2))
|
||||
}
|
||||
|
||||
// 根据给定的 HSV 对象和索引值计算新的亮度值
|
||||
// 如果 light 参数为 true,则亮度增加;否则减少
|
||||
function getValue(hsv : HSV, i : number, light : boolean = false) : number {
|
||||
let value : number;
|
||||
// 如果 light 参数为 true,则亮度增加
|
||||
if (light) {
|
||||
value = hsv.v + brightnessStep1 * i;
|
||||
} else {
|
||||
value = hsv.v - brightnessStep2 * i;
|
||||
}
|
||||
if (value > 1) {
|
||||
value = 1;
|
||||
}
|
||||
// 返回保留两位小数的亮度值
|
||||
return parseFloat(value.toFixed(2));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* generate 函数用于生成一组基于给定颜色的色彩模式。
|
||||
* 它可以生成亮色、暗色和深色主题颜色模式。
|
||||
*
|
||||
* @param {string} color - 输入的颜色值,可以是十六进制、RGB、RGBA、HSL、HSLA或颜色名称。
|
||||
* @param {LGenerateOptions} [opts] - 可选的生成选项。
|
||||
* @returns {string[]} - 返回一个包含生成的颜色模式的字符串数组。
|
||||
*/
|
||||
export function generate(color : string, opts : LGenerateOptions = {} as LGenerateOptions) : string[] {
|
||||
const patterns : string[] = [];
|
||||
const pColor = inputToRGB(color);
|
||||
|
||||
// 生成亮色模式
|
||||
for (let i = lightColorCount; i > 0; i -= 1) {
|
||||
const hsv = toHsv(pColor);
|
||||
const colorString : string = toHex(
|
||||
inputToRGB({
|
||||
h: getHue(hsv, i, true),
|
||||
s: getSaturation(hsv, i, true),
|
||||
v: getValue(hsv, i, true),
|
||||
}),
|
||||
);
|
||||
patterns.push(colorString);
|
||||
}
|
||||
|
||||
// 添加原始颜色
|
||||
patterns.push(toHex(pColor));
|
||||
|
||||
// 生成暗色模式
|
||||
for (let i = 1; i <= darkColorCount; i += 1) {
|
||||
const hsv = toHsv(pColor);
|
||||
const colorString : string = toHex(
|
||||
inputToRGB({
|
||||
h: getHue(hsv, i),
|
||||
s: getSaturation(hsv, i),
|
||||
v: getValue(hsv, i),
|
||||
}),
|
||||
);
|
||||
patterns.push(colorString);
|
||||
}
|
||||
|
||||
// 如果选项中指定了 dark 主题,则生成深色主题颜色模式
|
||||
if (opts.theme == 'dark') {
|
||||
return darkColorMap.map(({ index, opacity }, _):string => {
|
||||
const darkColorString : string = toHex(
|
||||
mix(
|
||||
inputToRGB(opts.backgroundColor ?? '#141414'),
|
||||
inputToRGB(patterns[index]),
|
||||
opacity * 100,
|
||||
),
|
||||
);
|
||||
return darkColorString;
|
||||
});
|
||||
}
|
||||
|
||||
// 返回默认颜色模式
|
||||
return patterns;
|
||||
}
|
||||
|
||||
200
qiming-mobile/uni_modules/lime-color/common/util.uts
Normal file
200
qiming-mobile/uni_modules/lime-color/common/util.uts
Normal file
@@ -0,0 +1,200 @@
|
||||
// import {isNumber} from '@/uni_modules/lime-shared/isNumber'
|
||||
// import {isString} from '@/uni_modules/lime-shared/isString'
|
||||
// import {isNumeric} from '@/uni_modules/lime-shared/isNumeric'
|
||||
// #ifdef APP-ANDROID
|
||||
import BigDecimal from 'java.math.BigDecimal'
|
||||
// #endif
|
||||
export function isNumber(value: any|null):boolean{
|
||||
// #ifdef APP-ANDROID
|
||||
return ['Byte', 'UByte','Short','UShort','Int','UInt','Long','ULong','Float','Double','number'].includes(typeof value)
|
||||
// #endif
|
||||
// #ifdef APP-IOS
|
||||
return ['Int8', 'UInt8','Int16','UInt16','Int32','UInt32','Int64','UInt64','Int','UInt','Float','Float16','Float32','Float64','Double', 'number'].includes(typeof value)
|
||||
// #endif
|
||||
// #ifndef APP-ANDROID || APP-IOS
|
||||
return typeof value == 'number' && !isNaN(value);
|
||||
// #endif
|
||||
}
|
||||
export function isString(value: any|null):boolean{
|
||||
return typeof value == 'string';
|
||||
}
|
||||
export function isNumeric(value: any|null):boolean{
|
||||
if(isNumber(value)) {
|
||||
return true
|
||||
} else if(isString(value)) {
|
||||
// const regex = "-?\\d+(\\.\\d+)?".toRegex()
|
||||
const regex = new RegExp("^(-)?\\d+(\\.\\d+)?$")
|
||||
return regex.test(value as string) //regex.matches(value as string)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
export function toBoolean(value: any|null):boolean{
|
||||
// #ifdef APP
|
||||
// 根据输入值的类型,返回相应的布尔值
|
||||
if(isNumber(value)){
|
||||
return (value as number) != 0;
|
||||
}
|
||||
if(isString(value)){
|
||||
return `${value}`.length > 0;
|
||||
}
|
||||
if(typeof value == 'boolean'){
|
||||
return value as boolean;
|
||||
}
|
||||
|
||||
return value != null
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
return Boolean(value)
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if string passed in is a percentage
|
||||
* 检查传入的字符串是否为百分比
|
||||
* @hidden
|
||||
*/
|
||||
export function isPercentage(n : any) : boolean {
|
||||
return isString(n) && (n as string).indexOf('%') != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
|
||||
* 需要处理 1.0 为 100%,因为一旦它是数字,它与 1 没有区别
|
||||
* <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
|
||||
* @hidden
|
||||
*/
|
||||
export function isOnePointZero(n : any) : boolean {
|
||||
return isString(n) && (n as string).indexOf('.') != -1 && parseFloat(n as string) == 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Take input from [0, n] and return it as [0, 1]
|
||||
* 将输入值从 [0, n] 转换为 [0, 1]
|
||||
* @hidden
|
||||
*/
|
||||
function bound01(n: string, max: number): number
|
||||
function bound01(n: number, max: number): number
|
||||
// #ifndef APP
|
||||
function bound01(n : any, max : number) : number
|
||||
// #endif
|
||||
function bound01(n : any, max : number) : number {
|
||||
if(!(isNumber(n) || isString(n))){
|
||||
return 1
|
||||
}
|
||||
if (isOnePointZero(n)) {
|
||||
n = '100%';
|
||||
}
|
||||
|
||||
const isPercent = isPercentage(n);
|
||||
n = (isNumber(n) ? n : parseFloat(n as string)) as number
|
||||
n = max == 360 ? n : Math.min(max, Math.max(0, n));
|
||||
|
||||
// Automatically convert percentage into number
|
||||
// 自动将百分比转换为数字
|
||||
if (isPercent) {
|
||||
n = parseInt(`${Math.min(n, 100) * max}`, 10) / 100;
|
||||
}
|
||||
|
||||
// Handle floating point rounding errors
|
||||
// 处理浮点数舍入误差
|
||||
|
||||
if ( Math.abs(n - max) < 0.000001) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Convert into [0, 1] range if it isn't already
|
||||
// 如果它还不是,将其转换为 [0, 1] 范围
|
||||
if (max == 360) {
|
||||
// If n is a hue given in degrees,
|
||||
// wrap around out-of-range values into [0, 360] range
|
||||
// then convert into [0, 1].
|
||||
// 如果 n 是以度为单位的色调,
|
||||
// 将超出范围的值环绕到 [0, 360] 范围内
|
||||
// 然后将其转换为 [0, 1]。
|
||||
n = (n < 0 ? (n % max) + max : n % max) / max // parseFloat(`${max}`);
|
||||
} else {
|
||||
// If n not a hue given in degrees
|
||||
// Convert into [0, 1] range if it isn't already.
|
||||
// 如果 n 不是以度为单位的色调
|
||||
// 如果它还不是,将其转换为 [0, 1] 范围。
|
||||
n = (n % max) / max //parseFloat(`${max}`);
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
export {bound01}
|
||||
|
||||
|
||||
/**
|
||||
* Force a number between 0 and 1
|
||||
* 在 0 和 1 之间强制一个数字
|
||||
* @hidden
|
||||
*/
|
||||
export function clamp01(val : number) : number {
|
||||
return Math.min(1, Math.max(0, val));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Return a valid alpha value [0,1] with all invalid values being set to 1
|
||||
* 返回一个有效的 alpha 值 [0,1],将所有无效值设置为 1
|
||||
* @hidden
|
||||
*/
|
||||
function boundAlpha(a: number):number
|
||||
function boundAlpha(a: string):number
|
||||
// #ifndef APP
|
||||
function boundAlpha(a: any|null) : number
|
||||
// #endif
|
||||
function boundAlpha(a: any|null) : number {
|
||||
let n = a == null ? 1 : (isString(a) ? parseFloat(a as string) : a as number)
|
||||
|
||||
if (isNaN(n) || n < 0 || n > 1) {
|
||||
n = 1;
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
export {
|
||||
boundAlpha
|
||||
}
|
||||
/**
|
||||
* Replace a decimal with it's percentage value
|
||||
* 用百分比值替换小数
|
||||
* number | string
|
||||
* @hidden
|
||||
*/
|
||||
function convertToPercentage(n:number):number
|
||||
function convertToPercentage(n:string):string
|
||||
// #ifndef APP
|
||||
function convertToPercentage(n:any): any
|
||||
// #endif
|
||||
function convertToPercentage(n:any): any{
|
||||
// #ifdef APP-ANDROID
|
||||
n = isNumeric(n) ? parseFloat(typeof n == 'string' ? n as string : BigDecimal.valueOf((n as number).toDouble()).toPlainString()) : n// as number
|
||||
// #endif
|
||||
// #ifndef APP-ANDROID
|
||||
n = isNumeric(n) ? parseFloat(`${n}`) : n// as number
|
||||
// #endif
|
||||
if(isNumber(n) && (n as number) <= 1){
|
||||
return `${n * 100}%`.replace('.0%','%');
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
export {convertToPercentage}
|
||||
|
||||
/**
|
||||
* Force a hex value to have 2 characters
|
||||
* 强制使十六进制值具有 2 个字符
|
||||
* @hidden
|
||||
*/
|
||||
export function pad2(c : string) : string {
|
||||
//c.padStart(2, '0');//
|
||||
return c.length == 1 ? '0' + c : `${c}`;
|
||||
}
|
||||
5
qiming-mobile/uni_modules/lime-color/index.ts
Normal file
5
qiming-mobile/uni_modules/lime-color/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// @ts-nocheck
|
||||
export * from './common/color'
|
||||
export * from './common/generate'
|
||||
export * from './utssdk/interface'
|
||||
// export {LGenerateOptions} from './utssdk/interface'
|
||||
87
qiming-mobile/uni_modules/lime-color/interface.uts
Normal file
87
qiming-mobile/uni_modules/lime-color/interface.uts
Normal file
@@ -0,0 +1,87 @@
|
||||
export type RGB = {
|
||||
r : number;
|
||||
g : number;
|
||||
b : number;
|
||||
}
|
||||
export type RGBA = {
|
||||
r : number;
|
||||
g : number;
|
||||
b : number;
|
||||
a : number;
|
||||
}
|
||||
export type RGBAString = {
|
||||
r : string;
|
||||
g : string;
|
||||
b : string;
|
||||
a : number;
|
||||
}
|
||||
export type HSL = {
|
||||
h : number;
|
||||
s : number;
|
||||
l : number;
|
||||
}
|
||||
|
||||
export type HSLA = {
|
||||
h : number;
|
||||
s : number;
|
||||
l : number;
|
||||
a : number;
|
||||
}
|
||||
export type HSV = {
|
||||
h : number;
|
||||
s : number;
|
||||
v : number;
|
||||
}
|
||||
|
||||
export type HSVA = {
|
||||
h : number;
|
||||
s : number;
|
||||
v : number;
|
||||
a : number;
|
||||
}
|
||||
|
||||
// 增加部分
|
||||
export type HSB = {
|
||||
h : number;
|
||||
s : number;
|
||||
b : number;
|
||||
}
|
||||
export type HSBA = {
|
||||
h : number;
|
||||
s : number;
|
||||
b : number;
|
||||
a : number;
|
||||
}
|
||||
|
||||
export type LColorInfo = {
|
||||
ok ?: boolean;
|
||||
format ?: LColorFormats;
|
||||
r : number;
|
||||
g : number;
|
||||
b : number;
|
||||
a : number;
|
||||
}
|
||||
|
||||
export type LColorFormats =
|
||||
| 'rgb'
|
||||
| 'prgb'
|
||||
| 'hex'
|
||||
| 'hex3'
|
||||
| 'hex4'
|
||||
| 'hex6'
|
||||
| 'hex8'
|
||||
| 'name'
|
||||
| 'hsl'
|
||||
| 'hsb'
|
||||
| 'hsv';
|
||||
|
||||
export type LColorOptions = {
|
||||
format ?: LColorFormats;
|
||||
gradientType ?: string;
|
||||
}
|
||||
export type LColorInput = any //string | number | RGB | RGBA | HSL | HSLA | HSV | HSVA | LimeColor;
|
||||
|
||||
export type LGenerateOptions = {
|
||||
theme ?: 'dark' | 'default';
|
||||
backgroundColor ?: string;
|
||||
}
|
||||
107
qiming-mobile/uni_modules/lime-color/package.json
Normal file
107
qiming-mobile/uni_modules/lime-color/package.json
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"id": "lime-color",
|
||||
"displayName": "lime-color tinycolor颜色转换",
|
||||
"version": "0.0.7",
|
||||
"description": "lime-color是tinycolor UTS版的小型库,用于颜色操作和转换,内容Ant Design 的颜色等级生成算法",
|
||||
"keywords": [
|
||||
"lime-color",
|
||||
"TinyColor",
|
||||
"color",
|
||||
"颜色转换",
|
||||
"uts"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.0",
|
||||
"uni-app": "^4.72",
|
||||
"uni-app-x": "^4.74"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "uts",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "√"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "√",
|
||||
"vue3": "√"
|
||||
},
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"vue": "√",
|
||||
"nvue": "-",
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√",
|
||||
"alipay": "√",
|
||||
"toutiao": "√",
|
||||
"baidu": "√",
|
||||
"kuaishou": "√",
|
||||
"jd": "√",
|
||||
"harmony": "√",
|
||||
"qq": "√",
|
||||
"lark": "√"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "√",
|
||||
"union": "√"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
557
qiming-mobile/uni_modules/lime-color/readme - 副本.md
Normal file
557
qiming-mobile/uni_modules/lime-color/readme - 副本.md
Normal file
@@ -0,0 +1,557 @@
|
||||
# lime-color
|
||||
- 颜色转换
|
||||
|
||||
## 文档
|
||||
🚀 [color【站点1】](https://limex.qcoon.cn/uts/color.html)
|
||||
🌍 [color【站点2】](https://limeui.netlify.app/uts/color.html)
|
||||
🔥 [color【站点3】](https://limeui.familyzone.top/uts/color.html)
|
||||
|
||||
|
||||
|
||||
## 安装
|
||||
插件市场导入插件即可
|
||||
|
||||
## 使用
|
||||
```js
|
||||
import {tinyColor} from '@/uni_modules/lime-color'
|
||||
```
|
||||
|
||||
## 接受的字符串输入
|
||||
|
||||
字符串解析非常宽容。它的目的是使输入颜色尽可能简单。所有的逗号、百分比、括号都是可选的,大多数输入允许使用 0-1、0%-100% 或 0-n(其中 n 取决于值的 100、255 或 360)。
|
||||
HSL 和 HSV 都需要 0%-100% 或 0-1 作为 S/L/V 属性。H(色相)可以在 0%-100% 或 0-360 之间取值。
|
||||
RGB 输入需要 0-255 或 0%-100%。
|
||||
以下是一些字符串输入的示例:
|
||||
|
||||
|
||||
### Hex, 8-digit (RGBA) Hex
|
||||
|
||||
```ts
|
||||
tinyColor('#000');
|
||||
tinyColor('000');
|
||||
tinyColor('#369C');
|
||||
tinyColor('369C');
|
||||
tinyColor('#f0f0f6');
|
||||
tinyColor('f0f0f6');
|
||||
tinyColor('#f0f0f688');
|
||||
tinyColor('f0f0f688');
|
||||
```
|
||||
|
||||
### RGB, RGBA
|
||||
|
||||
```ts
|
||||
tinyColor('rgb (255, 0, 0)');
|
||||
tinyColor('rgb 255 0 0');
|
||||
tinyColor('rgba (255, 0, 0, .5)');
|
||||
tinyColor({ r: 255, g: 0, b: 0 });
|
||||
```
|
||||
|
||||
### HSL, HSLA
|
||||
|
||||
```ts
|
||||
tinyColor('hsl(0, 100%, 50%)');
|
||||
tinyColor('hsla(0, 100%, 50%, .5)');
|
||||
tinyColor('hsl(0, 100%, 50%)');
|
||||
tinyColor('hsl 0 1.0 0.5');
|
||||
tinyColor({ h: 0, s: 1, l: 0.5 });
|
||||
```
|
||||
|
||||
### HSV, HSVA
|
||||
|
||||
```ts
|
||||
tinyColor('hsv(0, 100%, 100%)');
|
||||
tinyColor('hsva(0, 100%, 100%, .5)');
|
||||
tinyColor('hsv (0 100% 100%)');
|
||||
tinyColor('hsv 0 1 1');
|
||||
tinyColor({ h: 0, s: 100, v: 100 });
|
||||
```
|
||||
|
||||
### Named
|
||||
|
||||
```ts
|
||||
tinyColor('RED');
|
||||
tinyColor('blanchedalmond');
|
||||
tinyColor('darkblue');
|
||||
```
|
||||
|
||||
### Number
|
||||
```ts
|
||||
tinyColor(0x0);
|
||||
tinyColor(0xaabbcc);
|
||||
```
|
||||
|
||||
|
||||
## 属性
|
||||
|
||||
### originalInput
|
||||
传递到构造函数中用于创建`tinyColor`实例的原始输入。
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.originalInput; // "red"
|
||||
const color2 = tinyColor({ r: 255, g: 255, b: 255 });
|
||||
color2.originalInput; // "{r: 255, g: 255, b: 255}"
|
||||
```
|
||||
|
||||
### format
|
||||
|
||||
返回用于创建`tinyColor`实例的格式
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.format; // "name"
|
||||
const color2 = tinyColor({ r: 255, g: 255, b: 255 });
|
||||
color2.format; // "rgb"
|
||||
```
|
||||
|
||||
### isValid
|
||||
|
||||
一个布尔值,指示颜色是否成功被解析。注意:如果颜色无效,则在与其他方法一起使用时将表现得像“黑色”。
|
||||
|
||||
```ts
|
||||
const color1 = tinyColor('red');
|
||||
color1.isValid; // true
|
||||
color1.toHexString(); // "#ff0000"
|
||||
|
||||
const color2 = tinyColor('not a color');
|
||||
color2.isValid; // false
|
||||
color2.toString(); // "#000000"
|
||||
```
|
||||
|
||||
## Methods 方法
|
||||
|
||||
### getBrightness
|
||||
|
||||
返回颜色的感知亮度,范围从 0-255,这是根据 [Web内容无障碍指南(第1版)](http://www.w3.org/TR/AERT#color-contrast) 定义的。
|
||||
|
||||
```ts
|
||||
const color1 = tinyColor('#fff');
|
||||
color1.getBrightness(); // 255
|
||||
|
||||
const color2 = tinyColor('#000');
|
||||
color2.getBrightness(); // 0
|
||||
```
|
||||
|
||||
### isLight
|
||||
|
||||
返回一个布尔值,指示颜色的感知亮度是否为浅色。
|
||||
|
||||
```ts
|
||||
const color1 = tinyColor('#fff');
|
||||
color1.isLight(); // true
|
||||
|
||||
const color2 = tinyColor('#000');
|
||||
color2.isLight(); // false
|
||||
```
|
||||
|
||||
### isDark
|
||||
|
||||
返回一个布尔值,指示颜色的感知亮度是否为深色。
|
||||
|
||||
```ts
|
||||
const color1 = tinyColor('#fff');
|
||||
color1.isDark(); // false
|
||||
|
||||
const color2 = tinyColor('#000');
|
||||
color2.isDark(); // true
|
||||
```
|
||||
|
||||
### getLuminance
|
||||
|
||||
返回颜色的感知亮度(luminance),范围从 0-1,这是根据 [Web内容无障碍指南(第2版)](http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef) 定义的。
|
||||
|
||||
```ts
|
||||
const color1 = tinyColor('#fff');
|
||||
color1.getLuminance(); // 1
|
||||
|
||||
const color2 = tinyColor('#000');
|
||||
color2.getLuminance(); // 0
|
||||
```
|
||||
|
||||
### getAlpha
|
||||
|
||||
返回颜色的`alpha`(透明度)值,范围从 `0-1`。
|
||||
|
||||
```ts
|
||||
const color1 = tinyColor('rgba(255, 0, 0, .5)');
|
||||
color1.getAlpha(); // 0.5
|
||||
|
||||
const color2 = tinyColor('rgb(255, 0, 0)');
|
||||
color2.getAlpha(); // 1
|
||||
|
||||
const color3 = tinyColor('transparent');
|
||||
color3.getAlpha(); // 0
|
||||
```
|
||||
|
||||
### setAlpha
|
||||
|
||||
在当前颜色上设置`alpha`(透明度)值。接受的范围是 `0-1` 之间。
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.getAlpha(); // 1
|
||||
color.setAlpha(0.5);
|
||||
color.getAlpha(); // .5
|
||||
color.toRgbString(); // "rgba(255, 0, 0, .5)"
|
||||
```
|
||||
|
||||
### onBackground
|
||||
|
||||
计算颜色在背景上的显示效果。当颜色完全透明(即 `getAlpha() == 0`)时,结果将是背景颜色。当颜色完全不透明(即 `getAlpha() == 1`)时,结果将是颜色本身。否则,你将得到一个计算结果。
|
||||
|
||||
```ts
|
||||
const color = tinyColor('rgba(255, 0, 0, .5)');
|
||||
const computedColor = color.onBackground('rgb(0, 0, 255)');
|
||||
computedColor.toRgbString(); // "rgb(128, 0, 128)"
|
||||
```
|
||||
|
||||
### toHsv
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.toHsv(); // { h: 0, s: 1, v: 1, a: 1 }
|
||||
```
|
||||
|
||||
### toHsvString
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.toHsvString(); // "hsv(0, 100%, 100%)"
|
||||
color.setAlpha(0.5);
|
||||
color.toHsvString(); // "hsva(0, 100%, 100%, 0.5)"
|
||||
```
|
||||
|
||||
### toHsl
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.toHsl(); // { h: 0, s: 1, l: 0.5, a: 1 }
|
||||
```
|
||||
|
||||
### toHslString
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.toHslString(); // "hsl(0, 100%, 50%)"
|
||||
color.setAlpha(0.5);
|
||||
color.toHslString(); // "hsla(0, 100%, 50%, 0.5)"
|
||||
```
|
||||
|
||||
### toNumber
|
||||
```ts
|
||||
tinyColor('#aabbcc').toNumber() === 0xaabbcc // true
|
||||
tinyColor('rgb(1, 1, 1)').toNumber() === (1 << 16) + (1 << 8) + 1 // true
|
||||
```
|
||||
|
||||
### toHex
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.toHex(); // "ff0000"
|
||||
```
|
||||
|
||||
### toHexString
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.toHexString(); // "#ff0000"
|
||||
```
|
||||
|
||||
### toHex8
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.toHex8(); // "ff0000ff"
|
||||
```
|
||||
|
||||
### toHex8String
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.toHex8String(); // "#ff0000ff"
|
||||
```
|
||||
|
||||
### toHexShortString
|
||||
根据颜色的透明度(Alpha值)返回较短的十六进制值,并且值前面带有#符号
|
||||
```ts
|
||||
const color1 = tinyColor('#ff000000');
|
||||
color1.toHexShortString(); // "#ff000000"
|
||||
color1.toHexShortString(true); // "#f000"
|
||||
|
||||
const color2 = tinyColor('#ff0000ff');
|
||||
color2.toHexShortString(); // "#ff0000"
|
||||
color2.toHexShortString(true); // "#f00"
|
||||
```
|
||||
|
||||
### toRgb
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.toRgb(); // { r: 255, g: 0, b: 0, a: 1 }
|
||||
```
|
||||
|
||||
### toRgbString
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.toRgbString(); // "rgb(255, 0, 0)"
|
||||
color.setAlpha(0.5);
|
||||
color.toRgbString(); // "rgba(255, 0, 0, 0.5)"
|
||||
```
|
||||
|
||||
### toPercentageRgb
|
||||
将当前颜色转换为百分比表示的 RGB
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.toPercentageRgb(); // { r: "100%", g: "0%", b: "0%", a: 1 }
|
||||
```
|
||||
|
||||
### toPercentageRgbString
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.toPercentageRgbString(); // "rgb(100%, 0%, 0%)"
|
||||
color.setAlpha(0.5);
|
||||
color.toPercentageRgbString(); // "rgba(100%, 0%, 0%, 0.5)"
|
||||
```
|
||||
|
||||
### toName
|
||||
|
||||
```ts
|
||||
const color = tinyColor('red');
|
||||
color.toName(); // "red"
|
||||
```
|
||||
|
||||
|
||||
### toString
|
||||
|
||||
根据输入格式打印成字符串。你也可以通过向函数中传入以下之一来覆盖这个行为:`"rgb", "prgb", "hex6", "hex3", "hex8", "name", "hsl", "hsv"`
|
||||
|
||||
```ts
|
||||
const color1 = tinyColor('red');
|
||||
color1.toString(); // "red"
|
||||
color1.toString('hsv'); // "hsv(0, 100%, 100%)"
|
||||
|
||||
const color2 = tinyColor('rgb(255, 0, 0)');
|
||||
color2.toString(); // "rgb(255, 0, 0)"
|
||||
color2.setAlpha(0.5);
|
||||
color2.toString(); // "rgba(255, 0, 0, 0.5)"
|
||||
```
|
||||
|
||||
### 颜色修改
|
||||
|
||||
这些方法操纵当前颜色,并返回它以进行链式调用。例如:
|
||||
|
||||
```ts
|
||||
tinyColor('red')
|
||||
.lighten()
|
||||
.desaturate()
|
||||
.toHexString(); // '#f53d3d'
|
||||
```
|
||||
|
||||
### lighten
|
||||
|
||||
`lighten: function(amount = 10) -> TinyColor`.根据给定的量(从0到100)调亮颜色。提供100将始终返回白色.
|
||||
|
||||
```ts
|
||||
tinyColor('#f00').lighten().toString(); // '#ff3333'
|
||||
tinyColor('#f00').lighten(100).toString(); // '#ffffff'
|
||||
```
|
||||
|
||||
### brighten
|
||||
|
||||
`brighten: function(amount = 10) -> TinyColor`. 根据给定的量(从0到100)提高颜色的亮度。
|
||||
|
||||
```ts
|
||||
tinyColor('#f00').brighten().toString(); // '#ff1919'
|
||||
```
|
||||
|
||||
### darken
|
||||
|
||||
`darken: function(amount = 10) -> TinyColor`. 根据给定的量(从0到100)调暗颜色。提供100将始终返回黑色.
|
||||
|
||||
```ts
|
||||
tinyColor('#f00').darken().toString(); // '#cc0000'
|
||||
tinyColor('#f00').darken(100).toString(); // '#000000'
|
||||
```
|
||||
|
||||
### tint
|
||||
|
||||
将颜色与纯白色混合,范围从0到100。提供0将不进行任何操作,提供100将始终返回白色.
|
||||
|
||||
```ts
|
||||
tinyColor('#f00').tint().toString(); // "#ff1a1a"
|
||||
tinyColor('#f00').tint(100).toString(); // "#ffffff"
|
||||
```
|
||||
|
||||
### shade
|
||||
|
||||
将颜色与纯黑色混合,范围从0到100。提供0将不进行任何操作,提供100将始终返回黑色。
|
||||
|
||||
```ts
|
||||
tinyColor('#f00').shade().toString(); // "#e60000"
|
||||
tinyColor('#f00').shade(100).toString(); // "#000000"
|
||||
```
|
||||
|
||||
### desaturate
|
||||
|
||||
`desaturate: function(amount = 10) -> TinyColor`. 根据给定的量(从0到100)降低颜色的饱和度。提供100将与调用`greyscale`相同。
|
||||
|
||||
|
||||
```ts
|
||||
tinyColor('#f00').desaturate().toString(); // "#f20d0d"
|
||||
tinyColor('#f00').desaturate(100).toString(); // "#808080"
|
||||
```
|
||||
|
||||
### saturate
|
||||
|
||||
`saturate: function(amount = 10) -> TinyColor`. 根据给定的量(从0到100)增加颜色的饱和度。
|
||||
|
||||
```ts
|
||||
tinyColor('hsl(0, 10%, 50%)').saturate().toString(); // "hsl(0, 20%, 50%)"
|
||||
```
|
||||
|
||||
### greyscale
|
||||
|
||||
`greyscale: function() -> TinyColor`. 完全降低颜色的饱和度,使其变为灰度。与调用`desaturate(100)`相同。
|
||||
|
||||
```ts
|
||||
tinyColor('#f00').greyscale().toString(); // "#808080"
|
||||
```
|
||||
|
||||
### spin
|
||||
|
||||
`spin: function(amount = 0) -> TinyColor`. 根据给定的量(从-360到360)旋转色相。调用时使用0、360或-360将不进行任何操作(因为它将色相设置回原来的值)。
|
||||
|
||||
```ts
|
||||
tinyColor('#f00').spin(180).toString(); // "#00ffff"
|
||||
tinyColor('#f00').spin(-90).toString(); // "#7f00ff"
|
||||
tinyColor('#f00').spin(90).toString(); // "#80ff00"
|
||||
|
||||
// spin(0) and spin(360) do nothing
|
||||
tinyColor('#f00').spin(0).toString(); // "#ff0000"
|
||||
tinyColor('#f00').spin(360).toString(); // "#ff0000"
|
||||
```
|
||||
|
||||
### mix
|
||||
|
||||
`mix: function(amount = 50) => TinyColor`. 将当前颜色与另一种颜色按给定量(从0到100)混合。0表示不混合(返回当前颜色)。
|
||||
|
||||
```ts
|
||||
let color1 = tinyColor('#f0f');
|
||||
let color2 = tinyColor('#0f0');
|
||||
|
||||
color1.mix(color2).toHexString(); // #808080
|
||||
```
|
||||
|
||||
### 颜色组合
|
||||
|
||||
组合函数除非特别说明,否则返回一个`TinyColor`对象的数组。
|
||||
|
||||
### analogous
|
||||
生成一组与当前颜色相似的颜色。
|
||||
`analogous: function(results = 6, slices = 30) -> array<TinyColor>`.
|
||||
|
||||
```ts
|
||||
const colors = tinyColor('#f00').analogous();
|
||||
colors.map((t):string => t.toHexString()); // [ "#ff0000", "#ff0066", "#ff0033", "#ff0000", "#ff3300", "#ff6600" ]
|
||||
```
|
||||
|
||||
### monochromatic
|
||||
生成一组与当前颜色具有相同色相和饱和度的颜色。
|
||||
`monochromatic: function(, results = 6) -> array<TinyColor>`.
|
||||
|
||||
```ts
|
||||
const colors = tinyColor('#f00').monochromatic();
|
||||
colors.map((t):string => t.toHexString()); // [ "#ff0000", "#2a0000", "#550000", "#800000", "#aa0000", "#d40000" ]
|
||||
```
|
||||
|
||||
### splitcomplement
|
||||
生成当前颜色的分裂补色。
|
||||
`splitcomplement: function() -> array<TinyColor>`.
|
||||
|
||||
```ts
|
||||
const colors = tinyColor('#f00').splitcomplement();
|
||||
colors.map((t):string => t.toHexString()); // [ "#ff0000", "#ccff00", "#0066ff" ]
|
||||
```
|
||||
|
||||
### triad
|
||||
生成当前颜色的三色调。
|
||||
`triad: function() -> array<TinyColor>`. Alias for `polyad(3)`.
|
||||
|
||||
```ts
|
||||
const colors = tinyColor('#f00').triad();
|
||||
colors.map((t):string => t.toHexString()); // [ "#ff0000", "#00ff00", "#0000ff" ]
|
||||
```
|
||||
|
||||
### tetrad
|
||||
生成当前颜色的四色调。
|
||||
`tetrad: function() -> array<TinyColor>`. Alias for `polyad(4)`.
|
||||
|
||||
```ts
|
||||
const colors = tinyColor('#f00').tetrad();
|
||||
colors.map((t):string => t.toHexString()); // [ "#ff0000", "#80ff00", "#00ffff", "#7f00ff" ]
|
||||
```
|
||||
|
||||
### polyad
|
||||
生成当前颜色的 n 色调。
|
||||
`polyad: function(number) -> array<TinyColor>`.
|
||||
|
||||
```ts
|
||||
const colors = tinyColor('#f00').polyad(4);
|
||||
colors.map((t):string => t.toHexString()); // [ "#ff0000", "#80ff00", "#00ffff", "#7f00ff" ]
|
||||
```
|
||||
|
||||
### complement
|
||||
计算当前颜色的补色。
|
||||
`complement: function() -> TinyColor`.
|
||||
|
||||
```ts
|
||||
tinyColor('#f00').complement().toHexString(); // "#00ffff"
|
||||
```
|
||||
|
||||
## 颜色工具
|
||||
|
||||
### equals
|
||||
判断两色是否相同
|
||||
|
||||
```ts
|
||||
let color1 = tinyColor('red');
|
||||
let color2 = tinyColor('#f00');
|
||||
|
||||
color1.equals(color2); // true
|
||||
```
|
||||
|
||||
|
||||
## 常见操作
|
||||
|
||||
### clone
|
||||
|
||||
`clone: function() -> TinyColor`.
|
||||
使用相同的颜色实例化一个新的`TinyColor`对象。对新的对象的任何更改都不会影响旧的对象。
|
||||
|
||||
```ts
|
||||
const color1 = tinyColor('#F00');
|
||||
const color2 = color1.clone();
|
||||
color2.setAlpha(0.5);
|
||||
|
||||
color1.toString(); // "#ff0000"
|
||||
color2.toString(); // "rgba(255, 0, 0, 0.5)"
|
||||
```
|
||||
|
||||
### generate
|
||||
通过一个主色生成10个等级的颜色数组,使用 Ant Design 的颜色生成算法。
|
||||
|
||||
```ts
|
||||
import {generate, LGenerateOptions} from '@/uni_modules/lime-color'
|
||||
// 第二个参数为选填,如果不填则默认为 'default'
|
||||
generate('red',{ theme: 'default'|'dark'} as LGenerateOptions)
|
||||
// ['#2c1113', '#450f11', '#5b0e0e', '#7e0b0b', '#ad0707', '#dc0303', '#e82d27', '#f3594f', '#f88577', '#faaca0']
|
||||
```
|
||||
|
||||
|
||||
## 打赏
|
||||
|
||||
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
|
||||

|
||||

|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"minSdkVersion": "21"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from '../../common/color'
|
||||
export * from '../../common/generate'
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"deploymentTarget": "9"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from '../../common/color'
|
||||
export * from '../../common/generate'
|
||||
3
qiming-mobile/uni_modules/lime-color/utssdk/index-o.uts
Normal file
3
qiming-mobile/uni_modules/lime-color/utssdk/index-o.uts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from '../common/color'
|
||||
export * from '../common/generate'
|
||||
export * from './interface'
|
||||
87
qiming-mobile/uni_modules/lime-color/utssdk/interface.uts
Normal file
87
qiming-mobile/uni_modules/lime-color/utssdk/interface.uts
Normal file
@@ -0,0 +1,87 @@
|
||||
export type RGB = {
|
||||
r : number;
|
||||
g : number;
|
||||
b : number;
|
||||
}
|
||||
export type RGBA = {
|
||||
r : number;
|
||||
g : number;
|
||||
b : number;
|
||||
a : number;
|
||||
}
|
||||
export type RGBAString = {
|
||||
r : string;
|
||||
g : string;
|
||||
b : string;
|
||||
a : number;
|
||||
}
|
||||
export type HSL = {
|
||||
h : number;
|
||||
s : number;
|
||||
l : number;
|
||||
}
|
||||
|
||||
export type HSLA = {
|
||||
h : number;
|
||||
s : number;
|
||||
l : number;
|
||||
a : number;
|
||||
}
|
||||
export type HSV = {
|
||||
h : number;
|
||||
s : number;
|
||||
v : number;
|
||||
}
|
||||
|
||||
export type HSVA = {
|
||||
h : number;
|
||||
s : number;
|
||||
v : number;
|
||||
a : number;
|
||||
}
|
||||
|
||||
// 增加部分
|
||||
export type HSB = {
|
||||
h : number;
|
||||
s : number;
|
||||
b : number;
|
||||
}
|
||||
export type HSBA = {
|
||||
h : number;
|
||||
s : number;
|
||||
b : number;
|
||||
a : number;
|
||||
}
|
||||
|
||||
export type LColorInfo = {
|
||||
ok ?: boolean;
|
||||
format ?: LColorFormats;
|
||||
r : number;
|
||||
g : number;
|
||||
b : number;
|
||||
a : number;
|
||||
}
|
||||
|
||||
export type LColorFormats =
|
||||
| 'rgb'
|
||||
| 'prgb'
|
||||
| 'hex'
|
||||
| 'hex3'
|
||||
| 'hex4'
|
||||
| 'hex6'
|
||||
| 'hex8'
|
||||
| 'name'
|
||||
| 'hsl'
|
||||
| 'hsb'
|
||||
| 'hsv';
|
||||
|
||||
export type LColorOptions = {
|
||||
format ?: LColorFormats;
|
||||
gradientType ?: string;
|
||||
}
|
||||
export type LColorInput = any //string | number | RGB | RGBA | HSL | HSLA | HSV | HSVA | LimeColor;
|
||||
|
||||
export type LGenerateOptions = {
|
||||
theme ?: 'dark' | 'default';
|
||||
backgroundColor ?: string;
|
||||
}
|
||||
41
qiming-mobile/uni_modules/lime-color/utssdk/unierror.uts
Normal file
41
qiming-mobile/uni_modules/lime-color/utssdk/unierror.uts
Normal file
@@ -0,0 +1,41 @@
|
||||
/* 此规范为 uni 规范,可以按照自己的需要选择是否实现 */
|
||||
import { MyApiErrorCode, MyApiFail } from "./interface.uts"
|
||||
/**
|
||||
* 错误主题
|
||||
* 注意:错误主题一般为插件名称,每个组件不同,需要使用时请更改。
|
||||
* [可选实现]
|
||||
*/
|
||||
export const UniErrorSubject = 'uts-api';
|
||||
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
* @UniError
|
||||
* [可选实现]
|
||||
*/
|
||||
export const UniErrors : Map<MyApiErrorCode, string> = new Map([
|
||||
/**
|
||||
* 错误码及对应的错误信息
|
||||
*/
|
||||
[9010001, 'custom error mseeage1'],
|
||||
[9010002, 'custom error mseeage2'],
|
||||
]);
|
||||
|
||||
|
||||
/**
|
||||
* 错误对象实现
|
||||
*/
|
||||
export class MyApiFailImpl extends UniError implements MyApiFail {
|
||||
|
||||
/**
|
||||
* 错误对象构造函数
|
||||
*/
|
||||
constructor(errCode : MyApiErrorCode) {
|
||||
super();
|
||||
this.errSubject = UniErrorSubject;
|
||||
this.errCode = errCode;
|
||||
this.errMsg = UniErrors[errCode] ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// $prefix: l !default;
|
||||
/* #ifndef APP-NVUE || UNI-APP-X && APP */
|
||||
@font-face {
|
||||
font-family: $prefix;
|
||||
src: url('https://tdesign.gtimg.com/icon/0.3.0/fonts/t.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
/* #ifdef UNI-APP-X && APP */
|
||||
@font-face {
|
||||
font-family: $prefix;
|
||||
/* #ifdef APP-HARMONY */
|
||||
src: url('/uni_modules/lime-icon/hybrid/html/t3.ttf');
|
||||
/* #endif */
|
||||
/* #ifndef APP-HARMONY */
|
||||
src: url('uni_modules/lime-icon/hybrid/html/t3.ttf');
|
||||
/* #endif */
|
||||
}
|
||||
/* #endif */
|
||||
@@ -0,0 +1,27 @@
|
||||
// #ifndef APP-ANDROID || APP-HARMONY
|
||||
import iconList from '../../static/icons.json';
|
||||
export const icons = ref<Map<string, any | null>>((iconList as UTSJSONObject).toMap())
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID || APP-HARMONY
|
||||
export const icons = ref<Map<string, any | null>>(new Map<string, any | null>())
|
||||
|
||||
if (icons.value.size == 0) {
|
||||
uni.getFileSystemManager().readFile({
|
||||
// #ifdef APP-HARMONY
|
||||
filePath: '/uni_modules/lime-icon/static/icons.json',
|
||||
// #endif
|
||||
// #ifndef APP-HARMONY
|
||||
filePath: 'uni_modules/lime-icon/static/icons.json',
|
||||
// #endif
|
||||
encoding: 'utf-8',
|
||||
success: (res) => {
|
||||
const obj = JSON.parseObject(res.data as string)
|
||||
if (obj == null) return
|
||||
icons.value = obj!.toMap();
|
||||
},
|
||||
fail(err) {
|
||||
console.log('[lime-icon getFileSystemManager]', err)
|
||||
}
|
||||
} as ReadFileOptions);
|
||||
}
|
||||
// #endif
|
||||
@@ -0,0 +1,64 @@
|
||||
// 公共前缀
|
||||
@import '@/uni_modules/lime-style/index.scss';
|
||||
@import './icon';
|
||||
|
||||
/* #ifdef uniVersion >= 4.75 */
|
||||
$use-css-var: true;
|
||||
/* #endif */
|
||||
|
||||
$prefix: l !default;
|
||||
$icon: #{$prefix}-icon;
|
||||
|
||||
/* #ifdef APP-NVUE || UNI-APP-X && APP */
|
||||
$icon-size: create-var(icon-size, 16px);
|
||||
$icon-color: create-var(icon-color, $text-color-1);
|
||||
/* #endif */
|
||||
|
||||
/* #ifndef APP-NVUE || UNI-APP-X && APP */
|
||||
$icon-size: create-var(icon-size, 1em);
|
||||
$icon-color: create-var(icon-color, currentColor);
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
|
||||
.#{$icon} {
|
||||
/* #ifndef APP-NVUE || UNI-APP-X && APP */
|
||||
font-family: $prefix;
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
/* #endif */
|
||||
|
||||
&--font {
|
||||
font-family: $prefix;
|
||||
text-align: center;
|
||||
font-size: $icon-size;
|
||||
color: $icon-color;
|
||||
/* #ifndef APP-NVUE || UNI-APP-X && APP */
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
// -webkit-background-clip: text;
|
||||
// background-clip: text;
|
||||
/* #endif */
|
||||
}
|
||||
&--image {
|
||||
width: $icon-size;
|
||||
height: $icon-size;
|
||||
/* #ifndef APP-NVUE || UNI-APP-X && APP */
|
||||
display: block;
|
||||
/* #endif */
|
||||
/* #ifdef WEB */
|
||||
position: relative;
|
||||
::deep(img) {
|
||||
z-index: -1;
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<text :key="iconCode" class="l-icon" :class="[classes, lClass]" :style="[styles, lStyle]" v-if="!isImage && !isIconify && !isSVG">{{iconCode}}</text>
|
||||
<image class="l-icon" :class="[classes, lClass]" :style="[styles, lStyle]" v-else-if="(!isSVG && !isIconify) && isImage" :src="iconUrl"></image>
|
||||
<l-svg class="l-icon" :class="[classes, lClass]" :style="[styles, lStyle]" :color="color" :src="iconUrl" v-else :web="web" @error="imageError" @load="imageload"></l-svg>
|
||||
</template>
|
||||
<script lang="uts" setup>
|
||||
/**
|
||||
* LimeIcon 图标
|
||||
* @description ICON集
|
||||
* <br> 插件类型:LIconComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=14057
|
||||
* @property {String} name 图标名称
|
||||
* @property {String} color 颜色
|
||||
* @property {String} size 尺寸
|
||||
* @property {String} prefix 字体图标前缀
|
||||
* @property {Boolean} inherit 是否继承颜色
|
||||
* @property {Boolean} web 原生 app(nvue,uvue) 是否使用web渲染
|
||||
* @event {Function} click 点击事件
|
||||
*/
|
||||
|
||||
import { addUnit } from '@/uni_modules/lime-shared/addUnit';
|
||||
import { IconCollection } from './types';
|
||||
import { icons } from './icons'
|
||||
// defineOptions({
|
||||
// name: 'l-icon'
|
||||
// })
|
||||
const name = 'l-icon'
|
||||
const IconifyURL : string = 'https://api.iconify.design/';
|
||||
const $iconsHost : string | null = uni.getStorageSync('$limeIconsHost') as string | null
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: true,
|
||||
// validator: (value: string) : boolean => {
|
||||
// // 确保是字符串类型且不为空
|
||||
// return typeof value == 'string' && value.trim().length > 0
|
||||
// }
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
// default: ''
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
lClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 对安卓IOS无效
|
||||
inherit: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
web: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
lStyle: {
|
||||
type: [String, Object, Array],
|
||||
default: ''
|
||||
},
|
||||
})
|
||||
|
||||
// const emits = defineEmits(['click'])
|
||||
const $iconCollection = inject<IconCollection>('$iconCollection', {has: false, icons: new Map<string, any|null>()} as IconCollection)
|
||||
// #ifndef APP-ANDROID
|
||||
const innerName = computed(():string => props.name ?? '')
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
const innerName = computed(():string => props.name)
|
||||
// #endif
|
||||
const collectionIcon = computed(():string|null => {
|
||||
return $iconCollection.icons.get(innerName.value) as string | null
|
||||
})
|
||||
const webviewRef = ref<UniWebViewElement | null>(null)
|
||||
const hasHost = computed<boolean>(() : boolean => innerName.value.indexOf('/') != -1)
|
||||
const isIconify = computed<boolean>(() : boolean => {
|
||||
return !hasHost.value && innerName.value.includes(':')
|
||||
})
|
||||
const isImage = computed<boolean>(() : boolean => {
|
||||
return /\.(jpe?g|png|gif|bmp|webp|tiff?)$/i.test(innerName.value) || /^data:image\/(jpeg|png|gif|bmp|webp|tiff);base64,/.test(innerName.value)
|
||||
})
|
||||
const isSVG = computed<boolean>(():boolean => {
|
||||
return /\.svg$/i.test(innerName.value) || innerName.value.startsWith('data:image/svg+xml') || innerName.value.startsWith('<svg')
|
||||
})
|
||||
const classes = computed<Map<string, any>>(() : Map<string, any> => {
|
||||
const cls = new Map<string, any>()
|
||||
cls.set(`${name}--font`, !isImage.value && !isIconify.value && !isSVG.value)
|
||||
cls.set(`${name}--image`, isImage.value || isIconify.value || isSVG.value)
|
||||
cls.set(props.prefix, props.prefix.length > 0)
|
||||
cls.set(props.lClass, props.lClass.length > 0)
|
||||
// #ifndef APP-ANDROID || APP-IOS || APP-HARMONY
|
||||
cls.set(`is-inherit`, (isIconify.value) && (props.color && props.color.length > 0 || props.inherit))
|
||||
// #endif
|
||||
return cls
|
||||
})
|
||||
const styles = computed<Map<string, any>>(() : Map<string, any> => {
|
||||
const style = new Map<string, any>();
|
||||
const size = addUnit(props.size)
|
||||
// #ifdef APP
|
||||
if ((props.color != '' && props.color != null) && !isImage.value && !isIconify.value) {
|
||||
style.set('color', props.color!)
|
||||
}
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
if(props.color) {
|
||||
style.set('color', props.color!)
|
||||
}
|
||||
// #endif
|
||||
if (size != null) {
|
||||
if (isImage.value || isIconify.value || isSVG.value) {
|
||||
style.set('height', size)
|
||||
style.set('width', size)
|
||||
} else {
|
||||
style.set('font-size', size)
|
||||
}
|
||||
|
||||
}
|
||||
return style
|
||||
})
|
||||
const iconCode = computed<string>(() : string => {
|
||||
return icons.value.get(innerName.value) as string | null ?? (/[^\x00-\x7F]/.test(innerName.value) ? innerName.value : '')
|
||||
})
|
||||
const isError = ref(false)
|
||||
const cacheMap = new Map<string, string>()
|
||||
const iconUrl = computed(():string => {
|
||||
const hasIconsHost = $iconsHost != null && $iconsHost != ''
|
||||
// const hasIconCollection = $iconCollection.has
|
||||
if(isImage.value) {
|
||||
return hasHost.value ? innerName.value : ($iconsHost ?? '') + innerName.value
|
||||
} else if(isIconify.value) {
|
||||
// 防止重绘
|
||||
if(cacheMap.has(innerName.value) && !isError.value) {
|
||||
return cacheMap.get(innerName.value)!
|
||||
}
|
||||
// 如果存在collectionIcon则使用
|
||||
// 如果设置的路径加载失败 就使用网络地址 就使用iconify api
|
||||
const _host = `${hasIconsHost ? $iconsHost : IconifyURL}`
|
||||
const _icon =collectionIcon.value ?? _host + `${innerName.value}.svg`.replace(/:/g, '/')
|
||||
cacheMap.set(innerName.value, _icon)
|
||||
return _icon
|
||||
} else if(isSVG.value) {
|
||||
return (/\.svg$/i.test(innerName.value) && $iconsHost != null && !hasHost.value ? $iconsHost : '') + innerName.value.replace(/'/g, '"')
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const imageError = () => {
|
||||
isError.value = true
|
||||
}
|
||||
const imageload = () => {
|
||||
isError.value = false
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import './index.scss';
|
||||
</style>
|
||||
151
qiming-mobile/uni_modules/lime-icon/components/l-icon/l-icon.vue
Normal file
151
qiming-mobile/uni_modules/lime-icon/components/l-icon/l-icon.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<text class="l-icon" :class="[classes]" :style="[styles, lStyle]" v-if="!isImage && !isIconify && !isSVG" @click="$emit('click')">{{iconCode}}</text>
|
||||
<image class="l-icon" :class="[classes]" :style="[styles, lStyle]" v-else-if="(!isSVG && !isIconify) && isImage" :src="iconUrl" @click="$emit('click')"></image>
|
||||
<l-svg class="l-icon" :class="[classes]" :style="[styles, lStyle]" v-else :web="web" :color="color" :src="iconUrl" @error="imageError" @load="imageLoad" @click="$emit('click')"></l-svg>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* LimeIcon 图标
|
||||
* @description ICON集
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=14057
|
||||
* @property {String} name 图标名称
|
||||
* @property {String} color 颜色
|
||||
* @property {String} size 尺寸
|
||||
* @property {String} prefix 字体图标前缀
|
||||
* @property {Boolean} inherit 是否继承颜色
|
||||
* @property {Boolean} web 原生 app(nvue,uvue) 是否使用web渲染
|
||||
* @event {Function} click 点击事件
|
||||
*/
|
||||
// @ts-nocheck
|
||||
import { computed, defineComponent, ref, inject } from '@/uni_modules/lime-shared/vue';
|
||||
import icons from '../../static/icons.json';
|
||||
import { addUnit } from '@/uni_modules/lime-shared/addUnit';
|
||||
|
||||
import { isObject } from '@/uni_modules/lime-shared/isObject';
|
||||
import IconProps from './props';
|
||||
|
||||
// #ifdef VUE2 && MP
|
||||
import { getClassStr } from '@/uni_modules/lime-shared/getClassStr';
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
import iconSrc from '@/uni_modules/lime-icon/hybrid/html/t3.ttf';
|
||||
var domModule = weex.requireModule('dom');
|
||||
domModule.addRule('fontFace', {
|
||||
'fontFamily': "uniicons",
|
||||
'src': "url('" + iconSrc + "')"
|
||||
});
|
||||
// #endif
|
||||
|
||||
const name = 'l-icon';
|
||||
export default defineComponent({
|
||||
name,
|
||||
externalClasses: ['l-class'],
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
},
|
||||
props: IconProps,
|
||||
emits: ['click'],
|
||||
setup(props, { emit }) {
|
||||
const $iconCollection = inject('$iconCollection', null)
|
||||
const { $limeIconsHost: $iconsHost } = uni as any
|
||||
const IconifyURL = 'https://api.iconify.design/'
|
||||
// const isAPP = uni.getSystemInfoSync().uniPlatform == 'app'
|
||||
const innerName = computed(():string => props.name || '')
|
||||
const hasHost = computed(() => `${innerName.value}`.indexOf('/') !== -1);
|
||||
const isIconify = computed(() => !hasHost.value && `${innerName.value}`.includes(':'))
|
||||
const collectionIcon = computed(() => isObject($iconCollection) && $iconCollection.icons[innerName.value])
|
||||
const isImage = computed<boolean>(() : boolean => {
|
||||
return /\.(jpe?g|png|gif|bmp|webp|tiff?)$/i.test(innerName.value) || /^data:image\/(jpeg|png|gif|bmp|webp|tiff);base64,/.test(innerName.value)
|
||||
})
|
||||
const isSVG = computed<boolean>(() : boolean => {
|
||||
return /\.svg$/i.test(innerName.value) || innerName.value.startsWith('data:image/svg+xml') || innerName.value.startsWith('<svg')
|
||||
})
|
||||
const classes = computed(() => {
|
||||
const { prefix } = props
|
||||
const iconPrefix = prefix || name
|
||||
const iconName = `${iconPrefix}-${innerName.value}`
|
||||
const isFont = !isImage.value && !isIconify.value && !isSVG.value
|
||||
const isImages = isImage.value || isIconify.value || isSVG.value
|
||||
const cls = {
|
||||
[iconPrefix]: !isImages && prefix,
|
||||
[iconName]: !isImages,
|
||||
[`${name}--image`]: isImages,
|
||||
[`${name}--font`]: isFont,
|
||||
// [`is-inherit`]: isIconify.value && (props.color || props.inherit)
|
||||
}
|
||||
|
||||
// #ifdef VUE2 && MP
|
||||
return getClassStr(cls)
|
||||
// #endif
|
||||
|
||||
return cls
|
||||
})
|
||||
const iconCode = computed(() => {
|
||||
const isImages = isImage.value || isIconify.value || isSVG.value
|
||||
return (!isImages && icons[innerName.value]) || (/[^\x00-\x7F]/.test(innerName.value) ? innerName.value : '')
|
||||
})
|
||||
const isError = ref(false)
|
||||
const cacheMap = new Map<string, string>()
|
||||
const iconUrl = computed(() => {
|
||||
const hasIconsHost = $iconsHost != null && $iconsHost != ''
|
||||
// const hasIconCollection = $iconCollectiont != null
|
||||
if (isImage.value) {
|
||||
return hasHost.value ? innerName.value : ($iconsHost || '') + innerName.value
|
||||
} else if (isIconify.value) {
|
||||
// 防止重绘
|
||||
if(cacheMap.has(innerName.value) && !isError.value) {
|
||||
return cacheMap.get(innerName.value)!
|
||||
}
|
||||
// 如果存在collectionIcon则使用
|
||||
// 如果设置的路径加载失败 就使用网络地址 就使用iconify api
|
||||
// !isError.value &&
|
||||
const _host = `${hasIconsHost ? $iconsHost : IconifyURL}`
|
||||
const _icon = collectionIcon.value || _host + `${innerName.value}.svg`.replace(/:/g, '/')
|
||||
cacheMap.set(innerName.value, _icon)
|
||||
return _icon
|
||||
} else if (isSVG.value) {
|
||||
return (/\.svg$/i.test(innerName.value) && hasIconsHost && !hasHost.value ? $iconsHost : '') + innerName.value.replace(/'/g, '"')
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
const styles = computed(() => {
|
||||
const style : Record<string, any> = {
|
||||
'color': props.color,
|
||||
}
|
||||
if (typeof props.size == 'number' || props.size) {
|
||||
style['font-size'] = addUnit(props.size)
|
||||
}
|
||||
//#ifdef VUE2
|
||||
// VUE2小程序最后一个值莫名的出现undefined
|
||||
style['undefined'] = ''
|
||||
// #endif
|
||||
return style
|
||||
})
|
||||
const imageLoad = () => {
|
||||
isError.value = false
|
||||
}
|
||||
const imageError = () => {
|
||||
isError.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
iconCode,
|
||||
classes,
|
||||
styles,
|
||||
isImage,
|
||||
isSVG,
|
||||
isIconify,
|
||||
iconUrl,
|
||||
imageLoad,
|
||||
imageError
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import './index.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
export const ariaProps = {
|
||||
ariaHidden: Boolean,
|
||||
ariaRole: String,
|
||||
ariaLabel: String,
|
||||
ariaLabelledby: String,
|
||||
ariaDescribedby: String,
|
||||
ariaBusy: Boolean,
|
||||
// lStyle: String
|
||||
}
|
||||
|
||||
export default {
|
||||
...ariaProps,
|
||||
lClass: String,
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: String,
|
||||
size: [String, Number],
|
||||
prefix: String,
|
||||
// type: String,
|
||||
inherit: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
web: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
lStyle:[String, Object, Array],
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export type IconCollection = {
|
||||
has: boolean,
|
||||
icons: Map<string, any|null>
|
||||
}
|
||||
2
qiming-mobile/uni_modules/lime-icon/generate-icons.js
Normal file
2
qiming-mobile/uni_modules/lime-icon/generate-icons.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const { generate } = require('./utils/generate.js');
|
||||
generate()
|
||||
BIN
qiming-mobile/uni_modules/lime-icon/hybrid/html/t3.ttf
Normal file
BIN
qiming-mobile/uni_modules/lime-icon/hybrid/html/t3.ttf
Normal file
Binary file not shown.
111
qiming-mobile/uni_modules/lime-icon/index.uts
Normal file
111
qiming-mobile/uni_modules/lime-icon/index.uts
Normal file
@@ -0,0 +1,111 @@
|
||||
// @ts-nocheck
|
||||
import {IconCollection} from './components/l-icon/types'
|
||||
// #ifndef UNI-APP-X
|
||||
|
||||
// #ifdef VUE3
|
||||
import { reactive } from 'vue';
|
||||
function definePlugin(options: any) {
|
||||
return options
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE2
|
||||
import Vue from 'vue'
|
||||
let reactive = Vue.observable
|
||||
type VueApp = any
|
||||
// #endif
|
||||
|
||||
type UTSJSONObject = any//Record<string, any>
|
||||
|
||||
// #endif
|
||||
|
||||
let topApp : VueApp | null = null;
|
||||
const _iconCollection = reactive<IconCollection>({
|
||||
has: true,
|
||||
// #ifdef UNI-APP-X
|
||||
icons: new Map<string, any|null>()
|
||||
// #endif
|
||||
|
||||
// #ifndef UNI-APP-X
|
||||
icons: {}
|
||||
// #endif
|
||||
})
|
||||
|
||||
|
||||
export function useIconHost(iconHost : string) {
|
||||
// #ifdef UNI-APP-X
|
||||
uni.setStorageSync('$limeIconsHost', iconHost)
|
||||
// #endif
|
||||
// #ifndef UNI-APP-X
|
||||
uni.$limeIconsHost = iconHost
|
||||
// #endif
|
||||
}
|
||||
let isInstall = false
|
||||
export function useIconCollection(iconCollection: UTSJSONObject|null = {}) {
|
||||
if(!isInstall) {
|
||||
console.warn('[lime-icon]: useIconCollection 请先注册,app.use(limeIcons, null, iconjson)')
|
||||
return
|
||||
}
|
||||
// #ifdef UNI-APP-X
|
||||
const map = (iconCollection as UTSJSONObject).toMap()
|
||||
if(map.size != 0) {
|
||||
uni.setStorageSync('$limeIconCollection', iconCollection)
|
||||
_iconCollection.icons = map
|
||||
}
|
||||
// #endif
|
||||
// #ifndef UNI-APP-X
|
||||
if(Object.keys(iconCollection).length != 0) {
|
||||
uni.setStorageSync('$limeIconCollection', iconCollection)
|
||||
_iconCollection.icons = iconCollection
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
function useProvide() {
|
||||
if(topApp == null) return
|
||||
isInstall = true
|
||||
// #ifdef VUE3
|
||||
topApp!.provide('$iconCollection', _iconCollection)
|
||||
// #endif
|
||||
// #ifndef VUE3
|
||||
topApp.mixin({
|
||||
provide: {
|
||||
$iconCollection: _iconCollection
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
// #ifdef VUE3
|
||||
export const limeIcons = definePlugin({
|
||||
install: (app: VueApp, iconHost: string | null, iconCollection: UTSJSONObject | null):void => {
|
||||
topApp = app;
|
||||
if(iconHost != null || iconHost != '') {
|
||||
useIconHost(iconHost!)
|
||||
}
|
||||
if(iconCollection != null) {
|
||||
useProvide()
|
||||
useIconCollection(iconCollection)
|
||||
}
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef VUE2
|
||||
export const limeIcons = {
|
||||
install: (app: any, options: any[]) => {
|
||||
topApp = app;
|
||||
let [iconHost, iconCollection] = options
|
||||
if(iconHost != null && typeof iconHost == 'object') {
|
||||
iconCollection = iconHost
|
||||
}
|
||||
if(iconHost && iconHost != '') {
|
||||
useIconHost(iconHost!)
|
||||
}
|
||||
if(iconCollection != null) {
|
||||
useProvide()
|
||||
useIconCollection(iconCollection)
|
||||
}
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
23
qiming-mobile/uni_modules/lime-icon/lime-icons.config.js
Normal file
23
qiming-mobile/uni_modules/lime-icon/lime-icons.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// lime.config.js
|
||||
module.exports = {
|
||||
// 输入的文件目录,自有的SVG,如果没有则不需要
|
||||
input: {
|
||||
prefix: "my-icons",
|
||||
dir: '/static/svg',
|
||||
},
|
||||
// 输出的配置
|
||||
output: {
|
||||
// 输出的文件目录
|
||||
dir: '/static/icon',
|
||||
// 输出的文件的格式,如果是JSON则是一个图标合集
|
||||
// file: 'icons.json',
|
||||
// 如果是SVG则是每个图标做为单独的文件
|
||||
file: '*.svg',
|
||||
},
|
||||
icons: [
|
||||
'el:address-book',
|
||||
'uil:12-plus',
|
||||
'icon-park-outline:abdominal',
|
||||
'icon-park-outline:acoustic'
|
||||
]
|
||||
}
|
||||
110
qiming-mobile/uni_modules/lime-icon/package.json
Normal file
110
qiming-mobile/uni_modules/lime-icon/package.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"id": "lime-icon",
|
||||
"displayName": "lime-icon 图标 iconify 图标集合",
|
||||
"version": "0.3.7",
|
||||
"description": "lime-icon 图标插件可方便快捷按需的使用iconify图标,iconify超过150,000个开源矢量图标,插件内置tdesign icon。使用兼容uniapp/uniappx",
|
||||
"keywords": [
|
||||
"icon",
|
||||
"iconify",
|
||||
"图标集合",
|
||||
"按需加载"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.8.7",
|
||||
"uni-app": "^4.54",
|
||||
"uni-app-x": "^4.61"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"lime-style",
|
||||
"lime-shared",
|
||||
"lime-svg"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "√",
|
||||
"vue3": "√"
|
||||
},
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"vue": "√",
|
||||
"nvue": "-",
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√",
|
||||
"alipay": "√",
|
||||
"toutiao": "-",
|
||||
"baidu": "-",
|
||||
"kuaishou": "-",
|
||||
"jd": "-",
|
||||
"harmony": "-",
|
||||
"qq": "-",
|
||||
"lark": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "-",
|
||||
"union": "-"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
231
qiming-mobile/uni_modules/lime-icon/readme_old.md
Normal file
231
qiming-mobile/uni_modules/lime-icon/readme_old.md
Normal file
@@ -0,0 +1,231 @@
|
||||
# lime-icon 图标
|
||||
图标组件,方便快捷地使用[iconify](https://iconify.design/)图标集合,提供超过150,000个开源矢量图标。支持自定义颜色、大小、前缀等属性,还可以使用自定义图标和图标URL。
|
||||
|
||||
> 注意:插件依赖的`lime-svg`为收费插件,若不需要svg功能,删除svg插件即可。
|
||||
|
||||
## 文档链接
|
||||
📚 组件详细文档请访问以下站点:
|
||||
- [图标文档 - 站点1](https://limex.qcoon.cn/components/icon.html)
|
||||
- [图标文档 - 站点2](https://limeui.netlify.app/components/icon.html)
|
||||
- [图标文档 - 站点3](https://limeui.familyzone.top/components/icon.html)
|
||||
|
||||
## 安装方法
|
||||
1. 在uni-app插件市场中搜索并导入`lime-icon`
|
||||
2. 导入后可能需要重新编译项目
|
||||
3. 在页面中使用`l-icon`组件(组件)或`lime-icon`(演示)
|
||||
|
||||
::: tip 注意🔔
|
||||
本插件依赖的[【lime-svg】](https://ext.dcloud.net.cn/plugin?id=18519)是原生插件,如果购买(收费为6元)则需要自定义基座,才能使用,
|
||||
若不需要删除即可
|
||||
:::
|
||||
|
||||
## 代码演示
|
||||
|
||||
### 基础使用
|
||||
使用`name`属性指定要显示的图标。👉️[【全部图标】](#全部图标)
|
||||
|
||||
```html
|
||||
<l-icon name="circle" />
|
||||
```
|
||||
|
||||
### 使用Iconify
|
||||
到 [icones](https://icones.js.org/) 网站找到需要的图标,通过 `name` 属性来指定需要使用的图标
|
||||
|
||||
```html
|
||||
<l-icon name="ri:account-box-fill" />
|
||||
<l-icon name="icon-park-outline:acoustic" />
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
### 使用图标URL
|
||||
```html
|
||||
<l-icon name="https://fastly.jsdelivr.net/npm/@vant/assets/icon-demo.png"></l-icon>
|
||||
```
|
||||
|
||||
### 图标颜色
|
||||
通过 `color` 属性来设置图标的颜色。
|
||||
|
||||
```html
|
||||
<l-icon name="ri:aliens-fill" color="#1989fa" />
|
||||
<l-icon name="icon-park-outline:acoustic" color="#ee0a24" />
|
||||
```
|
||||
|
||||
### 图标大小
|
||||
|
||||
通过 `size` 属性来设置图标的尺寸大小,可以指定任意 CSS 单位。
|
||||
|
||||
```html
|
||||
<!-- 不指定单位,默认使用 px -->
|
||||
<l-icon name="ri:aliens-fill" size="40" />
|
||||
<!-- 指定使用 rpx 单位 -->
|
||||
<l-icon name="ri:aliens-fill" size="34rpx" />
|
||||
```
|
||||
|
||||
|
||||
### 自定义图标
|
||||
通过`prefix`设置iconfot图标类,通过`name`传入`Unicode`字符
|
||||
```html
|
||||
<l-icon size="30px" prefix="keyicon" :name="`\uE6EF`" color="blue"></l-icon>
|
||||
```
|
||||
```css
|
||||
@font-face {
|
||||
font-family: keyicon;
|
||||
src: url('https://at.alicdn.com/t/c/font_4741157_ul7wcp52yys.ttf');
|
||||
}
|
||||
.keyicon {
|
||||
font-family: keyicon;
|
||||
}
|
||||
```
|
||||
|
||||
## 私有化iconify
|
||||
默认会使用`iconify`的API,如果你想私有化可按以下步骤来
|
||||
### 第一步 安装
|
||||
|
||||
```cmd
|
||||
yarn add @iconify/json @iconify/tools @iconify/utils
|
||||
```
|
||||
### 第二步 配置
|
||||
- 需要在根目录新建一个`lime-icons.config.js`文件
|
||||
|
||||
```
|
||||
// 在根目录新建一个lime-icons.config.js文件
|
||||
// lime-icons.config.js
|
||||
module.exports = {
|
||||
// 输入的文件目录,自有的SVG,如果没有则不需要
|
||||
input: {
|
||||
prefix: "my-icons",
|
||||
dir: '/static/svg',
|
||||
},
|
||||
// 输出的配置
|
||||
output: {
|
||||
// 输出的文件目录
|
||||
dir: '/static/icons',
|
||||
// 输出的文件的格式,如果是JSON则是一个图标合集
|
||||
// file: 'icons.json',
|
||||
// 如果是SVG则是每个图标做为单独的文件
|
||||
file: '*.svg',
|
||||
},
|
||||
// 指定使用的图标
|
||||
icons: [
|
||||
'el:address-book',
|
||||
'uil:12-plus',
|
||||
'icon-park-outline:abdominal',
|
||||
'icon-park-outline:acoustic'
|
||||
]
|
||||
}
|
||||
```
|
||||
在终端执行脚本
|
||||
```
|
||||
node ./uni_modules/lime-icon/generate-icons.js
|
||||
```
|
||||
|
||||
### ~~2、自动引入~~
|
||||
~~如果使用的是`vue3`,通过配置 `vite.config.js` 达到自动引入~~
|
||||
这个方法作废,因有些图标是动态的,在编译阶段不知道图标的名称无法捕获
|
||||
```js
|
||||
import uni from '@dcloudio/vite-plugin-uni';
|
||||
import limeIcon from './uni_modules/lime-icon/vite-plugin';
|
||||
import path from 'path'
|
||||
export default defineConfig({
|
||||
plugins: [uni(), limeIcon({
|
||||
// 输出的配置
|
||||
output: {
|
||||
// 输出的文件目录
|
||||
dir: path.join(__dirname, '/static/icons'),
|
||||
// 输出的文件的格式,如果是JSON则是生成一个图标合集, 例如: /static/icons/icons.json
|
||||
// file: 'icons.json',
|
||||
// 如果是SVG则是每个图标做为单独的文件 例如: /static/icons/xx/xxx.svg
|
||||
file: '*.svg',
|
||||
},
|
||||
// 可选
|
||||
icons: []
|
||||
})]
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
|
||||
### 第三步 挂载图标地址
|
||||
|
||||
> 注意:如果使用了`iconify` 的API, 小程序需要去公众平台设置下载白名单 `https://api.iconify.design`
|
||||
```js
|
||||
// main.js | main.ts | main.uts
|
||||
// 配置svg指定路径,后期可上传到后端,不占用本地空间,如果使用的是`iconify`也可以不配置这一步
|
||||
import {limeIcons} from '@/uni_modules/lime-icon'
|
||||
|
||||
// 第一个参数是icon host地址,没有则填null
|
||||
// 第二个参数是icons json合集,没有则填null
|
||||
// app.use(limeIcons, null, null)
|
||||
|
||||
// 示例1 配置icons地址
|
||||
app.use(limeIcons, 'https://xxx.cn/static/icons', null)
|
||||
|
||||
// 示例2 配置icons集合json
|
||||
import icons from './static/icons/icons.json'
|
||||
app.use(limeIcons, null, icons)
|
||||
```
|
||||
|
||||
## 快速预览
|
||||
导入插件后,可以直接使用以下标签查看演示效果:
|
||||
|
||||
```html
|
||||
<!-- 代码位于 uni_modules/lime-icon/components/lime-icon -->
|
||||
<lime-icon />
|
||||
```
|
||||
|
||||
## 插件标签说明
|
||||
- `l-icon`: 组件标签,用于实际开发中
|
||||
- `lime-icon`: 演示标签,用于查看示例效果
|
||||
## Vue2使用说明
|
||||
本插件使用了`composition-api`,如需在Vue2项目中使用,请按照[官方教程](https://uniapp.dcloud.net.cn/tutorial/vue-composition-api.html)配置。
|
||||
|
||||
关键配置代码(在main.js中添加):
|
||||
|
||||
```js
|
||||
// vue2
|
||||
import Vue from 'vue'
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
|
||||
// 配置svg指定路径,后期可上传到后端,不占用本地空间,如果使用的是`iconify`也可以不配置这一步
|
||||
import {limeIcons} from '@/uni_modules/lime-icon'
|
||||
|
||||
Vue.use(VueCompositionAPI)
|
||||
|
||||
// 示例1 配置icons地址
|
||||
Vue.use(limeIcons, ['https://xxx.cn/static/icons', null])
|
||||
|
||||
// 示例2 配置icons集合json
|
||||
import icons from './static/icons/icons.json'
|
||||
Vue.use(limeIcons, [null, icons])
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --------------------------| ------------------------------------------------------------ | ---------------- | ------------ |
|
||||
| name | 图标名称 | <em>string</em> | `` |
|
||||
| color | 颜色 | <em>string</em> | `` |
|
||||
| size | 尺寸 | <em>string</em> | `square` |
|
||||
| prefix | 字体图标前缀 | <em>string</em> | `` |
|
||||
| inherit | 是否继承颜色 | <em>boolean</em> | `true` |
|
||||
| web | 原生`app(nvue,uvue)`是否使用web渲染 | <em>boolean</em> | `false` |
|
||||
|
||||
### Events
|
||||
| 参数 | 说明 | 参数 |
|
||||
| --------------------------| ------------------------------------------------------------ | ---------------- |
|
||||
| click | 点击 | |
|
||||
|
||||
|
||||
## 打赏
|
||||
|
||||
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
|
||||

|
||||

|
||||
2116
qiming-mobile/uni_modules/lime-icon/static/icons.json
Normal file
2116
qiming-mobile/uni_modules/lime-icon/static/icons.json
Normal file
File diff suppressed because it is too large
Load Diff
120
qiming-mobile/uni_modules/lime-icon/utils/generate.js
Normal file
120
qiming-mobile/uni_modules/lime-icon/utils/generate.js
Normal file
@@ -0,0 +1,120 @@
|
||||
const path = require('path');
|
||||
const fs = require("fs");
|
||||
const rootPath = process.cwd(); // 获取根目录
|
||||
const { importDirectory, blankIconSet } = require("@iconify/tools");
|
||||
const { locate } = require('@iconify/json');
|
||||
const { getIconData } = require('@iconify/utils');
|
||||
const { encodeSvg, saveFile, customOptions, deleteDirectory } = require('./index.js')
|
||||
async function fetchIconsData(icons) {
|
||||
const collections = {}
|
||||
for (const iconName of icons) {
|
||||
const [collectionName, iconNameWithoutPrefix] = iconName.split(':');
|
||||
const filename = locate(collectionName)
|
||||
if(!fs.existsSync(filename)) {
|
||||
continue
|
||||
}
|
||||
const icons = JSON.parse(fs.readFileSync(filename, 'utf8'))
|
||||
if(!icons) {
|
||||
continue
|
||||
}
|
||||
if(collectionName && iconNameWithoutPrefix) {
|
||||
const iconData = getIconData(icons, iconNameWithoutPrefix);
|
||||
if(iconData) {
|
||||
if(!collections[collectionName]) {
|
||||
collections[collectionName] = blankIconSet(collectionName);
|
||||
}
|
||||
collections[collectionName].setIcon(iconNameWithoutPrefix, iconData);
|
||||
} else {
|
||||
console.log(`Icon '${iconName}' not found in '${collectionName}' collection.`)
|
||||
}
|
||||
} else if(collectionName) {
|
||||
if(!collections[collectionName]) {
|
||||
collections[collectionName] = blankIconSet(collectionName)
|
||||
}
|
||||
Object.keys(icons.icons).forEach(iconName => {
|
||||
const iconData = getIconData(icons, iconName)
|
||||
if(iconData) {
|
||||
collections[collectionName].setIcon(iconName, iconData)
|
||||
} else {
|
||||
console.log(`Icon '${iconName}' not found in '${collectionName}' collection.`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return collections;
|
||||
}
|
||||
|
||||
async function generate(config){
|
||||
try {
|
||||
if(!config) {
|
||||
// 从配置文件中读取选项
|
||||
const rootConfigPath = path.join(rootPath, 'lime-icons.config.js');
|
||||
let configPath = ''
|
||||
if(fs.existsSync(rootConfigPath)) {
|
||||
configPath = rootConfigPath
|
||||
} else {
|
||||
configPath = path.dirname(__filename) + '/lime-icons.config.js'; // 配置文件路径
|
||||
}
|
||||
|
||||
const configFile = fs.readFileSync(configPath, 'utf8');
|
||||
config = eval(`(${configFile})`);
|
||||
}
|
||||
|
||||
// 根据配置文件中的字段设置选项
|
||||
const options = {
|
||||
input: Object.assign({}, customOptions, config.input || {}), // 输入的文件目录
|
||||
output: {
|
||||
dir: config.output.dir || '/static', // 输出的文件目录
|
||||
file: config.output.file || 'icons.json', // 输出的文件的格式,默认为 JSON
|
||||
},
|
||||
icons: config.icons || [], // 图标名称列表
|
||||
};
|
||||
|
||||
// 先删除原来的
|
||||
deleteDirectory(options.output.dir)
|
||||
// 处理输入目录的逻辑
|
||||
if (config.input.dir.startsWith('/')) {
|
||||
options.input.dir = path.join(rootPath, config.input.dir);
|
||||
} else if (config.input.dir.startsWith('./')) {
|
||||
options.input.dir = path.join(__dirname, config.input.dir.slice(2));
|
||||
}
|
||||
let iconCollections = {}
|
||||
// 异步地从目录中导入图标
|
||||
if(fs.existsSync(options.input.dir)) {
|
||||
const iconSet = await importDirectory(options.input.dir, options.input);
|
||||
// 导出为 JSON 文件
|
||||
iconCollections[options.input.prefix] = iconSet
|
||||
}
|
||||
|
||||
// 获取指定图标的数据
|
||||
if(options.icons.length) {
|
||||
const iconCollection = await fetchIconsData(options.icons);
|
||||
Object.assign(iconCollections, iconCollection)
|
||||
}
|
||||
|
||||
if(/\.json$/i.test(options.output.file)) {
|
||||
const collections = {}
|
||||
Object.values(iconCollections).forEach((iconSet) => {
|
||||
|
||||
iconSet.forEach(iconName => {
|
||||
// 将 SVG 转换为 Data URL
|
||||
collections[iconSet.prefix + ':' + iconName] = `data:image/svg+xml;utf8,${encodeSvg(iconSet.toString(iconName))}`
|
||||
})
|
||||
})
|
||||
await saveFile(`${options.output.dir}/${options.output.file}`, JSON.stringify(collections))
|
||||
} else {
|
||||
Object.values(iconCollections).forEach((iconSet) => {
|
||||
iconSet.forEach(async iconName => {
|
||||
await saveFile(`${options.output.dir}/${iconSet.prefix}/${iconName}.svg`, iconSet.toString(iconName))
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("导出图标集为 JSON 文件时出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
generate
|
||||
}
|
||||
89
qiming-mobile/uni_modules/lime-icon/utils/index.js
Normal file
89
qiming-mobile/uni_modules/lime-icon/utils/index.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const fs = require("fs");
|
||||
const glob = require('glob');
|
||||
const path = require("path");
|
||||
const rootPath = process.cwd(); // 获取根目录
|
||||
// https://bl.ocks.org/jennyknuth/222825e315d45a738ed9d6e04c7a88d0
|
||||
function encodeSvg(svg) {
|
||||
return svg
|
||||
.replace(
|
||||
"<svg",
|
||||
~svg.indexOf("xmlns") ? "<svg" : '<svg xmlns="http://www.w3.org/2000/svg"'
|
||||
)
|
||||
.replace(/"/g, "'")
|
||||
.replace(/%/g, "%25")
|
||||
.replace(/#/g, "%23")
|
||||
.replace(/{/g, "%7B")
|
||||
.replace(/}/g, "%7D")
|
||||
.replace(/</g, "%3C")
|
||||
.replace(/>/g, "%3E");
|
||||
}
|
||||
|
||||
|
||||
function isDirectoryEmpty(path) {
|
||||
const files = fs.readdirSync(path);
|
||||
return files.length === 0;
|
||||
}
|
||||
function deleteFolderBFS(folderPath) {
|
||||
const outputPath = /^\.|\/|\\/.test(folderPath) ? path.join(rootPath, folderPath): folderPath
|
||||
if(!fs.existsSync(outputPath)) {
|
||||
return
|
||||
}
|
||||
const queue = [outputPath];
|
||||
while (queue.length > 0) {
|
||||
const currentPath = queue.shift();
|
||||
const currentStats = fs.statSync(currentPath);
|
||||
|
||||
if (currentStats.isDirectory()) {
|
||||
const files = fs.readdirSync(currentPath);
|
||||
for (const file of files) {
|
||||
const filePath = path.join(currentPath, file);
|
||||
const fileStats = fs.statSync(filePath);
|
||||
if (fileStats.isDirectory()) {
|
||||
queue.push(filePath);
|
||||
} else {
|
||||
fs.unlinkSync(filePath); // 删除文件
|
||||
}
|
||||
}
|
||||
if(isDirectoryEmpty(currentPath)) {
|
||||
fs.rmdirSync(currentPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存
|
||||
async function saveFile(file, data) {
|
||||
const outputPath = /^(\.|\/|\\)/.test(file) ? path.join(rootPath, file) : file;
|
||||
const outputDir = path.dirname(outputPath);
|
||||
try {
|
||||
// 创建文件夹
|
||||
await fs.promises.mkdir(outputDir, {
|
||||
recursive: true
|
||||
});
|
||||
|
||||
// 使用 Promise 进行写入文件操作
|
||||
await fs.promises.writeFile(outputPath, data, "utf8");
|
||||
// console.log(`成功保存文件:${outputPath}`);
|
||||
} catch (error) {
|
||||
console.error("保存文件时出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 可选的选项对象
|
||||
const customOptions = {
|
||||
prefix: "l", // 为图标集设置前缀
|
||||
includeSubDirs: true, // 启用扫描子目录中的文件(默认启用)
|
||||
keyword: (fileName, defaultKeyword, iconSet) => {
|
||||
// 根据文件名自定义关键字生成
|
||||
// 返回关键字或 undefined 以跳过该文件
|
||||
return defaultKeyword;
|
||||
},
|
||||
ignoreImportErrors: true, // 禁用未成功导入图标时的错误抛出(默认启用)
|
||||
keepTitles: false, // 禁用在 SVG 中保留标题(默认禁用)
|
||||
};
|
||||
module.exports = {
|
||||
encodeSvg,
|
||||
saveFile,
|
||||
deleteDirectory: deleteFolderBFS,
|
||||
customOptions,
|
||||
};
|
||||
87
qiming-mobile/uni_modules/lime-icon/vite-plugin.js
Normal file
87
qiming-mobile/uni_modules/lime-icon/vite-plugin.js
Normal file
@@ -0,0 +1,87 @@
|
||||
const { readFileSync, existsSync } = require('fs');
|
||||
const path = require('path');
|
||||
const {generate} = require('./utils/generate')
|
||||
|
||||
// 插件的名称
|
||||
const pluginName = 'vite-plugin-limeIcon';
|
||||
|
||||
// 要监听的组件的名称
|
||||
const targetComponent = 'l-icon';
|
||||
|
||||
function parseAttributes(attributesStr) {
|
||||
if (!attributesStr.includes("'")) {
|
||||
return [attributesStr]
|
||||
}
|
||||
const regex = /'([^']+)'/g;
|
||||
const matches = attributesStr.match(regex);
|
||||
if (matches) {
|
||||
const targetContent = matches.map(item => item.replace(/'/g, ''));
|
||||
return targetContent
|
||||
} else {
|
||||
return [attributesStr]
|
||||
}
|
||||
}
|
||||
|
||||
function extractAttributes(content) {
|
||||
const regex = /<l-icon\s*[^>]*(:?)name=["]([^"]+)["][^>]*>/g; // /<l-icon\s+(.*?)\s*\/?>/g //<l-icon\s+([^>]+)\s*\/?>/g;
|
||||
let attributes = [];
|
||||
const attributesSet = new Set(attributes);
|
||||
let match;
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
const attributesStr = match[2];
|
||||
const attributesList = parseAttributes(attributesStr);
|
||||
for (const attribute of attributesList) {
|
||||
attributesSet.add(attribute); // 添加新属性到Set中
|
||||
}
|
||||
}
|
||||
attributes = [...attributesSet];
|
||||
return attributes;
|
||||
}
|
||||
// 遍历每个文件并检查是否使用了目标组件
|
||||
let iconCollections = {}
|
||||
let files = {}
|
||||
let timer = null
|
||||
function processFile(file, options) {
|
||||
const filePath = path.resolve(file);
|
||||
const content = readFileSync(filePath, 'utf-8');
|
||||
|
||||
// 检查文件是否包含目标组件
|
||||
if (content.includes(targetComponent) && (!file.includes('l-icon.vue') || !file.includes('l-icon.uvue')) && files[file] !== content) {
|
||||
const icons = extractAttributes(content)
|
||||
if(icons && icons.length) {
|
||||
files[file] = content
|
||||
iconCollections[file] = icons
|
||||
}
|
||||
Object.values(iconCollections).forEach(icons => {
|
||||
if(options.icons) {
|
||||
options.icons = options.icons.concat(icons);
|
||||
} else {
|
||||
options.icons = icons
|
||||
}
|
||||
})
|
||||
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
options.icons = Array.from(new Set(options.icons))
|
||||
generate(options)
|
||||
},500)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 插件的钩子函数
|
||||
function vitePlugin(options = {}) {
|
||||
const {useInDevelopment = false} = options
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
return {
|
||||
name: pluginName,
|
||||
transform(code, id) {
|
||||
if (id.endsWith('.vue') && (useInDevelopment && isDev || !useInDevelopment && !isDev)) {
|
||||
// 处理Vue文件
|
||||
processFile(id, options);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = vitePlugin;
|
||||
@@ -0,0 +1,254 @@
|
||||
@import '@/uni_modules/lime-style/index.scss';
|
||||
|
||||
/* #ifdef uniVersion >= 4.75 */
|
||||
$use-css-var: true;
|
||||
/* #endif */
|
||||
|
||||
$loading-color: create-var(loading-color, $primary-color);
|
||||
$loading-size: create-var(loading-size, 20px);
|
||||
$loading-text-color: create-var(loading-text-color, $text-color-3);
|
||||
$loading-font-size: create-var(loading-font-size, $font-size);
|
||||
|
||||
/* #ifndef APP-ANDROID || APP-HARMONY || APP-IOS || APP-NVUE */
|
||||
/* #ifndef MP-ALIPAY || MP-WEIXIN */
|
||||
$loading-duration: var(--l-loading-duration, 2s);
|
||||
|
||||
@property --l-loading-start {
|
||||
syntax: '<length-percentage>';
|
||||
initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
@property --l-loading-end {
|
||||
syntax: '<length-percentage>';
|
||||
initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
@property --l-left {
|
||||
syntax: '<length-percentage>';
|
||||
initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
@property --l-loadding-ball-size {
|
||||
syntax: '<length> | <length-percentage>';
|
||||
// initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
/* #ifdef MP-ALIPAY || MP-WEIXIN */
|
||||
$loading-duration: var(--l-loading-duration, 1s);
|
||||
/* #endif */
|
||||
|
||||
|
||||
/* #endif */
|
||||
|
||||
|
||||
|
||||
.l-loading {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
// align-self: flex-start;
|
||||
/* #ifndef APP-ANDROID || APP-HARMONY || APP-IOS || APP-NVUE */
|
||||
color: $loading-color;
|
||||
&--ball{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
.l-loading {
|
||||
&__ball {
|
||||
position: relative;
|
||||
perspective: calc(var(--l-loadding-ball-size) * 4);
|
||||
transform-style: preserve-3d;
|
||||
// border: 1px solid;
|
||||
|
||||
&:before{
|
||||
background-color: $primary-color;
|
||||
left: 0%;
|
||||
// mix-blend-mode: darken;
|
||||
animation-name: l-ball-before;
|
||||
}
|
||||
&:after{
|
||||
right: 0;
|
||||
background-color: red;
|
||||
// mix-blend-mode: darken;
|
||||
animation-name: l-ball-after;
|
||||
}
|
||||
&:before,&:after{
|
||||
top: 0;
|
||||
content: '';
|
||||
position: absolute;
|
||||
// width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 50%;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: -100ms;
|
||||
animation-duration: 900ms;
|
||||
mix-blend-mode: darken;
|
||||
animation-play-state: var(--l-play-state, running);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&--circular {
|
||||
.l-loading {
|
||||
&__circular {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
animation: l-rotate $loading-duration linear infinite;
|
||||
animation-play-state: var(--l-play-state, running);
|
||||
vertical-align: middle;
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
/* #ifndef MP-ALIPAY */
|
||||
background: conic-gradient(
|
||||
transparent 0%,
|
||||
transparent var(--l-loading-start, 0%), var(--l-loading-color-1, currentColor) var(--l-loading-start, 0%),
|
||||
var(--l-loading-color-2, currentColor) var(--l-loading-end, 0%), transparent var(--l-loading-end, 0%),
|
||||
transparent 100%);
|
||||
/* #endif */
|
||||
/* #ifdef MP-ALIPAY */
|
||||
background: conic-gradient(
|
||||
var(--l-loading-color-1, transparent) 0%,
|
||||
var(--l-loading-color-2, currentColor) 100%);
|
||||
/* #endif */
|
||||
mask: radial-gradient(closest-side, transparent calc(80% - 1px), #fff 80%);
|
||||
-webkit-mask: radial-gradient(closest-side, transparent calc(80% - 1px), #fff 80%);
|
||||
animation: l-circular 3s ease-in-out infinite;
|
||||
animation-play-state: var(--l-play-state, running);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&--spinner {
|
||||
.l-loading {
|
||||
&__spinner {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
// max-width: 100%;
|
||||
// max-height: 100%;
|
||||
animation-timing-function: steps(12);
|
||||
animation: l-rotate 1.5s linear infinite;
|
||||
animation-play-state: var(--l-play-state, running)
|
||||
}
|
||||
&__dot {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(calc(var(--l-loading-dot, 1) * 30deg));
|
||||
opacity: calc(var(--l-loading-dot, 1) / 12);
|
||||
&::before {
|
||||
display: block;
|
||||
width: 5rpx;
|
||||
height: 25%;
|
||||
margin: 0 auto;
|
||||
background-color: currentColor;
|
||||
border-radius: 40%;
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* #endif */
|
||||
/* #ifdef APP-ANDROID || APP-HARMONY || APP-IOS || APP-NVUE */
|
||||
&__view{
|
||||
// background-color: aqua;
|
||||
// background-color: #1677ff;
|
||||
// transition-duration: 1.5s;
|
||||
// transition-property: transform;
|
||||
// transition-timing-function: linear;
|
||||
}
|
||||
/* #endif */
|
||||
&__text {
|
||||
margin-left: $spacer-xs;
|
||||
color: $loading-text-color;
|
||||
font-size: $loading-font-size;
|
||||
}
|
||||
|
||||
|
||||
&.is-vertical {
|
||||
flex-direction: column;
|
||||
.l-loading__text {
|
||||
margin: $spacer-tn 0 0;
|
||||
}
|
||||
}
|
||||
&__ball,&__circular,&__spinner {
|
||||
width: $loading-size;
|
||||
height: $loading-size;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* #ifndef APP-ANDROID || APP-HARMONY || APP-IOS || APP-NVUE */
|
||||
@keyframes l-circular {
|
||||
0% {
|
||||
--l-loading-start: 0%;
|
||||
--l-loading-end: 0%;
|
||||
}
|
||||
50% {
|
||||
--l-loading-start: 0%;
|
||||
--l-loading-end: 100%;
|
||||
}
|
||||
100% {
|
||||
--l-loading-start: 100%;
|
||||
--l-loading-end: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes l-rotate {
|
||||
to {
|
||||
transform: rotate(1turn)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes l-ball-before {
|
||||
0%{
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
25% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2);
|
||||
transform: translate3d(var(--l-left), 0, var(--l-loadding-ball-size));
|
||||
}
|
||||
50% {
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)));
|
||||
animation-timing-function:ease-in;
|
||||
transform: translate3d(var(--l-left), 0, 0);
|
||||
}
|
||||
75% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2);
|
||||
transform: translate3d(var(--l-left), 0, calc(var(--l-loadding-ball-size) * -1));
|
||||
}
|
||||
}
|
||||
@keyframes l-ball-after {
|
||||
0%{
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
25% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2 * -1);
|
||||
transform: translate3d(var(--l-left), 0, calc(var(--l-loadding-ball-size) * -1));
|
||||
}
|
||||
50% {
|
||||
animation-timing-function:ease-in;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) * -1);
|
||||
transform: translate3d(var(--l-left), 0, 0);
|
||||
}
|
||||
75% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2 * -1);
|
||||
transform: translate3d(var(--l-left), 0, var(--l-loadding-ball-size));
|
||||
}
|
||||
}
|
||||
/* #endif */
|
||||
@@ -0,0 +1,246 @@
|
||||
@import '@/uni_modules/lime-style/index.scss';
|
||||
|
||||
$loading-color: create-var(loading-color, $primary-color);
|
||||
$loading-size: create-var(loading-size, 20px);
|
||||
$loading-text-color: create-var(loading-text-color, $text-color-3);
|
||||
$loading-font-size: create-var(loading-font-size, $font-size);
|
||||
|
||||
/* #ifndef MP-ALIPAY */
|
||||
$loading-duration: create-var(loading-duration, 2s);
|
||||
/* #endif */
|
||||
/* #ifdef MP-ALIPAY */
|
||||
$loading-duration: create-var(loading-duration, 1s);
|
||||
/* #endif */
|
||||
|
||||
/* #ifndef APP-NVUE */
|
||||
|
||||
|
||||
|
||||
/* #ifndef MP-ALIPAY */
|
||||
@property --l-loading-start {
|
||||
syntax: '<length-percentage>';
|
||||
initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
@property --l-loading-end {
|
||||
syntax: '<length-percentage>';
|
||||
initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
@property --l-left {
|
||||
syntax: '<length-percentage>';
|
||||
initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
@property --l-loadding-ball-size {
|
||||
syntax: '<length> | <length-percentage>';
|
||||
// initial-value: 1%;
|
||||
inherits: false;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
:host {
|
||||
display: inline-flex;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
|
||||
.l-loading {
|
||||
position: relative;
|
||||
// color: #c8c9cc;
|
||||
color: $loading-color;
|
||||
font-size: 0;
|
||||
vertical-align: middle;
|
||||
&--ball{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
.l-loading {
|
||||
&__ball {
|
||||
position: relative;
|
||||
perspective: calc(var(--l-loadding-ball-size) * 4);
|
||||
transform-style: preserve-3d;
|
||||
// border: 1px solid;
|
||||
|
||||
&:before{
|
||||
background-color: $primary-color;
|
||||
left: 0%;
|
||||
// mix-blend-mode: darken;
|
||||
animation-name: l-ball-before;
|
||||
}
|
||||
&:after{
|
||||
right: 0;
|
||||
background-color: red;
|
||||
// mix-blend-mode: darken;
|
||||
animation-name: l-ball-after;
|
||||
}
|
||||
&:before,&:after{
|
||||
top: 0;
|
||||
content: '';
|
||||
position: absolute;
|
||||
// width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
border-radius: 50%;
|
||||
animation-iteration-count: infinite;
|
||||
animation-delay: -100ms;
|
||||
animation-duration: 900ms;
|
||||
mix-blend-mode: darken;
|
||||
animation-play-state: var(--l-play-state, running);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&--circular {
|
||||
.l-loading {
|
||||
&__circular {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
/* #ifndef APP-NVUE */
|
||||
animation: l-rotate $loading-duration linear infinite;
|
||||
vertical-align: middle;
|
||||
animation-play-state: var(--l-play-state, running);
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
/* #ifndef MP-ALIPAY || APP-VUE */
|
||||
background-image: conic-gradient(
|
||||
transparent 0%,
|
||||
transparent var(--l-loading-start, 0%), var(--l-loading-color-1, currentColor) var(--l-loading-start, 0%),
|
||||
var(--l-loading-color-2, currentColor) var(--l-loading-end, 0%), transparent var(--l-loading-end, 0%),
|
||||
transparent 100%);
|
||||
/* #endif */
|
||||
/* #ifdef MP-ALIPAY || APP-VUE */
|
||||
background-image: conic-gradient(
|
||||
var(--l-loading-color-1, transparent) 0%,
|
||||
var(--l-loading-color-2, currentColor) 100%);
|
||||
/* #endif */
|
||||
mask: radial-gradient(closest-side, transparent calc(80% - 1px), #fff 80%);
|
||||
-webkit-mask: radial-gradient(closest-side, transparent calc(80% - 1px), #fff 80%);
|
||||
animation: l-circular 2.5s ease-in-out infinite;
|
||||
transform: rotate(90deg);
|
||||
animation-play-state: var(--l-play-state, running);
|
||||
}
|
||||
/* #endif */
|
||||
}
|
||||
}
|
||||
}
|
||||
&--spinner {
|
||||
.l-loading {
|
||||
&__spinner {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
animation-timing-function: steps(12);
|
||||
animation: l-rotate 0.8s linear infinite;
|
||||
animation-play-state: var(--l-play-state, running);
|
||||
}
|
||||
&__dot {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(calc(var(--l-loading-dot, 1) * 30deg));
|
||||
opacity: calc(var(--l-loading-dot, 1) / 12);
|
||||
&::before {
|
||||
display: block;
|
||||
width: 5rpx;
|
||||
height: 25%;
|
||||
margin: 0 auto;
|
||||
background-color: currentColor;
|
||||
border-radius: 40%;
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
&__text{
|
||||
display: inline-block;
|
||||
margin-left: $spacer-xs;
|
||||
color: $loading-text-color;
|
||||
font-size: $loading-font-size;
|
||||
vertical-align: middle;
|
||||
}
|
||||
&.is-vertical {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.l-loading__text {
|
||||
margin: $spacer-tn 0 0;
|
||||
}
|
||||
}
|
||||
&__ball,&__circular,&__spinner {
|
||||
width: $loading-size;
|
||||
height: $loading-size;
|
||||
}
|
||||
}
|
||||
/* #ifndef APP-NVUE */
|
||||
@keyframes l-circular {
|
||||
0% {
|
||||
--l-loading-start: 0%;
|
||||
--l-loading-end: 0%;
|
||||
}
|
||||
50% {
|
||||
--l-loading-start: 0%;
|
||||
--l-loading-end: 100%;
|
||||
}
|
||||
100% {
|
||||
--l-loading-start: 100%;
|
||||
--l-loading-end: 100%;
|
||||
}
|
||||
}
|
||||
@keyframes l-rotate {
|
||||
to {
|
||||
transform: rotate(1turn)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes l-ball-before {
|
||||
0%{
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
25% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2);
|
||||
transform: translate3d(var(--l-left), 0, var(--l-loadding-ball-size));
|
||||
}
|
||||
50% {
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)));
|
||||
animation-timing-function:ease-in;
|
||||
transform: translate3d(var(--l-left), 0, 0);
|
||||
}
|
||||
75% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2);
|
||||
transform: translate3d(var(--l-left), 0, calc(var(--l-loadding-ball-size) * -1));
|
||||
}
|
||||
}
|
||||
@keyframes l-ball-after {
|
||||
0%{
|
||||
animation-timing-function: ease-in;
|
||||
}
|
||||
25% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2 * -1);
|
||||
transform: translate3d(var(--l-left), 0, calc(var(--l-loadding-ball-size) * -1));
|
||||
}
|
||||
50% {
|
||||
animation-timing-function:ease-in;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) * -1);
|
||||
transform: translate3d(var(--l-left), 0, 0);
|
||||
}
|
||||
75% {
|
||||
animation-timing-function: ease-out;
|
||||
--l-left: calc((var(--l-loadding-ball-size,100%) * 2.1 - var(--l-loadding-ball-size,100%)) / 2 * -1);
|
||||
transform: translate3d(var(--l-left), 0, var(--l-loadding-ball-size));
|
||||
}
|
||||
}
|
||||
/* #endif */
|
||||
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<view class="l-loading" :class="classes">
|
||||
<!-- #ifndef APP-ANDROID || APP-IOS || APP-HARMONY -->
|
||||
<view class="l-loading__ball" v-if="type == 'ball'" :style="[spinnerStyle]"></view>
|
||||
<view class="l-loading__circular" v-if="type == 'circular'" :style="[spinnerStyle]"></view>
|
||||
<view class="l-loading__spinner" v-if="type == 'spinner'" :style="[spinnerStyle]">
|
||||
<view class="l-loading__dot" v-for="item in 12" :key="item" :style="{'--l-loading-dot': item}"></view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-ANDROID || APP-IOS || APP-HARMONY -->
|
||||
<view class="l-loading__view" ref="loadingRef" :style="spinnerStyle"></view>
|
||||
<!-- #endif -->
|
||||
<text class="l-loading__text" v-if="$slots['default'] != null || text != null" :style="textStyle">
|
||||
<slot>{{text}}</slot>
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="uts" setup>
|
||||
/**
|
||||
* Loading 加载指示器
|
||||
* @description 用于表示加载中的过渡状态,支持多种动画类型和布局方式
|
||||
* <br> 插件类型:LLoadingComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-loading
|
||||
*
|
||||
* @property {string} color 加载图标颜色(默认:主题色)
|
||||
* @property {'circular' | 'spinner' | 'failed'} mode 动画实现的模式.只针对APP
|
||||
* @value raf 延时
|
||||
* @value animate 基于元素的annimate方法
|
||||
* @property {'circular' | 'spinner' | 'failed'} type 加载状态类型
|
||||
* @value circular 环形旋转动画(默认)
|
||||
* @value spinner 菊花转动画
|
||||
* @value failed 加载失败提示
|
||||
* @property {string} text 提示文字内容
|
||||
* @property {string} textColor 文字颜色(默认同color)
|
||||
* @property {string} textSize 文字字号(默认:14px)
|
||||
* @property {boolean} vertical 是否垂直排列图标和文字
|
||||
* @property {boolean} animated 是否启用旋转动画(failed类型自动禁用)
|
||||
* @property {string} size 图标尺寸(默认:'40px')
|
||||
*/
|
||||
import { LoadingProps } from './type'
|
||||
// #ifdef APP
|
||||
// import {useLoading} from './useLoading'
|
||||
const themeVars = inject('limeConfigProviderThemeVars', computed(()=> ({})))
|
||||
import {useLoading} from '@/uni_modules/lime-loading'
|
||||
// #endif
|
||||
const name = 'l-loading'
|
||||
const props = withDefaults(defineProps<LoadingProps>(), {
|
||||
size: '40rpx', // 为所有平台设置默认值
|
||||
type: 'circular',
|
||||
mode: 'raf',
|
||||
animated: true,
|
||||
vertical: false,
|
||||
})
|
||||
|
||||
|
||||
const classes = computed<Map<string,any>>(():Map<string,any> => {
|
||||
const cls = new Map<string,any>()
|
||||
cls.set(name + '--' + props.type, true)
|
||||
if (props.vertical) {
|
||||
cls.set('is-vertical', props.vertical)
|
||||
} else {
|
||||
cls.set('is-horizontal', !props.vertical)
|
||||
}
|
||||
return cls
|
||||
})
|
||||
|
||||
const spinnerStyle = computed<Map<string,any>>(():Map<string,any> => {
|
||||
const style = new Map<string,any>()
|
||||
// 只在值不为 undefined 时设置,避免微信小程序报错
|
||||
if (props.size != null) {
|
||||
style.set('width', props.size)
|
||||
style.set('height', props.size)
|
||||
}
|
||||
// #ifndef APP
|
||||
if (props.color != null) {
|
||||
style.set('color', props.color)
|
||||
}
|
||||
style.set('--l-play-state', props.animated ? 'running' : 'paused')
|
||||
// #endif
|
||||
return style
|
||||
})
|
||||
|
||||
const textStyle = computed<Map<string,any>>(():Map<string,any> => {
|
||||
const style = new Map<string,any>()
|
||||
if (props.textColor != null) {
|
||||
style.set('color', props.textColor!)
|
||||
}
|
||||
if (props.textSize != null) {
|
||||
style.set('font-size', props.textSize!)
|
||||
}
|
||||
return style
|
||||
})
|
||||
// #ifdef APP
|
||||
const loadingRef = ref<UniElement|null>(null)
|
||||
const loading = useLoading(loadingRef)
|
||||
loading.type = props.type;
|
||||
loading.mode = props.mode;
|
||||
if(props.animated){
|
||||
loading.play()
|
||||
} else {
|
||||
loading.pause()
|
||||
}
|
||||
|
||||
watchEffect(()=>{
|
||||
if(loadingRef.value == null) return
|
||||
loading.color = props.color ?? `${themeVars.value['loadingColor'] ?? '#3283ff'}`
|
||||
|
||||
if(props.animated){
|
||||
loading.play()
|
||||
} else {
|
||||
loading.pause()
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index-u.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<view class="l-class l-loading" :class="['l-loading--' + type, {'is-vertical': vertical}]" :style="{color: inheritColor ? 'inherit': ''}">
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<loading-indicator class="l-loading__circular" :style="[spinnerStyle]" :animating="true"/>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<view class="l-loading__ball" v-if="type == 'ball'" :style="[spinnerStyle]"></view>
|
||||
<view class="l-loading__circular" v-if="type == 'circular'" :style="[spinnerStyle]"></view>
|
||||
<view class="l-loading__spinner" v-if="type == 'spinner'" :style="[spinnerStyle]">
|
||||
<view class="l-loading__dot" v-for="item in 12" :key="item" :style="{'--l-loading-dot': item}"></view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<text class="l-loading__text" v-if="$slots['default']||text" :style="[textStyle]">
|
||||
{{text}}<slot></slot>
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Loading 加载指示器
|
||||
* @description 用于表示加载中的过渡状态,支持多种动画类型和布局方式
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-loading
|
||||
*
|
||||
* @property {string} color 加载图标颜色(默认:主题色)
|
||||
* @property {'circular' | 'spinner' | 'failed'} type 加载状态类型
|
||||
* @value circular 环形旋转动画(默认)
|
||||
* @value spinner 菊花转动画
|
||||
* @value failed 加载失败提示
|
||||
* @property {string} text 提示文字内容
|
||||
* @property {string} textColor 文字颜色(默认同color)
|
||||
* @property {string} textSize 文字字号(默认:14px)
|
||||
* @property {boolean} vertical 是否垂直排列图标和文字
|
||||
* @property {boolean} animated 是否启用旋转动画(failed类型自动禁用)
|
||||
* @property {string} size 图标尺寸(默认:'40px')
|
||||
*/
|
||||
import {computed, defineComponent} from '@/uni_modules/lime-shared/vue';
|
||||
import {addUnit} from '@/uni_modules/lime-shared/addUnit';
|
||||
import {unitConvert} from '@/uni_modules/lime-shared/unitConvert';
|
||||
import LoadingProps from './props';
|
||||
const name = 'l-loading';
|
||||
|
||||
export default defineComponent({
|
||||
// name,
|
||||
props:LoadingProps,
|
||||
setup(props) {
|
||||
const classes = computed(() => {
|
||||
const {type, vertical} = props
|
||||
return {
|
||||
[name + '--' + type]: type,
|
||||
['is-vertical']: vertical,
|
||||
}
|
||||
})
|
||||
const spinnerStyle = computed(() => {
|
||||
const size = unitConvert(props.size ?? 0) * (props.type == 'ball' ? 0.6 : 1);
|
||||
return {
|
||||
color: props.color,
|
||||
width: size != 0 && (props.type == 'ball' ? addUnit(size * 2.1) : addUnit(size)),
|
||||
height: size != 0 && addUnit(size),
|
||||
'--l-loadding-ball-size': size != 0 && addUnit(size)
|
||||
}
|
||||
})
|
||||
const textStyle = computed(() => {
|
||||
return {
|
||||
color: props.textColor,
|
||||
fontSize: props.textSize,
|
||||
}
|
||||
})
|
||||
return {
|
||||
classes,
|
||||
spinnerStyle,
|
||||
textStyle
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index.scss';
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
// import {PropType} from 'vue'
|
||||
export default {
|
||||
color: {
|
||||
type: String,
|
||||
// default: '#c9c9c9'
|
||||
},
|
||||
type: {
|
||||
type: String, //as PropType<'circular'|'spinner'>,
|
||||
default: 'circular'
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
// #ifdef APP-NVUE
|
||||
default: '40rpx'
|
||||
// #endif
|
||||
},
|
||||
text: String,
|
||||
textColor: String,
|
||||
textSize: String,
|
||||
vertical: Boolean,
|
||||
inheritColor: Boolean,
|
||||
animated: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// 完整类型定义
|
||||
export interface LoadingProps {
|
||||
color?: string;
|
||||
type: 'circular' | 'spinner' | 'failed';
|
||||
// #ifndef APP
|
||||
size?: string;
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
size: string;
|
||||
// #endif
|
||||
text?: string;
|
||||
textColor?: string;
|
||||
textSize?: string;
|
||||
mode: 'raf' | 'animate';
|
||||
vertical: boolean;
|
||||
animated: boolean;
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
// type UseLoadingOtions = {
|
||||
// type: string,
|
||||
// color: string,
|
||||
// el: UniElement
|
||||
// }
|
||||
import {tinyColor} from '@/uni_modules/lime-color'
|
||||
function easeInOutCubic(t : number) : number {
|
||||
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
|
||||
}
|
||||
|
||||
type useLoadingReturnType = {
|
||||
state : Ref<boolean>
|
||||
color : Ref<string>
|
||||
play: () => void
|
||||
failed: () => void
|
||||
clear : () => void
|
||||
destroy : () => void
|
||||
}
|
||||
type Point = {
|
||||
x1: number
|
||||
y1: number
|
||||
x2: number
|
||||
y2: number
|
||||
}
|
||||
export function useLoading(
|
||||
element : Ref<UniElement | null>,
|
||||
type : 'circular' | 'spinner',
|
||||
strokeColor : string,
|
||||
ratio : number,
|
||||
immediate: boolean = false,
|
||||
) : useLoadingReturnType {
|
||||
const state = ref(false)
|
||||
const color = ref(strokeColor)
|
||||
let tick = 0 // 0 不绘制 | 1 旋转 | 2 错误
|
||||
let init = false
|
||||
let isDestroy = ref(false)
|
||||
let width = 0
|
||||
let height = 0
|
||||
let size = 0
|
||||
let x = 0
|
||||
let y = 0
|
||||
let ctx : DrawableContext | null = null
|
||||
let timer = -1
|
||||
let isClear = false;
|
||||
let drawing = false;
|
||||
const updateSize = () => {
|
||||
if (element.value == null) return
|
||||
|
||||
const rect = element.value!.getBoundingClientRect();
|
||||
ctx = element.value!.getDrawableContext()! as DrawableContext
|
||||
width = rect.width
|
||||
height = rect.height
|
||||
size = ratio > 1 ? ratio : Math.floor(Math.min(width, height) * ratio)
|
||||
x = width / 2
|
||||
y = height / 2
|
||||
}
|
||||
const circular = () => {
|
||||
if (ctx == null) return
|
||||
let _ctx = ctx!
|
||||
let startAngle = 0;
|
||||
let endAngle = 0;
|
||||
let startSpeed = 0;
|
||||
let endSpeed = 0;
|
||||
let rotate = 0;
|
||||
|
||||
// 不使用360的原因是加上rotate后,会导致闪烁
|
||||
const ARC_LENGTH = 359.5
|
||||
const PI = Math.PI / 180
|
||||
const SPEED = 0.018
|
||||
const ROTATE_INTERVAL = 0.09
|
||||
const center = size / 2
|
||||
const lineWidth = size / 10;
|
||||
|
||||
function draw() {
|
||||
if(isClear) return
|
||||
_ctx.reset();
|
||||
_ctx.beginPath();
|
||||
_ctx.arc(
|
||||
x,
|
||||
y,
|
||||
center - lineWidth,
|
||||
startAngle * PI + rotate,
|
||||
endAngle * PI + rotate);
|
||||
_ctx.lineWidth = lineWidth;
|
||||
_ctx.strokeStyle = color.value;
|
||||
_ctx.stroke();
|
||||
|
||||
if (endAngle < ARC_LENGTH && startAngle == 0) {
|
||||
endSpeed += SPEED
|
||||
endAngle = Math.min(ARC_LENGTH, easeInOutCubic(endSpeed) * ARC_LENGTH)
|
||||
} else if (endAngle == ARC_LENGTH && startAngle < ARC_LENGTH) {
|
||||
startSpeed += SPEED
|
||||
startAngle = Math.min(ARC_LENGTH, easeInOutCubic(startSpeed) * ARC_LENGTH);
|
||||
} else if (endAngle >= ARC_LENGTH && startAngle >= ARC_LENGTH) {
|
||||
endSpeed = 0
|
||||
startSpeed = 0
|
||||
startAngle = 0;
|
||||
endAngle = 0;
|
||||
}
|
||||
rotate += ROTATE_INTERVAL;
|
||||
_ctx.update()
|
||||
// clearTimeout(timer)
|
||||
|
||||
timer = setTimeout(() => draw(), 24)
|
||||
}
|
||||
draw()
|
||||
}
|
||||
const spinner = () => {
|
||||
if (ctx == null) return
|
||||
let _ctx = ctx!
|
||||
const steps = 12;
|
||||
let step = 0;
|
||||
const lineWidth = size / 10;
|
||||
// 线长度和距离圆心距离
|
||||
const length = size / 4 - lineWidth;
|
||||
const offset = size / 4;
|
||||
|
||||
|
||||
function generateColorGradient(hex: string, steps: number):string[]{
|
||||
const colors:string[] = []
|
||||
const _color = tinyColor(hex)
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
_color.setAlpha(i/steps);
|
||||
colors.push(_color.toRgbString());
|
||||
}
|
||||
return colors
|
||||
}
|
||||
let colors = computed(():string[]=> generateColorGradient(color.value, steps))
|
||||
|
||||
function draw() {
|
||||
if(tick == 0) return
|
||||
_ctx.reset();
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const stepAngle = 360 / steps
|
||||
const angle = stepAngle * i;
|
||||
const index =(steps + i - (step % steps)) % steps
|
||||
// 正余弦
|
||||
const sin = Math.sin(angle / 180 * Math.PI);
|
||||
const cos = Math.cos(angle / 180 * Math.PI);
|
||||
// 开始绘制
|
||||
_ctx.lineWidth = lineWidth;
|
||||
_ctx.lineCap = 'round';
|
||||
_ctx.beginPath();
|
||||
_ctx.moveTo(size / 2 + offset * cos, size / 2 + offset * sin);
|
||||
_ctx.lineTo(size / 2 + (offset + length) * cos, size / 2 + (offset + length) * sin);
|
||||
_ctx.strokeStyle = colors.value[index]
|
||||
_ctx.stroke();
|
||||
}
|
||||
step += 1
|
||||
_ctx.update()
|
||||
timer = setTimeout(() => draw(), 1000/10)
|
||||
}
|
||||
draw()
|
||||
}
|
||||
const clear = () => {
|
||||
clearTimeout(timer)
|
||||
drawing = false
|
||||
tick = 0
|
||||
if(ctx == null) return
|
||||
// ctx?.reset()
|
||||
// ctx?.update()
|
||||
setTimeout(()=>{
|
||||
ctx!.reset()
|
||||
ctx!.update()
|
||||
},1000)
|
||||
|
||||
}
|
||||
const failed = () => {
|
||||
if(tick == 1) {
|
||||
drawing = false
|
||||
}
|
||||
clearTimeout(timer)
|
||||
tick = 2
|
||||
if (ctx == null || drawing) return
|
||||
let _ctx = ctx!
|
||||
const _size = size * 0.61
|
||||
const _sizeX = _size * 0.65
|
||||
const lineWidth = _size / 6;
|
||||
const lineLength = Math.ceil(Math.sqrt(Math.pow(_sizeX, 2) * 2))
|
||||
|
||||
const startX1 = (width - _sizeX) * 0.5
|
||||
const startY = (height - _sizeX) * 0.5
|
||||
const startX2 = startX1 + _sizeX
|
||||
|
||||
// 添加圆的参数
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
const radius = (_size * Math.sqrt(2)) / 2 + lineWidth / 2;
|
||||
const totalSteps = 36;
|
||||
|
||||
function generateSteps(stepsCount: number):Point[][] {
|
||||
|
||||
const halfStepsCount = stepsCount / 2;
|
||||
const step = lineLength / halfStepsCount //Math.floor(lineLength / 18);
|
||||
const steps:Point[][] = []
|
||||
for (let i = 0; i < stepsCount; i++) {
|
||||
const sub:Point[] = []
|
||||
const index = i % 18 + 1
|
||||
if(i < halfStepsCount) {
|
||||
|
||||
const x2 = Math.sin(45 * Math.PI / 180) * step * index + startX1
|
||||
const y2 = Math.cos(45 * Math.PI / 180) * step * index + startY
|
||||
|
||||
const start1 = {
|
||||
x1: startX1,
|
||||
y1: startY,
|
||||
x2,
|
||||
y2,
|
||||
} as Point
|
||||
|
||||
sub.push(start1)
|
||||
} else {
|
||||
sub.push(steps[halfStepsCount-1][0])
|
||||
const x2 = Math.sin((45 - 90) * Math.PI / 180) * step * index + startX2
|
||||
const y2 = Math.cos((45 - 90) * Math.PI / 180) * step * index + startY
|
||||
|
||||
const start2 = {
|
||||
x1: startX2,
|
||||
y1: startY,
|
||||
x2,
|
||||
y2,
|
||||
} as Point
|
||||
sub.push(start2)
|
||||
}
|
||||
steps.push(sub)
|
||||
}
|
||||
|
||||
return steps
|
||||
}
|
||||
const steps = generateSteps(36);
|
||||
function draw(){
|
||||
if(steps.length == 0 || tick == 0) {
|
||||
clearTimeout(timer)
|
||||
return
|
||||
}
|
||||
const drawStep = steps.shift()!
|
||||
_ctx.reset()
|
||||
_ctx.lineWidth = lineWidth;
|
||||
_ctx.strokeStyle = color.value;
|
||||
|
||||
// 绘制逐渐显示的圆
|
||||
_ctx.beginPath();
|
||||
_ctx.arc(centerX, centerY, radius, 0, (2 * Math.PI) * (totalSteps - steps.length) / totalSteps);
|
||||
_ctx.lineWidth = lineWidth;
|
||||
_ctx.strokeStyle = color.value;
|
||||
_ctx.stroke();
|
||||
|
||||
// 绘制X
|
||||
_ctx.beginPath();
|
||||
drawStep.forEach(item => {
|
||||
_ctx.beginPath();
|
||||
_ctx.moveTo(item.x1, item.y1)
|
||||
_ctx.lineTo(item.x2, item.y2)
|
||||
_ctx.stroke();
|
||||
})
|
||||
_ctx.update()
|
||||
timer = setTimeout(() => draw(), 1000/30)
|
||||
}
|
||||
draw()
|
||||
}
|
||||
const destroy = () => {
|
||||
isDestroy.value = true;
|
||||
clear()
|
||||
}
|
||||
const play = () => {
|
||||
if(tick == 2) {
|
||||
drawing = false
|
||||
}
|
||||
if(drawing) return
|
||||
tick = 1
|
||||
if(width == 0 || height == 0) return
|
||||
if (type == 'circular') {
|
||||
circular()
|
||||
} else if (type == 'spinner') {
|
||||
spinner()
|
||||
}
|
||||
drawing = true
|
||||
}
|
||||
|
||||
const _watch = (v:boolean) => {
|
||||
if(isDestroy.value) return
|
||||
if (v) {
|
||||
play()
|
||||
} else {
|
||||
failed()
|
||||
}
|
||||
}
|
||||
const stopWatchState = watch(state, _watch)
|
||||
|
||||
const ob = new UniResizeObserver((entries: UniResizeObserverEntry[])=>{
|
||||
if(isDestroy.value) return
|
||||
entries.forEach(entry => {
|
||||
if(isDestroy.value) return
|
||||
const rect = entry.target.getBoundingClientRect();
|
||||
if(rect.width > 0 && rect.height > 0) {
|
||||
updateSize();
|
||||
if(tick == 1) {
|
||||
play()
|
||||
state.value = true
|
||||
} else if(tick == 2) {
|
||||
failed()
|
||||
state.value = false
|
||||
} else if(immediate && !init) {
|
||||
_watch(state.value)
|
||||
init = true
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const stopWatchElement = watch(element, (el:UniElement|null) => {
|
||||
if(el == null || isDestroy.value) return
|
||||
ob.observe(el)
|
||||
})
|
||||
|
||||
onUnmounted(()=>{
|
||||
stopWatchState()
|
||||
stopWatchElement()
|
||||
clear()
|
||||
ob.disconnect()
|
||||
})
|
||||
return {
|
||||
state,
|
||||
play,
|
||||
failed,
|
||||
clear,
|
||||
color,
|
||||
destroy
|
||||
} as useLoadingReturnType
|
||||
}
|
||||
416
qiming-mobile/uni_modules/lime-loading/index.uts
Normal file
416
qiming-mobile/uni_modules/lime-loading/index.uts
Normal file
@@ -0,0 +1,416 @@
|
||||
// 引入颜色处理库
|
||||
import { tinyColor } from '@/uni_modules/lime-color';
|
||||
|
||||
|
||||
/**
|
||||
* 操作类型
|
||||
* play: 开始动画
|
||||
* failed: 显示失败状态
|
||||
* clear: 清除动画
|
||||
* destroy: 销毁实例
|
||||
*/
|
||||
export type TickType = 'play' | 'failed' | 'clear' | 'destroy' | 'pause'
|
||||
/**
|
||||
* 加载动画类型
|
||||
* circular: 环形加载动画
|
||||
* spinner: 旋转器加载动画
|
||||
* failed: 失败状态动画
|
||||
*/
|
||||
export type LoadingType = 'circular' | 'spinner' | 'failed';
|
||||
/**
|
||||
* 加载组件返回接口
|
||||
*/
|
||||
export type UseLoadingReturn = {
|
||||
ratio : 1;
|
||||
type : LoadingType;
|
||||
mode : 'raf' | 'animate'; //
|
||||
color : string;//Ref<string>;
|
||||
play : () => void;
|
||||
failed : () => void;
|
||||
clear : () => void;
|
||||
destroy : () => void;
|
||||
pause : () => void;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 计算圆周上指定角度的点的坐标
|
||||
* @param centerX 圆心的 X 坐标
|
||||
* @param centerY 圆心的 Y 坐标
|
||||
* @param radius 圆的半径
|
||||
* @param angleDegrees 角度(以度为单位)
|
||||
* @returns 包含 X 和 Y 坐标的对象
|
||||
*/
|
||||
function getPointOnCircle(
|
||||
centerX : number,
|
||||
centerY : number,
|
||||
radius : number,
|
||||
angleDegrees : number
|
||||
) : number[] {
|
||||
// 将角度转换为弧度
|
||||
const angleRadians = (angleDegrees * Math.PI) / 180;
|
||||
|
||||
// 计算点的 X 和 Y 坐标
|
||||
const x = centerX + radius * Math.cos(angleRadians);
|
||||
const y = centerY + radius * Math.sin(angleRadians);
|
||||
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
export function useLoading(element : Ref<UniElement | null>) : UseLoadingReturn {
|
||||
|
||||
const tick = ref<TickType>('pause')
|
||||
|
||||
const state = reactive<UseLoadingReturn>({
|
||||
color: '#000',
|
||||
type: 'circular',
|
||||
ratio: 1,
|
||||
mode: 'raf',
|
||||
play: () => {
|
||||
tick.value = 'play'
|
||||
},
|
||||
failed: () => {
|
||||
tick.value = 'failed'
|
||||
},
|
||||
clear: () => {
|
||||
tick.value = 'clear'
|
||||
},
|
||||
destroy: () => {
|
||||
tick.value = 'destroy'
|
||||
},
|
||||
pause: () => {
|
||||
tick.value = 'pause'
|
||||
}
|
||||
})
|
||||
|
||||
const context = shallowRef<DrawableContext | null>(null);
|
||||
// let ctx:DrawableContext|null = null
|
||||
|
||||
// let rotation = 0
|
||||
let isPlaying = false
|
||||
let canvasWidth = ref(0)
|
||||
let canvasHeight = ref(0)
|
||||
let canvasSize = ref(0)
|
||||
|
||||
let animationFrameId = -1
|
||||
let animation : UniAnimation | null = null
|
||||
|
||||
let drawFrame : (() => void) | null = null
|
||||
const size = computed(() : number => state.ratio > 1 ? state.ratio : canvasSize.value * state.ratio)
|
||||
// 绘制圆形加载
|
||||
const drawCircular = () => {
|
||||
let startAngle = 0; // 起始角度
|
||||
let endAngle = 0; // 结束角度
|
||||
let rotate = 0; // 旋转角度
|
||||
|
||||
// const ctx = context.value!
|
||||
// 动画参数配置
|
||||
const MIN_ANGLE = 5; // 最小保持角度
|
||||
const ARC_LENGTH = 359.5 // 最大弧长(避免闭合)
|
||||
const PI = Math.PI / 180 // 角度转弧度系数
|
||||
const SPEED = 0.018 / 4 // 动画速度
|
||||
const ROTATE_INTERVAL = 0.09 / 4 // 旋转增量
|
||||
|
||||
const lineWidth = size.value / 10; // 线宽计算
|
||||
const x = canvasWidth.value / 2 // 中心点X
|
||||
const y = canvasHeight.value / 2 // 中心点Y
|
||||
const radius = size.value / 2 - lineWidth // 实际绘制半径
|
||||
try {
|
||||
drawFrame = () => {
|
||||
if (context.value == null || !isPlaying) return
|
||||
let ctx = context.value!
|
||||
|
||||
|
||||
// console.log('radius', radius, size.value)
|
||||
ctx.reset();
|
||||
|
||||
// 绘制圆弧
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
x,
|
||||
y,
|
||||
radius,
|
||||
startAngle * PI + rotate,
|
||||
endAngle * PI + rotate
|
||||
);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = state.color;
|
||||
ctx.stroke();
|
||||
|
||||
// 角度更新逻辑
|
||||
if (endAngle < ARC_LENGTH) {
|
||||
endAngle = Math.min(ARC_LENGTH, endAngle + (ARC_LENGTH - MIN_ANGLE) * SPEED);
|
||||
} else if (startAngle < ARC_LENGTH) {
|
||||
startAngle = Math.min(ARC_LENGTH, startAngle + (ARC_LENGTH - MIN_ANGLE) * SPEED);
|
||||
} else {
|
||||
// 重置时保留最小可见角度
|
||||
startAngle = 0;
|
||||
endAngle = MIN_ANGLE;
|
||||
}
|
||||
|
||||
ctx.update()
|
||||
|
||||
|
||||
|
||||
if (state.mode == 'raf') {
|
||||
rotate = (rotate + ROTATE_INTERVAL) % 360; // 持续旋转并限制范围
|
||||
if (isPlaying && drawFrame != null) {
|
||||
animationFrameId = requestAnimationFrame(drawFrame!)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(err) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
let lastTime = Date.now();
|
||||
const drawSpinner = () => {
|
||||
const steps = 12; // 旋转线条数量
|
||||
// const size = state.ratio > 1 ? state.ratio : canvasSize.value
|
||||
const lineWidth = size.value / 10; // 线宽
|
||||
const x = canvasWidth.value / 2 // 中心坐标
|
||||
const y = canvasHeight.value / 2
|
||||
|
||||
let step = 0; // 当前步数
|
||||
// #ifdef APP-HARMONY
|
||||
const length = size.value / 3.4 - lineWidth; // 线长
|
||||
// #endif
|
||||
// #ifndef APP-HARMONY
|
||||
const length = size.value / 3.6 - lineWidth; // 线长
|
||||
// #endif
|
||||
const offset = size.value / 4; // 距中心偏移
|
||||
|
||||
/** 生成颜色渐变数组 */
|
||||
function generateColorGradient(hex : string, steps : number) : string[] {
|
||||
const colors : string[] = []
|
||||
const _color = tinyColor(hex)
|
||||
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
_color.setAlpha(i / steps);
|
||||
colors.push(_color.toRgbString());
|
||||
}
|
||||
return colors
|
||||
}
|
||||
|
||||
// 计算颜色渐变
|
||||
let colors = computed(() : string[] => generateColorGradient(state.color, steps))
|
||||
|
||||
/** 帧绘制函数 */
|
||||
drawFrame = () => {
|
||||
if (context.value == null || !isPlaying) return
|
||||
const delta = Date.now() - lastTime;
|
||||
|
||||
|
||||
if (delta >= 1000 / 10) {
|
||||
lastTime = Date.now();
|
||||
let ctx = context.value!
|
||||
ctx.reset();
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const stepAngle = 360 / steps; // 单步角度
|
||||
const angle = stepAngle * i; // 当前角度
|
||||
const index = (steps + i - step) % steps // 颜色索引
|
||||
// 计算线段坐标
|
||||
const radian = angle * Math.PI / 180;
|
||||
const cos = Math.cos(radian);
|
||||
const sin = Math.sin(radian);
|
||||
|
||||
// 绘制线段
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + offset * cos, y + offset * sin);
|
||||
ctx.lineTo(x + (offset + length) * cos, y + (offset + length) * sin);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = colors.value[index];
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.update()
|
||||
if(state.mode == 'raf') {
|
||||
// step += 1
|
||||
step = (step + 1) % steps; // 限制step范围
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (state.mode == 'raf') {
|
||||
if (isPlaying && drawFrame != null) {
|
||||
animationFrameId = requestAnimationFrame(drawFrame!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const drwaFailed = () => {
|
||||
if (context.value == null) return
|
||||
let ctx = context.value!
|
||||
|
||||
// const size = state.ratio > 1 ? state.ratio : canvasSize.value
|
||||
const innerSize = size.value * 0.8 // 内圈尺寸
|
||||
const lineWidth = innerSize / 10; // 线宽
|
||||
const lineLength = (size.value - lineWidth) / 2 // X长度
|
||||
const centerX = canvasWidth.value / 2;
|
||||
const centerY = canvasHeight.value / 2;
|
||||
const radius = (size.value - lineWidth) / 2
|
||||
|
||||
|
||||
|
||||
const angleRadians1 = 45 * Math.PI / 180
|
||||
const angleRadians2 = (45 - 90) * Math.PI / 180
|
||||
|
||||
ctx.reset()
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = state.color;
|
||||
|
||||
// 绘制逐渐显示的圆
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = state.color;
|
||||
ctx.stroke();
|
||||
|
||||
const [startX1, startY] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 45)
|
||||
const [startX2] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 90 + 45)
|
||||
|
||||
const x2 = Math.sin(angleRadians1) * lineLength + startX1
|
||||
const y2 = Math.cos(angleRadians1) * lineLength + startY
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX1, startY)
|
||||
ctx.lineTo(x2, y2)
|
||||
ctx.stroke();
|
||||
|
||||
const x3 = Math.sin(angleRadians2) * lineLength + startX2
|
||||
const y3 = Math.cos(angleRadians2) * lineLength + startY
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(startX2, startY)
|
||||
ctx.lineTo(x3, y3)
|
||||
ctx.stroke();
|
||||
|
||||
|
||||
ctx.update()
|
||||
}
|
||||
|
||||
let currentType : LoadingType | null = null
|
||||
const useMode = () => {
|
||||
if (state.mode != 'raf') {
|
||||
const keyframes = [{ transform: 'rotate(0)' }, { transform: 'rotate(360)' }]
|
||||
animation = element.value!.animate(keyframes, {
|
||||
duration: 80000,
|
||||
easing: 'linear',
|
||||
// fill: 'forwards',
|
||||
iterations: Infinity
|
||||
})
|
||||
}
|
||||
}
|
||||
const startAnimation = (type : string) => {
|
||||
if (context.value == null || element.value == null) return
|
||||
animation?.pause()
|
||||
|
||||
if (currentType == type) {
|
||||
isPlaying = true
|
||||
animation?.play()
|
||||
drawFrame?.()
|
||||
return
|
||||
}
|
||||
|
||||
if (type == 'circular') {
|
||||
currentType = 'circular'
|
||||
drawCircular()
|
||||
useMode()
|
||||
|
||||
}
|
||||
|
||||
if (type == 'spinner') {
|
||||
currentType = 'spinner'
|
||||
drawSpinner()
|
||||
useMode()
|
||||
}
|
||||
|
||||
isPlaying = true
|
||||
drawFrame?.()
|
||||
}
|
||||
|
||||
// 监听元素尺寸
|
||||
const getBoundingClientRect = () => {
|
||||
requestAnimationFrame(()=> {
|
||||
element.value?.getBoundingClientRectAsync()?.then(rect => {
|
||||
if (rect.width == 0 || rect.height == 0) return
|
||||
context.value = element.value!.getDrawableContext() as DrawableContext;
|
||||
canvasWidth.value = rect.width;
|
||||
canvasHeight.value = rect.height;
|
||||
canvasSize.value = Math.min(rect.width, rect.height);
|
||||
// startAnimation(state.type)
|
||||
})
|
||||
})
|
||||
}
|
||||
const resizeObserver : UniResizeObserver = new UniResizeObserver((_entries : UniResizeObserverEntry[]) => {
|
||||
getBoundingClientRect()
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (element.value == null) return
|
||||
resizeObserver.observe(element.value!);
|
||||
// #ifdef APP-IOS
|
||||
getBoundingClientRect()
|
||||
// #endif
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (context.value == null) return
|
||||
if (tick.value == 'play') {
|
||||
animation?.pause()
|
||||
isPlaying = false
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
startAnimation(state.type)
|
||||
}
|
||||
if (tick.value == 'failed') {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
animation?.pause()
|
||||
animation?.cancel()
|
||||
drwaFailed()
|
||||
return
|
||||
}
|
||||
if (tick.value == 'clear') {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
animation?.pause()
|
||||
animation?.cancel()
|
||||
context.value?.reset();
|
||||
context.value?.update();
|
||||
isPlaying = false
|
||||
return
|
||||
}
|
||||
if (tick.value == 'destroy') {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
animation?.pause()
|
||||
animation?.cancel()
|
||||
context.value?.reset();
|
||||
context.value?.update();
|
||||
context.value = null
|
||||
animation = null
|
||||
isPlaying = false
|
||||
return
|
||||
}
|
||||
if (tick.value == 'pause') {
|
||||
// 首次需要绘制一帧
|
||||
if(animation == null) {
|
||||
startAnimation(state.type)
|
||||
}
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
isPlaying = false
|
||||
animation?.pause()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(()=>{
|
||||
if(state.color == '') return
|
||||
// #ifdef APP-HARMONY
|
||||
isPlaying = false
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
// #endif
|
||||
})
|
||||
|
||||
return state
|
||||
}
|
||||
595
qiming-mobile/uni_modules/lime-loading/index_old.uts
Normal file
595
qiming-mobile/uni_modules/lime-loading/index_old.uts
Normal file
@@ -0,0 +1,595 @@
|
||||
// 引入颜色处理库
|
||||
import { tinyColor } from '@/uni_modules/lime-color';
|
||||
|
||||
|
||||
// ===================== 类型定义 =====================
|
||||
/**
|
||||
* 加载动画类型
|
||||
* circular: 环形加载动画
|
||||
* spinner: 旋转器加载动画
|
||||
* failed: 失败状态动画
|
||||
*/
|
||||
export type LoadingType = 'circular' | 'spinner' | 'failed';
|
||||
|
||||
/**
|
||||
* 操作类型
|
||||
* play: 开始动画
|
||||
* failed: 显示失败状态
|
||||
* clear: 清除动画
|
||||
* destroy: 销毁实例
|
||||
*/
|
||||
export type TickType = 'play' | 'failed' | 'clear' | 'destroy' | 'pause'
|
||||
|
||||
/**
|
||||
* 加载组件配置选项
|
||||
* @property type - 初始动画类型
|
||||
* @property strokeColor - 线条颜色
|
||||
* @property ratio - 尺寸比例
|
||||
* @property immediate - 是否立即启动
|
||||
*/
|
||||
export type UseLoadingOptions = {
|
||||
type : LoadingType;
|
||||
strokeColor : string;
|
||||
ratio : number;
|
||||
immediate ?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载组件返回接口
|
||||
*/
|
||||
export type UseLoadingReturn = {
|
||||
// state : Ref<boolean>;
|
||||
// setOptions: (options: UseLoadingOptions) => void
|
||||
ratio : 1;
|
||||
type : LoadingType;
|
||||
color : string;//Ref<string>;
|
||||
play : () => void;
|
||||
failed : () => void;
|
||||
clear : () => void;
|
||||
destroy : () => void;
|
||||
pause : () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 画布尺寸信息
|
||||
*/
|
||||
export type Dimensions = {
|
||||
width : number;
|
||||
height : number;
|
||||
size : number
|
||||
}
|
||||
|
||||
/**
|
||||
* 线段坐标点
|
||||
*/
|
||||
type Point = {
|
||||
x1 : number
|
||||
y1 : number
|
||||
x2 : number
|
||||
y2 : number
|
||||
}
|
||||
|
||||
/**
|
||||
* 画布上下文信息
|
||||
*/
|
||||
type LoadingCanvasContext = {
|
||||
ctx : Ref<DrawableContext | null>;
|
||||
dimensions : Ref<Dimensions>;
|
||||
updateDimensions : (el : UniElement) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* 动画参数配置
|
||||
*/
|
||||
type AnimationParams = {
|
||||
width : number
|
||||
height : number
|
||||
center : number[] // 元组类型,明确表示两个数值的坐标
|
||||
color : string // 使用Ref类型包裹字符串
|
||||
size : number // 数值类型尺寸
|
||||
}
|
||||
|
||||
// ===================== 动画管理器 =====================
|
||||
type AnimationFrameHandler = () => boolean;
|
||||
|
||||
/**
|
||||
* 动画管理类
|
||||
* 封装动画的启动/停止逻辑
|
||||
*/
|
||||
export class AnimationManager {
|
||||
time : number = 1000 / 60 // 默认帧率60fps
|
||||
private timer : number = -1;// 定时器ID
|
||||
private isDestroyed : boolean = false; // 销毁状态
|
||||
private drawFrame : AnimationFrameHandler// 帧绘制函数
|
||||
private lastTime : number = 0;
|
||||
private isRunning : boolean = false;
|
||||
type : LoadingType;
|
||||
constructor(drawFrame : AnimationFrameHandler, type : LoadingType = 'circular') {
|
||||
this.type = type
|
||||
this.drawFrame = drawFrame
|
||||
}
|
||||
/** 启动动画循环 */
|
||||
start() {
|
||||
if (this.isRunning) return;
|
||||
this.isRunning = true;
|
||||
this.lastTime = Date.now();
|
||||
let animate : ((task : number) => void) | null = null
|
||||
|
||||
animate = (task : number) => {
|
||||
if (!this.isRunning) return;
|
||||
if (this.isDestroyed) return;
|
||||
const delta = Date.now() - this.lastTime;
|
||||
if (delta >= this.time || delta < 10) {
|
||||
const shouldContinue : boolean = this.drawFrame();
|
||||
this.lastTime = Date.now()
|
||||
if (!shouldContinue) {
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.timer = requestAnimationFrame(animate!);
|
||||
};
|
||||
|
||||
animate(Date.now());
|
||||
}
|
||||
/** 停止动画并清理资源 */
|
||||
stop() {
|
||||
cancelAnimationFrame(this.timer)
|
||||
this.isDestroyed = true;
|
||||
}
|
||||
pause() {
|
||||
this.isRunning = false;
|
||||
cancelAnimationFrame(this.timer);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 工具函数 =====================
|
||||
/**
|
||||
* 缓动函数 - 三次缓入缓出
|
||||
* @param t 时间系数 (0-1)
|
||||
* @returns 计算后的进度值
|
||||
*/
|
||||
function easeInOutCubic(t : number) : number {
|
||||
return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
|
||||
}
|
||||
|
||||
// ===================== 画布管理 =====================
|
||||
/**
|
||||
* 获取画布上下文信息
|
||||
* @param element 画布元素引用
|
||||
* @returns 包含画布上下文和尺寸信息的对象
|
||||
*/
|
||||
//_element : Ref<UniElement | null>
|
||||
export function useCanvas() : LoadingCanvasContext {
|
||||
const ctx = shallowRef<DrawableContext | null>(null);
|
||||
const dimensions = ref<Dimensions>({
|
||||
width: 0,
|
||||
height: 0,
|
||||
size: 0
|
||||
});
|
||||
|
||||
const updateDimensions = (el : UniElement) => {
|
||||
// const rect = el.getBoundingClientRect();
|
||||
// // 鸿蒙尺寸为0时 再次尺寸变化会不渲染
|
||||
// if(rect.width == 0) return
|
||||
// if(ctx.value == null) {
|
||||
// ctx.value = el.getDrawableContext() as DrawableContext;
|
||||
// }
|
||||
// dimensions.value.width = rect.width;
|
||||
// dimensions.value.height = rect.height;
|
||||
// dimensions.value.size = Math.min(rect.width, rect.height);
|
||||
el.getBoundingClientRectAsync()?.then(rect => {
|
||||
if (rect.width == 0 || rect.height == 0) return
|
||||
ctx.value = el.getDrawableContext() as DrawableContext;
|
||||
dimensions.value.width = rect.width;
|
||||
dimensions.value.height = rect.height;
|
||||
dimensions.value.size = Math.min(rect.width, rect.height);
|
||||
})
|
||||
};
|
||||
|
||||
return {
|
||||
ctx,
|
||||
dimensions,
|
||||
updateDimensions
|
||||
} as LoadingCanvasContext
|
||||
}
|
||||
|
||||
// ===================== 动画创建函数 =====================
|
||||
/**
|
||||
* 创建环形加载动画
|
||||
* @param ctx 画布上下文
|
||||
* @param animationParams 动画参数
|
||||
* @returns 动画管理器实例
|
||||
*/
|
||||
function createCircularAnimation(ctx : DrawableContext, animationParams : AnimationParams) : AnimationManager {
|
||||
const { size, color, width, height } = animationParams
|
||||
let startAngle = 0; // 起始角度
|
||||
let endAngle = 0; // 结束角度
|
||||
let rotate = 0; // 旋转角度
|
||||
|
||||
// 动画参数配置
|
||||
const MIN_ANGLE = 5; // 最小保持角度
|
||||
const ARC_LENGTH = 359.5 // 最大弧长(避免闭合)
|
||||
const PI = Math.PI / 180 // 角度转弧度系数
|
||||
const SPEED = 0.018 // 动画速度
|
||||
const ROTATE_INTERVAL = 0.09 // 旋转增量
|
||||
const lineWidth = size / 10; // 线宽计算
|
||||
const x = width / 2 // 中心点X
|
||||
const y = height / 2 // 中心点Y
|
||||
const radius = size / 2 - lineWidth // 实际绘制半径
|
||||
|
||||
/** 帧绘制函数 */
|
||||
const drawFrame = () : boolean => {
|
||||
ctx.reset();
|
||||
|
||||
// 绘制圆弧
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
x,
|
||||
y,
|
||||
radius,
|
||||
startAngle * PI + rotate,
|
||||
endAngle * PI + rotate
|
||||
);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.stroke();
|
||||
|
||||
// 角度更新逻辑
|
||||
if (endAngle < ARC_LENGTH) {
|
||||
endAngle = Math.min(ARC_LENGTH, endAngle + (ARC_LENGTH - MIN_ANGLE) * SPEED);
|
||||
} else if (startAngle < ARC_LENGTH) {
|
||||
startAngle = Math.min(ARC_LENGTH, startAngle + (ARC_LENGTH - MIN_ANGLE) * SPEED);
|
||||
} else {
|
||||
// 重置时保留最小可见角度
|
||||
startAngle = 0;
|
||||
endAngle = MIN_ANGLE;
|
||||
}
|
||||
|
||||
rotate = (rotate + ROTATE_INTERVAL) % 360; // 持续旋转并限制范围
|
||||
ctx.update()
|
||||
return true
|
||||
}
|
||||
|
||||
return new AnimationManager(drawFrame)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建旋转器动画
|
||||
* @param ctx 画布上下文
|
||||
* @param animationParams 动画参数
|
||||
* @returns 动画管理器实例
|
||||
*/
|
||||
function createSpinnerAnimation(ctx : DrawableContext, animationParams : AnimationParams) : AnimationManager {
|
||||
const { size, color, center } = animationParams
|
||||
const steps = 12; // 旋转线条数量
|
||||
let step = 0; // 当前步数
|
||||
const lineWidth = size / 10; // 线宽
|
||||
// #ifdef APP-HARMONY
|
||||
const length = size / 3.4 - lineWidth; // 线长
|
||||
// #endif
|
||||
// #ifndef APP-HARMONY
|
||||
const length = size / 3.6 - lineWidth; // 线长
|
||||
// #endif
|
||||
const offset = size / 4; // 距中心偏移
|
||||
const [x, y] = center // 中心坐标
|
||||
|
||||
/** 生成颜色渐变数组 */
|
||||
function generateColorGradient(hex : string, steps : number) : string[] {
|
||||
const colors : string[] = []
|
||||
const _color = tinyColor(hex)
|
||||
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
_color.setAlpha(i / steps);
|
||||
colors.push(_color.toRgbString());
|
||||
}
|
||||
return colors
|
||||
}
|
||||
|
||||
// 计算颜色渐变
|
||||
let colors = computed(() : string[] => generateColorGradient(color, steps))
|
||||
/** 帧绘制函数 */
|
||||
const drawFrame = () : boolean => {
|
||||
ctx.reset();
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const stepAngle = 360 / steps; // 单步角度
|
||||
const angle = stepAngle * i; // 当前角度
|
||||
const index = (steps + i - step) % steps // 颜色索引
|
||||
// const index = (i + step) % steps;
|
||||
// console.log('index', index)
|
||||
// 计算线段坐标
|
||||
const radian = angle * Math.PI / 180;
|
||||
const cos = Math.cos(radian);
|
||||
const sin = Math.sin(radian);
|
||||
|
||||
// 绘制线段
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + offset * cos, y + offset * sin);
|
||||
ctx.lineTo(x + (offset + length) * cos, y + (offset + length) * sin);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.strokeStyle = colors.value[index];
|
||||
ctx.stroke();
|
||||
}
|
||||
// step += 1
|
||||
step = (step + 1) % steps; // 限制step范围
|
||||
ctx.update()
|
||||
return true
|
||||
}
|
||||
return new AnimationManager(drawFrame)
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算圆周上指定角度的点的坐标
|
||||
* @param centerX 圆心的 X 坐标
|
||||
* @param centerY 圆心的 Y 坐标
|
||||
* @param radius 圆的半径
|
||||
* @param angleDegrees 角度(以度为单位)
|
||||
* @returns 包含 X 和 Y 坐标的对象
|
||||
*/
|
||||
function getPointOnCircle(
|
||||
centerX : number,
|
||||
centerY : number,
|
||||
radius : number,
|
||||
angleDegrees : number
|
||||
) : number[] {
|
||||
// 将角度转换为弧度
|
||||
const angleRadians = (angleDegrees * Math.PI) / 180;
|
||||
|
||||
// 计算点的 X 和 Y 坐标
|
||||
const x = centerX + radius * Math.cos(angleRadians);
|
||||
const y = centerY + radius * Math.sin(angleRadians);
|
||||
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建失败状态动画(包含X图标和外围圆圈)
|
||||
* @param ctx 画布上下文
|
||||
* @param animationParams 动画参数
|
||||
* @returns 动画管理器实例
|
||||
*/
|
||||
function createFailedAnimation(ctx : DrawableContext, animationParams : AnimationParams) : AnimationManager {
|
||||
|
||||
const { width, height, size, color } = animationParams
|
||||
const innerSize = size * 0.8 // 内圈尺寸
|
||||
const lineWidth = innerSize / 10; // 线宽
|
||||
const lineLength = (size - lineWidth) / 2 // X长度
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
const [startX1, startY] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 45)
|
||||
const [startX2] = getPointOnCircle(centerX, centerY, lineLength / 2, 180 + 90 + 45)
|
||||
const angleRadians1 = 45 * Math.PI / 180
|
||||
const angleRadians2 = (45 - 90) * Math.PI / 180
|
||||
|
||||
const radius = (size - lineWidth) / 2
|
||||
const totalSteps = 36; // 总动画步数
|
||||
|
||||
function generateSteps(stepsCount : number) : Point[][] {
|
||||
|
||||
const halfStepsCount = stepsCount / 2;
|
||||
const step = lineLength / halfStepsCount
|
||||
const steps : Point[][] = []
|
||||
for (let i = 0; i < stepsCount; i++) {
|
||||
const sub : Point[] = []
|
||||
const index = i % 18 + 1
|
||||
if (i < halfStepsCount) {
|
||||
|
||||
const x2 = Math.sin(angleRadians1) * step * index + startX1
|
||||
const y2 = Math.cos(angleRadians1) * step * index + startY
|
||||
|
||||
const start1 = {
|
||||
x1: startX1,
|
||||
y1: startY,
|
||||
x2,
|
||||
y2,
|
||||
} as Point
|
||||
|
||||
sub.push(start1)
|
||||
} else {
|
||||
sub.push(steps[halfStepsCount - 1][0])
|
||||
const x2 = Math.sin(angleRadians2) * step * index + startX2
|
||||
const y2 = Math.cos(angleRadians2) * step * index + startY
|
||||
|
||||
const start2 = {
|
||||
x1: startX2,
|
||||
y1: startY,
|
||||
x2,
|
||||
y2,
|
||||
} as Point
|
||||
sub.push(start2)
|
||||
}
|
||||
steps.push(sub)
|
||||
}
|
||||
|
||||
return steps
|
||||
}
|
||||
const steps = generateSteps(totalSteps);
|
||||
|
||||
const drawFrame = () : boolean => {
|
||||
const drawStep = steps.shift()!
|
||||
ctx.reset()
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = color;
|
||||
|
||||
// 绘制逐渐显示的圆
|
||||
ctx.beginPath();
|
||||
ctx.arc(centerX, centerY, radius, 0, (2 * Math.PI) * (totalSteps - steps.length) / totalSteps);
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.stroke();
|
||||
|
||||
// 绘制X
|
||||
ctx.beginPath();
|
||||
drawStep.forEach(item => {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(item.x1, item.y1)
|
||||
ctx.lineTo(item.x2, item.y2)
|
||||
ctx.stroke();
|
||||
})
|
||||
ctx.update()
|
||||
return steps.length != 0
|
||||
}
|
||||
return new AnimationManager(drawFrame)
|
||||
}
|
||||
|
||||
|
||||
// ===================== 主Hook函数 =====================
|
||||
/**
|
||||
* 加载动画组合式函数
|
||||
* @param element 画布元素引用
|
||||
* @returns 加载控制器实例
|
||||
*/
|
||||
export function useLoading(
|
||||
element : Ref<UniElement | null>,
|
||||
// options : UseLoadingOptions
|
||||
) : UseLoadingReturn {
|
||||
const ticks = ref<TickType[]>([]);
|
||||
const currentTick = ref<TickType>('clear');
|
||||
const currentAnimation = shallowRef<AnimationManager | null>(null);
|
||||
const state = reactive<UseLoadingReturn>({
|
||||
color: '#000',
|
||||
type: 'circular',
|
||||
ratio: 1,
|
||||
play: () => {
|
||||
// if (currentTick.value == 'pause' && currentAnimation.value != null) {
|
||||
// // 从暂停状态恢复时直接启动动画管理器
|
||||
// currentAnimation.value?.start()
|
||||
// } else {
|
||||
// // 首次播放或类型变化时重新初始化
|
||||
// ticks.value.length = 0
|
||||
// ticks.value.push('play')
|
||||
// }
|
||||
ticks.value.length = 0
|
||||
ticks.value.push('play')
|
||||
},
|
||||
failed: () => {
|
||||
ticks.value.length = 0
|
||||
ticks.value.push('failed')
|
||||
},
|
||||
clear: () => {
|
||||
ticks.value.length = 0
|
||||
ticks.value.push('clear')
|
||||
},
|
||||
destroy: () => {
|
||||
ticks.value.length = 0
|
||||
ticks.value.push('destroy')
|
||||
},
|
||||
pause: () => {
|
||||
ticks.value.push('pause')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const { ctx, dimensions, updateDimensions } = useCanvas();
|
||||
const resizeObserver : UniResizeObserver = new UniResizeObserver((_entries : UniResizeObserverEntry[]) => {
|
||||
updateDimensions(element.value!)
|
||||
});
|
||||
|
||||
|
||||
// 计算动画参数
|
||||
const animationParams = computed(() : AnimationParams => {
|
||||
return {
|
||||
width: dimensions.value.width,
|
||||
height: dimensions.value.height,
|
||||
center: [dimensions.value.width / 2, dimensions.value.height / 2],
|
||||
color: state.color,
|
||||
size: state.ratio > 1 ? state.ratio : dimensions.value.size * state.ratio
|
||||
} as AnimationParams
|
||||
})
|
||||
|
||||
const startAnimation = (type : LoadingType) => {
|
||||
currentAnimation.value?.pause();
|
||||
if (currentAnimation.value?.type == type) {
|
||||
currentAnimation.value!.start()
|
||||
return
|
||||
}
|
||||
if (type == 'circular') {
|
||||
currentAnimation.value = createCircularAnimation(ctx.value!, animationParams.value)
|
||||
currentAnimation.value!.time = 1000 / 30
|
||||
currentAnimation.value!.start()
|
||||
return
|
||||
}
|
||||
if (type == 'spinner') {
|
||||
currentAnimation.value = createSpinnerAnimation(ctx.value!, animationParams.value)
|
||||
currentAnimation.value!.time = 1000 / 10
|
||||
currentAnimation.value!.start()
|
||||
return
|
||||
}
|
||||
if (type == 'failed') {
|
||||
currentAnimation.value = createFailedAnimation(ctx.value!, animationParams.value)
|
||||
currentAnimation.value?.start()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const failed = () => {
|
||||
startAnimation('failed')
|
||||
}
|
||||
const play = () => {
|
||||
startAnimation(state.type)
|
||||
}
|
||||
const clear = () => {
|
||||
currentAnimation.value?.stop();
|
||||
ctx.value?.reset();
|
||||
ctx.value?.update();
|
||||
}
|
||||
const destroy = () => {
|
||||
clear();
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
|
||||
|
||||
watch(animationParams, () => {
|
||||
if (['clear', 'destroy', 'pause'].includes(currentTick.value)) return
|
||||
startAnimation(state.type)
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (ctx.value == null) return
|
||||
|
||||
const tick = ticks.value.pop()
|
||||
if (ticks.value.length > 0) { }
|
||||
if (tick != null) {
|
||||
currentTick.value = tick
|
||||
}
|
||||
if (tick == 'play') {
|
||||
play()
|
||||
return
|
||||
}
|
||||
if (tick == 'failed') {
|
||||
failed()
|
||||
return
|
||||
}
|
||||
if (tick == 'clear') {
|
||||
clear()
|
||||
return
|
||||
}
|
||||
if (tick == 'destroy') {
|
||||
destroy()
|
||||
return
|
||||
}
|
||||
if (tick == 'pause') {
|
||||
if (currentAnimation.value == null) {
|
||||
play()
|
||||
}
|
||||
currentAnimation.value?.pause();
|
||||
}
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
if (element.value == null) return
|
||||
resizeObserver.observe(element.value!);
|
||||
// #ifdef APP-IOS || APP-HARMONY
|
||||
// APP-HARMONY 在下拉组件中使用居中会影响绘制 故先获取一次
|
||||
updateDimensions(element.value!)
|
||||
// #endif
|
||||
})
|
||||
|
||||
onUnmounted(destroy);
|
||||
|
||||
|
||||
return state
|
||||
}
|
||||
110
qiming-mobile/uni_modules/lime-loading/package.json
Normal file
110
qiming-mobile/uni_modules/lime-loading/package.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"id": "lime-loading",
|
||||
"displayName": "lime-loading 加载",
|
||||
"version": "0.1.9",
|
||||
"description": "lime-loading 全端通用加载插件.用于表示页面或操作的加载状态,给予用户反馈的同时减缓等待的焦虑感。支持uniapp/uniappx",
|
||||
"keywords": [
|
||||
"loading",
|
||||
"lime-loading",
|
||||
"加载",
|
||||
"uvue"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.92",
|
||||
"uni-app": "^4.73",
|
||||
"uni-app-x": "^4.76"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "√",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"lime-shared",
|
||||
"lime-style",
|
||||
"lime-color"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "√",
|
||||
"aliyun": "√",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "√",
|
||||
"vue3": "√"
|
||||
},
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"vue": "√",
|
||||
"nvue": "-",
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√",
|
||||
"alipay": "-",
|
||||
"toutiao": "-",
|
||||
"baidu": "-",
|
||||
"kuaishou": "-",
|
||||
"jd": "-",
|
||||
"harmony": "-",
|
||||
"qq": "-",
|
||||
"lark": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "-",
|
||||
"union": "-"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
@import '~@/uni_modules/lime-style/index.scss';
|
||||
/* #ifdef uniVersion >= 4.75 */
|
||||
$use-css-var: true;
|
||||
/* #endif */
|
||||
|
||||
|
||||
$overlay: #{$prefix}-overlay;
|
||||
$overlay-bg-color: create-var(overlay-bg-color, $bg-color-mask);
|
||||
$overlay-z-index: create-var(overlay-z-index, 998);
|
||||
$overlay-transition-duration: create-var(overlay-transition-duration, 300ms);
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-NVUE || APP-HARMONY */
|
||||
$overlay-blur: blur(create-var(overlay-blur, 4px));
|
||||
/* #endif */
|
||||
|
||||
.#{$overlay}{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
background-color: $overlay-bg-color;
|
||||
|
||||
transition-property: opacity;
|
||||
transition-timing-function: ease;
|
||||
z-index: $overlay-z-index;
|
||||
opacity: 1; // uniapp x ios 必须要设置
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-NVUE || APP-HARMONY */
|
||||
backdrop-filter: $overlay-blur;
|
||||
transition-duration: $overlay-transition-duration;
|
||||
/* #endif */
|
||||
/* #ifdef APP-ANDROID || APP-IOS || APP-NVUE || APP-HARMONY */
|
||||
transition-duration: 300ms;//$overlay-transition-duration;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.l-fade-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.l-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<view v-if="inited"
|
||||
class="l-overlay"
|
||||
ref="overlayRef"
|
||||
:class="[lClass, classes]"
|
||||
:style="[styles, lStyle]"
|
||||
@click.stop="onClick"
|
||||
@touchmove.stop="noop"
|
||||
@transitionend="finished"
|
||||
:aria-role="ariaRole"
|
||||
:aria-label="ariaLabel">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts" setup>
|
||||
/**
|
||||
* Overlay 遮罩层组件
|
||||
* @description 用于创建模态遮罩层,通常配合弹窗、对话框等组件使用
|
||||
* <br>插件类型:LOverlayComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-overlay
|
||||
*
|
||||
* @property {string} ariaLabel 无障碍访问标签(需语义化描述作用)
|
||||
* @property {string} ariaRole ARIA角色属性(默认:'presentation')
|
||||
* @property {string} lClass 自定义类名(会覆盖默认样式)
|
||||
* @property {string} bgColor 背景颜色(默认:'rgba(0, 0, 0, 0.7)')
|
||||
* @property {string} lStyle 自定义样式(最高优先级,支持CSS字符串)
|
||||
* @property {number} duration 背景过渡动画时长(单位:ms,默认:300)
|
||||
* @property {boolean} preventScrollThrough 阻止滚动穿透(默认:true)
|
||||
* @property {boolean} visible 是否显示遮罩层(支持v-model)
|
||||
* @property {number} zIndex 层级(默认:1000)
|
||||
* @event {Function} click 点击遮罩层时触发(常用于关闭操作)
|
||||
* @event {Function} before-enter
|
||||
* @event {Function} enter
|
||||
* @event {Function} after-enter
|
||||
* @event {Function} before-leave
|
||||
* @event {Function} leave
|
||||
* @event {Function} after-leave
|
||||
*/
|
||||
import { useTransition, type UseTransitionOptions, type TransitionEmitStatus } from '@/uni_modules/lime-transition';
|
||||
import { OverlayProps } from './type';
|
||||
// defineOptions({
|
||||
// name:'l-overlay'
|
||||
// })
|
||||
|
||||
const props = withDefaults(defineProps<OverlayProps>(), {
|
||||
ariaLabel: '关闭',
|
||||
ariaRole: 'button',
|
||||
preventScrollThrough: true,
|
||||
zIndex: 998,
|
||||
visible: false,
|
||||
duration: 300,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['click', 'before-enter', 'enter', 'after-enter', 'before-leave', 'leave', 'after-leave'])
|
||||
|
||||
const {inited, display, classes, finished} = useTransition({
|
||||
defaultName: 'fade',
|
||||
appear: props.visible,
|
||||
emits: (name:TransitionEmitStatus) => { emit(name) },
|
||||
visible: (): boolean => props.visible,
|
||||
duration: props.duration,
|
||||
} as UseTransitionOptions)
|
||||
|
||||
const styles = computed<Map<string,any>>(():Map<string,any> => {
|
||||
const style = new Map<string,any>();
|
||||
if (props.bgColor != null) {
|
||||
style.set("background-color", props.bgColor!)
|
||||
}
|
||||
if (props.zIndex > 0) {
|
||||
style.set("z-index", props.zIndex)
|
||||
}
|
||||
// #ifndef APP || WEB
|
||||
style.set('transition-duration', props.duration + 'ms')
|
||||
if (!display.value) {
|
||||
style.set("display", "none")
|
||||
}
|
||||
// #endif
|
||||
return style
|
||||
})
|
||||
|
||||
const noop = () => {}
|
||||
const onClick = (event: UniPointerEvent) =>{
|
||||
// event.stopPropagation()
|
||||
emit('click', !props.visible)
|
||||
}
|
||||
// #ifdef APP || WEB
|
||||
const overlayRef = ref<UniElement|null>(null)
|
||||
|
||||
watchEffect(()=>{
|
||||
overlayRef.value?.style.setProperty('transition-duration', `${props.duration}ms`)
|
||||
if(!display.value){
|
||||
overlayRef.value?.style.setProperty('display', "none")
|
||||
} else {
|
||||
overlayRef.value?.style.setProperty('display', "flex")
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<view
|
||||
v-if="inited"
|
||||
class="l-overlay"
|
||||
:class="[lClass, classes]"
|
||||
:style="[styles, lStyle]"
|
||||
@click.stop="onClick"
|
||||
@touchmove.stop="noop"
|
||||
@transitionend="finished"
|
||||
:aria-role="ariaRole || 'button'"
|
||||
:aria-label="ariaLabel || '关闭'">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Overlay 遮罩层组件
|
||||
* @description 用于创建模态遮罩层,通常配合弹窗、对话框等组件使用
|
||||
* <br>插件类型:LOverlayComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-overlay
|
||||
*
|
||||
* @property {string} ariaLabel 无障碍访问标签(需语义化描述作用)
|
||||
* @property {string} ariaRole ARIA角色属性(默认:'presentation')
|
||||
* @property {string} lClass 自定义类名(会覆盖默认样式)
|
||||
* @property {string} bgColor 背景颜色(默认:'rgba(0, 0, 0, 0.7)')
|
||||
* @property {string} lStyle 自定义样式(最高优先级,支持CSS字符串)
|
||||
* @property {number} duration 背景过渡动画时长(单位:ms,默认:300)
|
||||
* @property {boolean} preventScrollThrough 阻止滚动穿透(默认:true)
|
||||
* @property {boolean} visible 是否显示遮罩层(支持v-model)
|
||||
* @property {number} zIndex 层级(默认:1000)
|
||||
* @event {Function} click 点击遮罩层时触发(常用于关闭操作)
|
||||
* @event {Function} before-enter
|
||||
* @event {Function} enter
|
||||
* @event {Function} after-enter
|
||||
* @event {Function} before-leave
|
||||
* @event {Function} leave
|
||||
* @event {Function} after-leave
|
||||
*/
|
||||
|
||||
import { computed, defineComponent } from '@/uni_modules/lime-shared/vue';
|
||||
import { useTransition, type UseTransitionOptions, type TransitionEmitStatus } from '@/uni_modules/lime-transition';
|
||||
import overlayProps from './props';
|
||||
|
||||
export default defineComponent({
|
||||
props: overlayProps,
|
||||
emits: ['click', 'before-enter', 'enter', 'after-enter', 'before-leave', 'leave', 'after-leave'],
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
externalClasses: true,
|
||||
},
|
||||
externalClasses: ['l-class'],
|
||||
setup(props, { emit }) {
|
||||
|
||||
const {inited, display, classes, finished} = useTransition({
|
||||
defaultName: 'fade',
|
||||
appear: props.visible,
|
||||
emits: (name:TransitionEmitStatus) => { emit(name) },
|
||||
visible: (): boolean => props.visible,
|
||||
duration: props.duration,
|
||||
} as UseTransitionOptions)
|
||||
|
||||
|
||||
const styles = computed(() => ({
|
||||
'transition-duration': props.duration + 'ms',
|
||||
'background': props.bgColor,
|
||||
'z-index': props.zIndex,
|
||||
'display': !display.value ? 'none' : '',
|
||||
}))
|
||||
|
||||
const onClick = () => {
|
||||
emit('click', !props.visible)
|
||||
}
|
||||
const noop = (e) => {
|
||||
e?.preventDefault();
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
inited,
|
||||
styles,
|
||||
classes,
|
||||
noop,
|
||||
onClick,
|
||||
finished
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
export default {
|
||||
visible: Boolean,
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 998,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 300
|
||||
},
|
||||
preventScrollThrough: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
bgColor: String,
|
||||
lStyle: [String, Object],
|
||||
lClass: String,
|
||||
ariaLabel: String,
|
||||
ariaRole: String,
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// @ts-nocheck
|
||||
export interface OverlayProps {
|
||||
ariaLabel: string;
|
||||
ariaRole: string;
|
||||
lClass?: string;
|
||||
/**
|
||||
* 遮罩层的背景色
|
||||
*/
|
||||
bgColor ?: string;
|
||||
/**
|
||||
* 遮罩层自定义样式。优先级低于其他属性
|
||||
*/
|
||||
lStyle ?: string|UTSJSONObject;
|
||||
/**
|
||||
* 背景色过渡时间,单位毫秒
|
||||
*/
|
||||
duration : number;
|
||||
/**
|
||||
* 防止滚动穿透,即不允许点击和滚动
|
||||
*/
|
||||
preventScrollThrough : boolean;
|
||||
/**
|
||||
* 是否展示
|
||||
*/
|
||||
visible : boolean;
|
||||
/**
|
||||
* 遮罩的层级
|
||||
*/
|
||||
zIndex : number;
|
||||
}
|
||||
109
qiming-mobile/uni_modules/lime-overlay/package.json
Normal file
109
qiming-mobile/uni_modules/lime-overlay/package.json
Normal file
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"id": "lime-overlay",
|
||||
"displayName": "lime-overlay 遮罩层",
|
||||
"version": "0.1.2",
|
||||
"description": "lime-overlay 通过遮罩层,可以强调部分内容,兼容uniapp/uniappx",
|
||||
"keywords": [
|
||||
"lime-overlay",
|
||||
"overlay",
|
||||
"遮罩层"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.28",
|
||||
"uni-app": "^4.52",
|
||||
"uni-app-x": "^4.75"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"lime-style",
|
||||
"lime-shared",
|
||||
"lime-transition"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "√",
|
||||
"vue3": "√"
|
||||
},
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"vue": "√",
|
||||
"nvue": "-",
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√",
|
||||
"alipay": "-",
|
||||
"toutiao": "-",
|
||||
"baidu": "-",
|
||||
"kuaishou": "-",
|
||||
"jd": "-",
|
||||
"harmony": "-",
|
||||
"qq": "-",
|
||||
"lark": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "-",
|
||||
"union": "-"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "21"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
109
qiming-mobile/uni_modules/lime-overlay/readme - 副本.md
Normal file
109
qiming-mobile/uni_modules/lime-overlay/readme - 副本.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# lime-overlay 遮罩层
|
||||
- 通过遮罩层,可以强调部分内容
|
||||
- 插件依赖`lime-style`,`lime-shared`,`lime-transition`,不喜勿下
|
||||
|
||||
|
||||
> 插件依赖:`lime-style`、`lime-shared`、`lime-transition`
|
||||
[overlay【站点1】](https://limex.qcoon.cn/components/overlay.html)
|
||||
[overlay【站点2】](https://limeui.netlify.app/components/overlay.html)
|
||||
[overlay【站点3】](https://limeui.familyzone.top/components/overlay.html)
|
||||
|
||||
|
||||
## 安装
|
||||
在插件市场导入即可,首次安装可能需要重新编译
|
||||
|
||||
## 代码演示
|
||||
### 基础用法
|
||||
```html
|
||||
<button @click="onClick">显示</button>
|
||||
<l-overlay :visible="show" @click="onClick"></l-overlay>
|
||||
```
|
||||
```js
|
||||
const show = refl(lflalse);
|
||||
const onClick = () => {
|
||||
show.value = !show.value
|
||||
}
|
||||
```
|
||||
|
||||
### 嵌入内容
|
||||
通过默认插槽可以在遮罩层上嵌入任意内容。
|
||||
```html
|
||||
<button @click="onClick">显示</button>
|
||||
<l-overlay :visible="show" @click="onClick">
|
||||
<view class="wrapper">
|
||||
<view class="block" />
|
||||
</view>
|
||||
</l-overlay>
|
||||
```
|
||||
```js
|
||||
const show = ref(false);
|
||||
const onClick = () => {
|
||||
show.value = !show.value
|
||||
}
|
||||
```
|
||||
|
||||
### 查看示例
|
||||
- 导入后直接使用这个标签查看演示效果
|
||||
|
||||
```html
|
||||
<!-- // 代码位于 uni_modules/lime-overlay/compoents/lime-overlay -->
|
||||
<lime-overlay />
|
||||
```
|
||||
|
||||
|
||||
### 插件标签
|
||||
- 默认 l-overlay 为 component
|
||||
- 默认 lime-overlay 为 demo
|
||||
|
||||
### Vue2使用
|
||||
- 插件使用了`composition-api`, 如果你希望在vue2中使用请按官方的教程[vue-composition-api](https://uniapp.dcloud.net.cn/tutorial/vue-composition-api.html)配置
|
||||
- 关键代码是: 在main.js中 在vue2部分加上这一段即可
|
||||
```js
|
||||
// vue2
|
||||
import Vue from 'vue'
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
Vue.use(VueCompositionAPI)
|
||||
```
|
||||
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| visible | 是否展示遮罩层 | _boolean_ | `false` |
|
||||
| z-index | z-index 层级 | _number_ | `1000` |
|
||||
| duration | 动画时长,单位ms,设置为 0 可以禁用动画 | _number_ | `300` |
|
||||
| lStyle | 样式 | _string_ | `` |
|
||||
| destoryOnClose | 隐藏时是否销毁 | _boolean_ | `false` |
|
||||
|
||||
### Events
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
| ------ | ---------- | ------------------- |
|
||||
| click | 点击时触发 | _-_ |
|
||||
|
||||
### Slots
|
||||
|
||||
| 名称 | 说明 |
|
||||
| ------- | ---------------------------------- |
|
||||
| default | 默认插槽,用于在遮罩层上方嵌入内容 |
|
||||
|
||||
|
||||
## 主题定制
|
||||
|
||||
### 样式变量
|
||||
|
||||
组件提供了下列 CSS 变量,可用于自定义样式,uvue app无效。
|
||||
|
||||
| 名称 | 默认值 | 描述 |
|
||||
| ------------------------ | -------------------- | ---- |
|
||||
| --l-overlay-z-index | _1000_ | - |
|
||||
| --l-overlay-background | _rgba(0, 0, 0, 0.6)_ | - |
|
||||
|
||||
## 打赏
|
||||
|
||||
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
|
||||

|
||||

|
||||
@@ -0,0 +1,157 @@
|
||||
@import '~@/uni_modules/lime-style/index.scss';
|
||||
// $prefix: l !default;
|
||||
/* #ifdef uniVersion >= 4.75 */
|
||||
$use-css-var: true;
|
||||
/* #endif */
|
||||
|
||||
$popup: #{$prefix}-popup;
|
||||
|
||||
$popup-bg-color: create-var(popup-bg-color, $bg-color-elevated);
|
||||
$popup-close-icon-color: create-var(popup-close-icon-color, $text-color-2);
|
||||
$popup-border-radius: create-var(popup-border-radius, $border-radius-lg);
|
||||
|
||||
|
||||
.#{$popup} {
|
||||
position: fixed;
|
||||
// z-index: 11500;
|
||||
// max-height: 100vh;
|
||||
// transition: translateY 1000ms ease;
|
||||
transition-duration: 300ms;
|
||||
transition-property: transform, opacity;
|
||||
transition-timing-function: ease;
|
||||
background-color: $popup-bg-color;
|
||||
overflow: visible;
|
||||
opacity: 1; // uniapp x ios 必须写
|
||||
|
||||
&__close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 20rpx;
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
color: $popup-close-icon-color;
|
||||
/* #endif */
|
||||
&-icon {
|
||||
color: $popup-close-icon-color;
|
||||
}
|
||||
}
|
||||
|
||||
&--top {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-bottom-left-radius: $popup-border-radius;
|
||||
border-bottom-right-radius: $popup-border-radius;
|
||||
// transform: scale(1) translateY(0)
|
||||
transform: scale(1) translate(0, 0)
|
||||
}
|
||||
|
||||
&--bottom {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-top-left-radius: $popup-border-radius;
|
||||
border-top-right-radius: $popup-border-radius;
|
||||
// transform: scale(1) translateY(0);
|
||||
transform: scale(1) translate(0, 0);
|
||||
|
||||
}
|
||||
&--safe-top {
|
||||
/* #ifndef UNI-APP-X */
|
||||
padding-top: constant(safe-area-inset-top);
|
||||
padding-top: env(safe-area-inset-top);
|
||||
/* #endif */
|
||||
|
||||
/* #ifdef UNI-APP-X */
|
||||
padding-top: var(--uni-safe-area-inset-top);
|
||||
// padding-top: constant(safe-area-inset-top);
|
||||
// padding-top: env(safe-area-inset-top);
|
||||
/* #endif */
|
||||
}
|
||||
&--safe-bottom {
|
||||
/* #ifndef UNI-APP-X */
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
/* #endif */
|
||||
|
||||
/* #ifdef UNI-APP-X */
|
||||
padding-bottom: var(--uni-safe-area-inset-bottom);
|
||||
// padding-bottom: constant(safe-area-inset-bottom);
|
||||
// padding-bottom: env(safe-area-inset-bottom);
|
||||
/* #endif */
|
||||
}
|
||||
&--left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
// transform: scale(1) translateX(0);
|
||||
transform: scale(1) translate(0, 0);
|
||||
// height: 100vh;
|
||||
}
|
||||
|
||||
&--right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
// transform: scale(1) translateX(0);
|
||||
transform: scale(1) translate(0, 0);
|
||||
// height: 100vh;
|
||||
}
|
||||
|
||||
&--center {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
/* #ifdef APP-IOS */
|
||||
transform: translate(-50%, -50%);
|
||||
/* #endif */
|
||||
/* #ifndef APP-IOS */
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
/* #endif */
|
||||
transform-origin: 50% 50%;
|
||||
// border-radius: $popup-border-radius;
|
||||
@include border-radius($popup-border-radius);
|
||||
}
|
||||
|
||||
&.#{$popup}-fade-enter,
|
||||
&.#{$popup}-fade-leave-to {
|
||||
opacity: 0;
|
||||
&.#{$popup}--top {
|
||||
// transform: translateY(-100%);
|
||||
transform: scale(1) translate(0, -100%);
|
||||
}
|
||||
|
||||
&.#{$popup}--bottom {
|
||||
// transform: translateY(100%);
|
||||
transform: scale(1) translate(0, 100%);
|
||||
}
|
||||
|
||||
&.#{$popup}--left {
|
||||
// transform: translateX(-100%);
|
||||
transform: scale(1) translate(-100%, 0);
|
||||
}
|
||||
|
||||
&.#{$popup}--right {
|
||||
// transform: translateX(100%);
|
||||
transform: scale(1) translate(100%, 0);
|
||||
}
|
||||
|
||||
&.#{$popup}--center {
|
||||
// transform: scale(0.6) translate(-50%, -50%);
|
||||
/* #ifndef APP-IOS */
|
||||
transform: translate(-50%, -50%) scale(0.6);
|
||||
/* #endif */
|
||||
/* #ifdef APP-IOS */
|
||||
transform: translate(-50%, -50%);
|
||||
/* #endif */
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.#{$prefix}-dialog-enter,
|
||||
&.#{$prefix}-dialog-leave-to {
|
||||
&.#{$popup}--center {
|
||||
transform: scale(0.6) translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<!-- #ifdef APP-HARMONY -->
|
||||
<view style="overflow: visible;">
|
||||
<!-- #endif -->
|
||||
<l-overlay
|
||||
:visible="innerValue"
|
||||
:zIndex="overlayZIndex"
|
||||
:appear="true"
|
||||
:preventScrollThrough="preventScrollThrough"
|
||||
:l-style="overlayStyle"
|
||||
@click="handleOverlayClick"
|
||||
v-if="destroyOnClose ? display && overlay : overlay">
|
||||
</l-overlay>
|
||||
<view class="l-popup"
|
||||
ref="popupRef"
|
||||
v-if="destroyOnClose ? display : inited"
|
||||
:class="rootClass"
|
||||
:style="[styles, lStyle]"
|
||||
@transitionend="finished">
|
||||
<!-- #ifdef APP-HARMONY -->
|
||||
<view style="overflow: visible;" :key="hosKey" ><slot></slot></view>
|
||||
<view class="l-popup__close" v-if="closeable" @click="handleClose">
|
||||
<slot name="close-btn">
|
||||
<l-icon class="l-popup__close-icon" :name="closeIcon" size="27px" :color="iconColor" />
|
||||
</slot>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-HARMONY -->
|
||||
<slot></slot>
|
||||
<view class="l-popup__close" v-if="closeable" @click="handleClose">
|
||||
<slot name="close-btn">
|
||||
<l-icon class="l-popup__close-icon" :name="closeIcon" size="27px" :color="iconColor" />
|
||||
</slot>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<!-- #ifdef APP-HARMONY -->
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
<script lang="uts" setup>
|
||||
/**
|
||||
* Popup 弹出层组件
|
||||
* @description 提供多种位置的弹窗展示能力,支持自定义内容和动画效果
|
||||
* <br>插件类型:LPopupComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-popup
|
||||
*
|
||||
* @property {boolean} closeable 显示关闭按钮(默认:false)
|
||||
* @property {boolean} closeOnClickOverlay 点击遮罩关闭(默认:true)
|
||||
* @property {boolean} destroyOnClose 关闭时销毁内容(默认:false)
|
||||
* @property {string} overlayStyle 遮罩层样式(支持CSS字符串)
|
||||
* @property {'top' | 'left' | 'right' | 'bottom' | 'center' | ''} position 弹出位置
|
||||
* @value top 从顶部滑入
|
||||
* @value bottom 从底部滑入
|
||||
* @value left 从左侧滑入
|
||||
* @value right 从右侧滑入
|
||||
* @value center 居中显示(默认)
|
||||
* @property {boolean} preventScrollThrough 阻止滚动穿透(默认:true)
|
||||
* @property {boolean} overlay 显示遮罩层(默认:true)
|
||||
* @property {string} transitionName 自定义动画名称(配合transition使用)
|
||||
* @property {string|number|Array} radius 圆角 可以是字符,数值,数组
|
||||
* @property {boolean} visible 控制显示/隐藏(支持v-model)
|
||||
* @property {number} zIndex 组件层级(默认:999)
|
||||
* @property {number} duration 动画时长(单位ms,默认:300)
|
||||
* @property {string} bgColor 内容区域背景色(默认:#ffffff)
|
||||
* @property {string} closeIcon 关闭图标名称/路径(默认:'close')
|
||||
* @property {string} iconColor 关闭图标颜色(默认:#333)
|
||||
* @property {string} lStyle 自定义内容区样式(支持CSS字符串)
|
||||
* @property {boolean} safeAreaInsetBottom 适配底部安全区域(默认:true)
|
||||
* @property {boolean} safeAreaInsetTop 适配顶部安全区域(默认:true)
|
||||
* @event {Function} close 关闭时触发(返回触发来源:'close-btn' | 'overlay')
|
||||
* @event {Function} change 切换时触发
|
||||
* @event {Function} click-overlay 点击遮罩触发
|
||||
* @event {Function} click-close 点击关闭触发
|
||||
* @event {Function} open 打开触发
|
||||
* @event {Function} opened 打开完成触发
|
||||
* @event {Function} closed 关闭完成触发
|
||||
* @event {Function} before-enter
|
||||
* @event {Function} enter
|
||||
* @event {Function} after-enter
|
||||
* @event {Function} before-leave
|
||||
* @event {Function} leave
|
||||
* @event {Function} after-leave
|
||||
*/
|
||||
import { useTransition, type UseTransitionOptions, type TransitionEmitStatus } from '@/uni_modules/lime-transition';
|
||||
import { convertRadius } from './utils'
|
||||
import { PopupProps } from './type';
|
||||
|
||||
const emit = defineEmits(['change', 'click-overlay', 'click-close', 'open', 'opened','close','closed', 'before-enter', 'enter', 'after-enter', 'before-leave', 'leave', 'after-leave'])
|
||||
const props = withDefaults(defineProps<PopupProps>(),{
|
||||
closeable: false,
|
||||
overlay: true,
|
||||
closeOnClickOverlay: true,
|
||||
preventScrollThrough: true,
|
||||
destroyOnClose: false,
|
||||
safeAreaInsetBottom: true,
|
||||
safeAreaInsetTop: false,
|
||||
position: 'center',
|
||||
zIndex: 999,
|
||||
duration: 300,
|
||||
closeIcon: 'close'
|
||||
})
|
||||
|
||||
const modelValue = defineModel({type: Boolean});
|
||||
const innerValue = computed({
|
||||
set(value: boolean) {
|
||||
modelValue.value = value;
|
||||
emit('change', value)
|
||||
},
|
||||
get():boolean {
|
||||
// #ifdef APP-ANDROID
|
||||
return (props.visible ?? false) || modelValue.value// ?? false
|
||||
// #endif
|
||||
// #ifndef APP-ANDROID
|
||||
return props.visible || modelValue.value
|
||||
// #endif
|
||||
}
|
||||
} as WritableComputedOptions<boolean>)
|
||||
const hosKey = ref(0)
|
||||
const status = ref<TransitionEmitStatus>('before-enter')
|
||||
const {inited, display, classes, finished} = useTransition({
|
||||
defaultName: props.transitionName ?? 'popup-fade',
|
||||
appear: innerValue.value,
|
||||
emits: (name:TransitionEmitStatus) => {
|
||||
status.value = name
|
||||
if(name == 'before-enter') {
|
||||
emit('open')
|
||||
|
||||
} else if(name == 'after-enter') {
|
||||
emit('opened')
|
||||
setTimeout(()=>{
|
||||
if(hosKey.value > 0) return
|
||||
hosKey.value++
|
||||
},5)
|
||||
} else if(name == 'before-leave') {
|
||||
emit('close')
|
||||
} else if(name == 'after-leave') {
|
||||
emit('closed')
|
||||
}
|
||||
emit(name)
|
||||
},
|
||||
visible: (): boolean => innerValue.value,
|
||||
duration: props.duration,
|
||||
} as UseTransitionOptions)
|
||||
|
||||
const overlayZIndex = computed(():number => props.zIndex > 0 ? props.zIndex - 1: 998);
|
||||
|
||||
const rootClass = computed(():string=>{
|
||||
const safe = props.safeAreaInsetTop && props.position == 'top'
|
||||
? 'l-popup--safe-top'
|
||||
: props.safeAreaInsetBottom && props.position == 'bottom'
|
||||
? 'l-popup--safe-bottom'
|
||||
: ''
|
||||
|
||||
return `l-popup--${props.position} ${safe} ${classes.value}`
|
||||
})
|
||||
|
||||
const {safeAreaInsets} = uni.getWindowInfo()
|
||||
|
||||
const styles = computed<Map<string,any>>(():Map<string,any> => {
|
||||
const style = new Map<string,any>();
|
||||
// #ifdef APP-ANDROID || APP-IOS || APP-HARMONY
|
||||
style.set('transition-duration', (['after-leave', 'before-enter'].includes(status.value) ? 0 : props.duration) + 'ms')
|
||||
// #endif
|
||||
// #ifndef APP-ANDROID || APP-IOS || APP-HARMONY
|
||||
style.set('transition-duration', props.duration + 'ms')
|
||||
// #endif
|
||||
if (props.bgColor != null) {
|
||||
style.set("background", props.bgColor!)
|
||||
}
|
||||
if (props.zIndex > 0) {
|
||||
style.set("z-index", props.zIndex)
|
||||
}
|
||||
|
||||
// if(props.safeAreaInsetBottom && props.position == 'bottom') {
|
||||
// style.set('padding-bottom', safeAreaInsets.bottom + 'px')
|
||||
// }
|
||||
// if(props.safeAreaInsetTop && props.position == 'top') {
|
||||
// style.set('padding-top', safeAreaInsets.top + 'px')
|
||||
// }
|
||||
|
||||
if(props.radius != null) {
|
||||
const values = convertRadius(props.radius!)
|
||||
style.set('border-top-left-radius', values[0])
|
||||
style.set('border-top-right-radius', values[1])
|
||||
style.set('border-bottom-right-radius', values[2])
|
||||
style.set('border-bottom-left-radius', values[3])
|
||||
}
|
||||
|
||||
// #ifndef APP
|
||||
if (!display.value) {
|
||||
// 会导致picker触发pick
|
||||
// style.set("display", "none")
|
||||
style.set("pointer-events", "none")
|
||||
}
|
||||
// #endif
|
||||
|
||||
return style
|
||||
})
|
||||
const handleOverlayClick = () => {
|
||||
if(props.closeOnClickOverlay) {
|
||||
innerValue.value = false;
|
||||
emit('click-overlay')
|
||||
}
|
||||
}
|
||||
const handleClose = () => {
|
||||
innerValue.value = false;
|
||||
emit('click-close')
|
||||
}
|
||||
|
||||
// #ifdef APP
|
||||
const popupRef = ref<UniElement|null>(null)
|
||||
|
||||
watchEffect(()=>{
|
||||
// 会导致picker触发pick
|
||||
if(!display.value) {
|
||||
// popupRef.value?.style.setProperty('display', 'none')
|
||||
popupRef.value?.style.setProperty('pointer-events', 'none')
|
||||
popupRef.value?.style.setProperty('z-index', -10000)
|
||||
} else {
|
||||
// popupRef.value?.style.setProperty('display', 'flex')
|
||||
popupRef.value?.style.setProperty('pointer-events', 'auto')
|
||||
popupRef.value?.style.setProperty('z-index', props.zIndex)
|
||||
}
|
||||
})
|
||||
|
||||
// #endif
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "./index-u";
|
||||
</style>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<view>
|
||||
<l-overlay
|
||||
:visible="innerValue"
|
||||
:zIndex="overlayZIndex"
|
||||
:preventScrollThrough="preventScrollThrough"
|
||||
:l-style="overlayStyle"
|
||||
@click="handleOverlayClick"
|
||||
v-if="destroyOnClose ? display && overlay : overlay">
|
||||
</l-overlay>
|
||||
<view class="l-popup"
|
||||
v-if="destroyOnClose ? display : inited"
|
||||
:class="rootClass"
|
||||
:style="[styles, lStyle]"
|
||||
@transitionend="finished">
|
||||
<slot></slot>
|
||||
<view class="l-popup__close" v-if="closeable" @click="handleClose">
|
||||
<slot name="close-btn">
|
||||
<l-icon class="l-popup__close-icon" :name="closeIcon" v-if="closeable" size="54rpx" :color="iconColor" />
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
/**
|
||||
* Popup 弹出层组件
|
||||
* @description 提供多种位置的弹窗展示能力,支持自定义内容和动画效果
|
||||
* <br>插件类型:LPopupComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-popup
|
||||
*
|
||||
* @property {boolean} closeable 显示关闭按钮(默认:false)
|
||||
* @property {boolean} closeOnClickOverlay 点击遮罩关闭(默认:true)
|
||||
* @property {boolean} destroyOnClose 关闭时销毁内容(默认:false)
|
||||
* @property {string} overlayStyle 遮罩层样式(支持CSS字符串)
|
||||
* @property {'top' | 'left' | 'right' | 'bottom' | 'center' | ''} position 弹出位置
|
||||
* @value top 从顶部滑入
|
||||
* @value bottom 从底部滑入
|
||||
* @value left 从左侧滑入
|
||||
* @value right 从右侧滑入
|
||||
* @value center 居中显示(默认)
|
||||
* @property {boolean} preventScrollThrough 阻止滚动穿透(默认:true)
|
||||
* @property {boolean} overlay 显示遮罩层(默认:true)
|
||||
* @property {string} transitionName 自定义动画名称(配合transition使用)
|
||||
* @property {string|number|Array} radius 圆角 可以是字符,数值,数组
|
||||
* @property {boolean} visible 控制显示/隐藏(支持v-model)
|
||||
* @property {number} zIndex 组件层级(默认:999)
|
||||
* @property {number} duration 动画时长(单位ms,默认:300)
|
||||
* @property {string} bgColor 内容区域背景色(默认:#ffffff)
|
||||
* @property {string} closeIcon 关闭图标名称/路径(默认:'close')
|
||||
* @property {string} iconColor 关闭图标颜色(默认:#333)
|
||||
* @property {string} lStyle 自定义内容区样式(支持CSS字符串)
|
||||
* @property {boolean} safeAreaInsetBottom 适配底部安全区域(默认:true)
|
||||
* @property {boolean} safeAreaInsetTop 适配顶部安全区域(默认:true)
|
||||
* @event {Function} close 关闭时触发(返回触发来源:'close-btn' | 'overlay')
|
||||
* @event {Function} change 切换时触发
|
||||
* @event {Function} click-overlay 点击遮罩触发
|
||||
* @event {Function} click-close 点击关闭触发
|
||||
* @event {Function} open 打开触发
|
||||
* @event {Function} opened 打开完成触发
|
||||
* @event {Function} closed 关闭完成触发
|
||||
* @event {Function} before-enter
|
||||
* @event {Function} enter
|
||||
* @event {Function} after-enter
|
||||
* @event {Function} before-leave
|
||||
* @event {Function} leave
|
||||
* @event {Function} after-leave
|
||||
*/
|
||||
import { computed, defineComponent } from '@/uni_modules/lime-shared/vue';
|
||||
import { useTransition, type UseTransitionOptions, type TransitionEmitStatus } from '@/uni_modules/lime-transition';
|
||||
import popupProps from './props'
|
||||
import { convertRadius } from './utils'
|
||||
export default defineComponent({
|
||||
name: 'l-popup',
|
||||
props: popupProps,
|
||||
options: {
|
||||
addGlobalClass: true,
|
||||
virtualHost: true,
|
||||
},
|
||||
externalClasses: ['l-class'],
|
||||
emits: ['change', 'click-overlay', 'click-close', 'open', 'opened','close','closed','update:modelValue', 'input', 'before-enter', 'enter', 'after-enter', 'before-leave', 'leave', 'after-leave'],
|
||||
setup(props, { emit }) {
|
||||
const innerValue = computed({
|
||||
set(value: boolean) {
|
||||
emit('change', value)
|
||||
emit('update:modelValue', value)
|
||||
// #ifdef VUE2
|
||||
emit('input', value)
|
||||
// #endif
|
||||
},
|
||||
get():boolean {
|
||||
return props.visible || props.modelValue || props.value || false
|
||||
}
|
||||
} as WritableComputedOptions<boolean>)
|
||||
|
||||
const {inited, display, classes, finished} = useTransition({
|
||||
defaultName: props.transitionName || 'popup-fade',
|
||||
appear: innerValue.value,
|
||||
emits: (name:TransitionEmitStatus) => {
|
||||
if(name == 'before-enter') {
|
||||
emit('open')
|
||||
} else if(name == 'after-enter') {
|
||||
emit('opened')
|
||||
} else if(name == 'before-leave') {
|
||||
emit('close')
|
||||
} else if(name == 'after-leave') {
|
||||
emit('closed')
|
||||
}
|
||||
emit(name)
|
||||
},
|
||||
visible: (): boolean => innerValue.value,
|
||||
duration: props.duration,
|
||||
} as UseTransitionOptions)
|
||||
|
||||
const overlayZIndex = computed(():number => props.zIndex > 0? props.zIndex - 1 : 998);
|
||||
|
||||
const rootClass = computed(():string=>{
|
||||
const safe = props.safeAreaInsetTop && props.position == 'top'
|
||||
? 'l-safe-area-top'
|
||||
: props.safeAreaInsetBottom && props.position == 'bottom'
|
||||
? 'l-safe-area-bottom'
|
||||
: ''
|
||||
|
||||
return `l-popup--${props.position} ${safe} ${classes.value}`
|
||||
// return `l-popup--${props.position} ${classes.value}`
|
||||
})
|
||||
|
||||
const styles = computed(() => {
|
||||
const style:Record<string, any> = {}
|
||||
style['transition-duration'] = props.duration + 'ms'
|
||||
if (props.bgColor != null) {
|
||||
style["background"] = props.bgColor!
|
||||
}
|
||||
if (props.zIndex > 0) {
|
||||
style["z-index"] = props.zIndex
|
||||
}
|
||||
if (!display.value) {
|
||||
style["display"] = "none"
|
||||
}
|
||||
|
||||
if(props.radius) {
|
||||
const values = convertRadius(props.radius!)
|
||||
|
||||
style['border-top-left-radius'] = values[0]
|
||||
style['border-top-right-radius'] = values[1]
|
||||
style['border-bottom-right-radius'] = values[2]
|
||||
style['border-bottom-left-radius'] = values[3]
|
||||
}
|
||||
|
||||
return style
|
||||
})
|
||||
|
||||
const handleOverlayClick = () => {
|
||||
if(props.closeOnClickOverlay) {
|
||||
innerValue.value = false
|
||||
emit('click-overlay')
|
||||
}
|
||||
}
|
||||
const handleClose = () => {
|
||||
innerValue.value = false
|
||||
emit('click-close')
|
||||
}
|
||||
|
||||
return {
|
||||
innerValue,
|
||||
inited,
|
||||
display,
|
||||
finished,
|
||||
overlayZIndex,
|
||||
rootClass,
|
||||
styles,
|
||||
handleOverlayClick,
|
||||
handleClose
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "./index-u";
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
// @ts-nocheck
|
||||
export default {
|
||||
/** 是否展示关闭按钮,值为 `true` 显示默认关闭按钮;值为 `false` 则不显示关闭按钮;也可以自定义关闭按钮 */
|
||||
closeable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
/** 点击遮罩层是否关闭 */
|
||||
closeOnClickOverlay: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/** 是否在关闭浮层时销毁浮层 */
|
||||
destroyOnClose: Boolean,
|
||||
/** 浮层出现位置 */
|
||||
position: {
|
||||
type: String,
|
||||
default: 'center',
|
||||
validator(val: string) : boolean {
|
||||
if (!val) return true;
|
||||
return ['top', 'left', 'right', 'bottom', 'center'].includes(val);
|
||||
},
|
||||
},
|
||||
/** 防止滚动穿透 */
|
||||
preventScrollThrough: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
overlayStyle: {
|
||||
type: [String, Object]
|
||||
},
|
||||
/** 是否显示遮罩层 */
|
||||
overlay: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/** 弹出层内容区的动画名,等价于transition组件的name属性 */
|
||||
transitionName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
/** 是否显示浮层 */
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
// vue2
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
/** 组件层级 默认为 999 */
|
||||
zIndex: {
|
||||
type: Number,
|
||||
default: 999
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 300
|
||||
},
|
||||
bgColor: {
|
||||
type: String
|
||||
},
|
||||
iconColor: {
|
||||
type: String
|
||||
},
|
||||
lStyle: {
|
||||
type: String
|
||||
},
|
||||
closeIcon: {
|
||||
type: String,
|
||||
default: 'close'
|
||||
},
|
||||
radius: {
|
||||
type: [String, Number, Array],
|
||||
default: undefined
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
// @ts-nocheck
|
||||
export interface PopupProps {
|
||||
/**
|
||||
* 是否展示关闭按钮,值为 `true` 显示默认关闭按钮;值为 `false` 则不显示关闭按钮;也可以自定义关闭按钮
|
||||
*/
|
||||
closeable : boolean;
|
||||
/**
|
||||
* 点击遮罩层是否关闭
|
||||
*/
|
||||
closeOnClickOverlay : boolean;
|
||||
/**
|
||||
* 是否在关闭浮层时销毁浮层
|
||||
*/
|
||||
destroyOnClose : boolean;
|
||||
/**
|
||||
* 遮罩层的属性,透传至 overlay
|
||||
*/
|
||||
overlayStyle ?: string | UTSJSONObject;
|
||||
// overlayProps ?: {
|
||||
// preventScrollThrough: boolean
|
||||
// zIndex: number
|
||||
// lStyle: string
|
||||
// };
|
||||
/**
|
||||
* 浮层出现位置
|
||||
*/
|
||||
position : 'top' | 'left' | 'right' | 'bottom' | 'center' | '';
|
||||
/**
|
||||
* 防止滚动穿透
|
||||
*/
|
||||
preventScrollThrough : boolean;
|
||||
/**
|
||||
* 是否显示遮罩层
|
||||
*/
|
||||
overlay : boolean;
|
||||
/**
|
||||
* 弹出层内容区的动画名,等价于transition组件的name属性
|
||||
*/
|
||||
transitionName ?: string;
|
||||
/**
|
||||
* 是否显示浮层
|
||||
*/
|
||||
visible ?: boolean;
|
||||
/**
|
||||
* 组件层级,Web 侧样式默认为 999,移动端和小程序样式默认为 999
|
||||
*/
|
||||
zIndex : number;
|
||||
duration : number;
|
||||
bgColor?: string;
|
||||
closeIcon: string;
|
||||
iconColor?: string;
|
||||
lStyle?: string | UTSJSONObject;
|
||||
safeAreaInsetBottom:boolean;
|
||||
safeAreaInsetTop:boolean;
|
||||
|
||||
radius?: string | number | (string|number)[]
|
||||
}
|
||||
|
||||
export type PopupSource = 'close-btn' | 'overlay';
|
||||
@@ -0,0 +1,21 @@
|
||||
import { addUnit } from '@/uni_modules/lime-shared/addUnit'
|
||||
export function convertRadius(radius : any) : string[] {
|
||||
if (Array.isArray(radius)) {
|
||||
const values = radius.map((item) : string|null => addUnit(item))
|
||||
if (values.length == 1) {
|
||||
return [values[0]!, values[0]!, values[0]!, values[0]!]
|
||||
}
|
||||
if (values.length == 2) {
|
||||
return [values[0]!, values[1]!, values[0]!, values[1]!]
|
||||
}
|
||||
if (values.length == 3) {
|
||||
return [values[0]!, values[1]!, values[2]!, values[1]!]
|
||||
}
|
||||
if (values.length == 4) {
|
||||
return [values[0]!, values[1]!, values[2]!, values[3]!]
|
||||
}
|
||||
return ['0', '0', '0', '0']
|
||||
}
|
||||
const value = addUnit(radius) ?? '0'
|
||||
return [value, value, value, value]
|
||||
}
|
||||
112
qiming-mobile/uni_modules/lime-popup/package.json
Normal file
112
qiming-mobile/uni_modules/lime-popup/package.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"id": "lime-popup",
|
||||
"displayName": "lime-popup 弹出层",
|
||||
"version": "0.2.2",
|
||||
"description": "lime-popup 弹出层容器,用于展示弹窗、信息提示等内容,兼容uniapp/uniappx",
|
||||
"keywords": [
|
||||
"lime-popup",
|
||||
"popup",
|
||||
"弹出层",
|
||||
"弹窗"
|
||||
],
|
||||
"repository": "",
|
||||
"engines": {
|
||||
"HBuilderX": "^4.29",
|
||||
"uni-app": "^4.62",
|
||||
"uni-app-x": "^4.75"
|
||||
},
|
||||
"dcloudext": {
|
||||
"type": "component-vue",
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"lime-style",
|
||||
"lime-shared",
|
||||
"lime-overlay",
|
||||
"lime-transition",
|
||||
"lime-icon"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "√",
|
||||
"vue3": "√"
|
||||
},
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"vue": "√",
|
||||
"nvue": "-",
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "22"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√",
|
||||
"alipay": "-",
|
||||
"toutiao": "-",
|
||||
"baidu": "-",
|
||||
"kuaishou": "-",
|
||||
"jd": "-",
|
||||
"harmony": "-",
|
||||
"qq": "-",
|
||||
"lark": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "-",
|
||||
"union": "-"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "22"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
217
qiming-mobile/uni_modules/lime-popup/readme - 副本.md
Normal file
217
qiming-mobile/uni_modules/lime-popup/readme - 副本.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# lime-popup 弹出层
|
||||
弹出层容器,用于展示弹窗、信息提示等内容,支持多种弹出位置和动画效果,兼容uniapp/uniappx。
|
||||
|
||||
> 插件依赖:`lime-style`、`lime-shared`、`lime-overlay`、`lime-transition`、`lime-icon`。
|
||||
> 注意:`lime-svg`为收费插件,若不需要svg功能可删除依赖。
|
||||
|
||||
## 文档链接
|
||||
📚 组件详细文档请访问以下站点:
|
||||
- [弹出层文档 - 站点1](https://limex.qcoon.cn/components/popup.html)
|
||||
- [弹出层文档 - 站点2](https://limeui.netlify.app/components/popup.html)
|
||||
- [弹出层文档 - 站点3](https://limeui.familyzone.top/components/popup.html)
|
||||
|
||||
## 安装方法
|
||||
1. 在uni-app插件市场中搜索并导入`lime-popup`
|
||||
2. 导入后可能需要重新编译项目
|
||||
::: tip 注意🔔
|
||||
本插件依赖的[【lime-svg】](https://ext.dcloud.net.cn/plugin?id=18519)是原生插件,如果购买(收费为6元)则需要自定义基座,才能使用,
|
||||
若不需要删除即可
|
||||
:::
|
||||
|
||||
|
||||
## 基础用法
|
||||
|
||||
### 基本示例
|
||||
通过 `v-model` 控制弹出层是否展示。
|
||||
|
||||
```html
|
||||
<button @click="show = true">展示弹出层</button>
|
||||
<l-popup v-model="show">
|
||||
<view style="padding: 100px;"></view>
|
||||
</l-popup>
|
||||
```
|
||||
|
||||
```js
|
||||
const show = ref(false);
|
||||
```
|
||||
|
||||
### 弹出位置
|
||||
通过 `position` 属性设置弹窗的弹出位置,默认为居中弹出,可以设置为 `top`、`bottom`、`left`、`right`、`center`。
|
||||
|
||||
- 当弹窗从顶部或底部弹出时,默认宽度与屏幕宽度保持一致,弹窗高度取决于内容的高度。
|
||||
- 当弹窗从左侧或右侧弹出时,默认不设置宽度和高度,弹窗的宽高取决于内容的宽高。
|
||||
|
||||
```html
|
||||
<!-- 顶部弹出 -->
|
||||
<l-popup v-model="showTop" position="top">
|
||||
<view style="padding: 100px;"></view>
|
||||
</l-popup>
|
||||
|
||||
<!-- 左边弹出 -->
|
||||
<l-popup v-model="showLeft" position="left">
|
||||
<view style="padding: 100px;"></view>
|
||||
</l-popup>
|
||||
|
||||
<!-- 底部弹出 -->
|
||||
<l-popup v-model="showBottom" position="bottom">
|
||||
<view style="padding: 100px;"></view>
|
||||
</l-popup>
|
||||
|
||||
<!-- 右边弹出 -->
|
||||
<l-popup v-model="showRight" position="right">
|
||||
<view style="padding: 100px;"></view>
|
||||
</l-popup>
|
||||
```
|
||||
|
||||
### 关闭图标
|
||||
设置 `closeable` 属性后,会在弹出层的右上角显示关闭图标,并且可以通过 `close-icon` 属性自定义图标。
|
||||
|
||||
```html
|
||||
<l-popup v-model="show" :closeable="true">
|
||||
<view style="padding: 100px;"></view>
|
||||
</l-popup>
|
||||
```
|
||||
|
||||
### 监听点击事件
|
||||
Popup 支持以下点击事件:
|
||||
- `click-overlay`: 点击遮罩层时触发。
|
||||
- `click-close`: 点击关闭图标时触发。
|
||||
|
||||
```html
|
||||
<button @click="show = true">展示弹出层</button>
|
||||
<l-popup
|
||||
v-model="show"
|
||||
position="bottom"
|
||||
:closeable="true"
|
||||
@click-overlay="onClickOverlay"
|
||||
@click-close="onClickClose"
|
||||
/>
|
||||
```
|
||||
|
||||
```js
|
||||
const onClickOverlay = () => {
|
||||
console.log('click-overlay');
|
||||
};
|
||||
|
||||
const onClickClose = () => {
|
||||
console.log('click-close');
|
||||
};
|
||||
```
|
||||
|
||||
### 监听显示事件
|
||||
当 Popup 被打开或关闭时,会触发以下事件:
|
||||
- `open`: 打开弹出层时立即触发。
|
||||
- `opened`: 打开弹出层且动画结束后触发。
|
||||
- `close`: 关闭弹出层时立即触发。
|
||||
- `closed`: 关闭弹出层且动画结束后触发。
|
||||
|
||||
```html
|
||||
<button @click="show = true">展示弹出层</button>
|
||||
<l-popup
|
||||
v-model="show"
|
||||
position="bottom"
|
||||
@open="handlePopupOpen"
|
||||
@opened="handlePopupOpened"
|
||||
@close="handlePopupClose"
|
||||
@closed="handlePopupClosed"
|
||||
/>
|
||||
```
|
||||
|
||||
```js
|
||||
const show = ref(false);
|
||||
|
||||
const handlePopupOpen = () => {
|
||||
// 处理弹出框打开前的逻辑
|
||||
}
|
||||
|
||||
const handlePopupOpened = () => {
|
||||
// 处理弹出框打开后的逻辑
|
||||
}
|
||||
|
||||
const handlePopupClose = () => {
|
||||
// 处理弹出框关闭前的逻辑
|
||||
}
|
||||
|
||||
const handlePopupClosed = () => {
|
||||
// 处理弹出框关闭后的逻辑
|
||||
}
|
||||
```
|
||||
|
||||
## 快速预览
|
||||
导入插件后,可以直接使用以下标签查看演示效果:
|
||||
|
||||
```html
|
||||
<!-- 代码位于 uni_modules/lime-popup/components/lime-popup -->
|
||||
<lime-popup />
|
||||
```
|
||||
|
||||
## 插件标签说明
|
||||
- `l-popup`: 组件标签,用于实际开发中
|
||||
- `lime-popup`: 演示标签,用于查看示例效果
|
||||
|
||||
## Vue2使用说明
|
||||
本插件使用了`composition-api`,如需在Vue2项目中使用,请按照[官方教程](https://uniapp.dcloud.net.cn/tutorial/vue-composition-api.html)配置。
|
||||
|
||||
关键配置代码(在main.js中添加):
|
||||
```js
|
||||
// vue2
|
||||
import Vue from 'vue'
|
||||
import VueCompositionAPI from '@vue/composition-api'
|
||||
Vue.use(VueCompositionAPI)
|
||||
```
|
||||
|
||||
## API文档
|
||||
|
||||
### Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| --- | --- | --- | --- |
|
||||
| v-model | 是否显示弹出层 | _boolean_ | `false` |
|
||||
| overlay | 是否显示遮罩层 | _boolean_ | `true` |
|
||||
| position | 弹出位置,可选值为 `top` `bottom` `right` `left` `center` | _string_ | `center` |
|
||||
| duration | 动画时长,单位毫秒,设置为 0 可以禁用动画 | _number_ | `300` |
|
||||
| z-index | 将弹窗的 z-index 层级设置为一个固定值 | _number_ | `999` |
|
||||
| preventScrollThrough | 是否锁定背景滚动 | _boolean_ | `true` |
|
||||
| closeOnClickOverlay | 是否在点击遮罩层后关闭 | _boolean_ | `true` |
|
||||
| destroyOnClose | 关闭后是否销毁 | _boolean_ | `false` |
|
||||
| closeable | 是否显示关闭图标 | _boolean_ | `false` |
|
||||
| closeIcon | 关闭图标名称或图片链接,等同于 Icon 组件的 [name 属性](https://ext.dcloud.net.cn/plugin?id=14057) | _string_ | `cross` |
|
||||
| bgColor | 背景色 | _string_ | `-` |
|
||||
| iconColor | 图标色 | _string_ | `-` |
|
||||
| lStyle | 自定义样式 | _string\|object_ | `-` |
|
||||
| radius | 圆角,可以是字符,数值,数组 | _string\|number\|Array\<string\|number\>_ | `-` |
|
||||
|
||||
### Events
|
||||
|
||||
| 事件名 | 说明 | 回调参数 |
|
||||
| --- | --- | --- |
|
||||
| click-overlay | 点击遮罩层时触发 | - |
|
||||
| click-close | 点击关闭图标时触发 | - |
|
||||
| open | 打开弹出层时立即触发 | - |
|
||||
| close | 关闭弹出层时立即触发 | - |
|
||||
| opened | 打开弹出层且动画结束后触发 | - |
|
||||
| closed | 关闭弹出层且动画结束后触发 | - |
|
||||
|
||||
### Slots
|
||||
|
||||
| 名称 | 说明 |
|
||||
| --- | --- |
|
||||
| default | 弹窗内容 |
|
||||
| close-btn | 关闭按钮 |
|
||||
|
||||
## 主题定制
|
||||
|
||||
组件提供了下列 CSS 变量,可用于自定义样式,uvue app无效。
|
||||
|
||||
| 名称 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| --l-popup-bg-color | _#fff_ | 弹出层背景色 |
|
||||
| --l-popup-close-icon-color | _rgba(0,0,0,0.6)_ | 关闭图标颜色 |
|
||||
| --l-popup-border-radius | _$border-radius_ | 弹出层圆角 |
|
||||
|
||||
## 支持与赞赏
|
||||
|
||||
如果你觉得本插件解决了你的问题,可以考虑支持作者:
|
||||
| 支付宝赞助 | 微信赞助 |
|
||||
|------------|------------|
|
||||
|  |  |
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user