chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View 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最大值60000010分钟
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,
};

View File

@@ -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"
}
}

View File

@@ -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 */
}
}

View File

@@ -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 */
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'
}
}

View File

@@ -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';
}

View File

@@ -0,0 +1,3 @@
export function getOffsetWithMinusString(val : string):string {
return val.startsWith('-') ? val.replace('-', '') : `-${val}`
};

View 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": "√"
}
}
}
}
}
}

View File

@@ -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*/
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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,
}

View File

@@ -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
}
}

View 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": "√"
}
}
}
}
}
}

View 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 | - | -
## 支持与赞赏
如果你觉得本插件解决了你的问题,可以考虑支持作者:
| 支付宝赞助 | 微信赞助 |
|------------|------------|
| ![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/alipay.png) | ![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/wpay.png) |

View File

@@ -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 组件

View 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 状态正确,数据绑定准确

View File

@@ -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

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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;
}

View File

@@ -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)
})
}
}

View File

@@ -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
})
}

View 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": "√"
}
}
}
}
}
}

View 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_ | - |
## 打赏
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/alipay.png)
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/wpay.png)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,
},
}

View File

@@ -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

View File

@@ -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';
});
}

View File

@@ -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;
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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]
}
}

View File

@@ -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

View 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": "√"
}
}
}
}
}
}

View 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_ | - |
## 打赏
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/alipay.png)
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/wpay.png)

View 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);
}

View 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;
}

View 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'],
]);

View 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
}

View 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;
}

View 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}`;
}

View File

@@ -0,0 +1,5 @@
// @ts-nocheck
export * from './common/color'
export * from './common/generate'
export * from './utssdk/interface'
// export {LGenerateOptions} from './utssdk/interface'

View 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;
}

View 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": "√"
}
}
}
}
}
}

View 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']
```
## 打赏
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/alipay.png)
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/wpay.png)

View File

@@ -0,0 +1,3 @@
{
"minSdkVersion": "21"
}

View File

@@ -0,0 +1,2 @@
export * from '../../common/color'
export * from '../../common/generate'

View File

@@ -0,0 +1,3 @@
{
"deploymentTarget": "9"
}

View File

@@ -0,0 +1,2 @@
export * from '../../common/color'
export * from '../../common/generate'

View File

@@ -0,0 +1,3 @@
export * from '../common/color'
export * from '../common/generate'
export * from './interface'

View 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;
}

View 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] ?? "";
}
}

View File

@@ -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 */

View File

@@ -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

View File

@@ -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 */
}
}

View File

@@ -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>

View 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>

View File

@@ -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],
}

View File

@@ -0,0 +1,4 @@
export type IconCollection = {
has: boolean,
icons: Map<string, any|null>
}

View File

@@ -0,0 +1,2 @@
const { generate } = require('./utils/generate.js');
generate()

Binary file not shown.

View 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

View 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'
]
}

View 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": "√"
}
}
}
}
}
}

View 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" />
```
![](https://img-cdn-tx.dcloud.net.cn/stream/plugin_screens/263cfd20-39e6-11ee-b4f0-9bc760224a38_1.png?1735701321)
![](https://img-cdn-tx.dcloud.net.cn/stream/plugin_screens/263cfd20-39e6-11ee-b4f0-9bc760224a38_2.png?1735701324)
### 使用图标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 | 点击 | |
## 打赏
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/alipay.png)
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/wpay.png)

File diff suppressed because it is too large Load Diff

View 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
}

View 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,
};

View 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;

View File

@@ -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 */

View File

@@ -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 */

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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;
}

View File

@@ -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
}

View 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
}

View 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
}

View 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": "√"
}
}
}
}
}
}

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,
}

View File

@@ -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;
}

View 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": "√"
}
}
}
}
}
}

View 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)_ | - |
## 打赏
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/alipay.png)
![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/wpay.png)

View File

@@ -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;
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
}
};

View File

@@ -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';

View File

@@ -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]
}

View 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": "√"
}
}
}
}
}
}

View 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_ | 弹出层圆角 |
## 支持与赞赏
如果你觉得本插件解决了你的问题,可以考虑支持作者:
| 支付宝赞助 | 微信赞助 |
|------------|------------|
| ![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/alipay.png) | ![](https://testingcf.jsdelivr.net/gh/liangei/image@1.9/wpay.png) |

Some files were not shown because too many files have changed in this diff Show More