添加圆形进度条

This commit is contained in:
icssoa
2025-07-26 17:42:36 +08:00
parent 70d993b14d
commit 0c4fc52b9a
23 changed files with 463 additions and 30 deletions

View File

@@ -325,7 +325,7 @@ export type UserLogin = {
mp(data?: any): Promise<any>;
};
export type DictKey = "brand" | "occupation" | "refund" | "ccc";
export type DictKey = "brand" | "occupation";
export type UserInterface = {
address: UserAddress;

Binary file not shown.

View File

@@ -45,6 +45,7 @@ export type Ctx = {
tabBar: TabBar;
subPackages: SubPackage[];
SAFE_CHAR_MAP_LOCALE: string[][];
color: UTSJSONObject;
};
// 初始化 ctx 对象,不可修改!!

View File

@@ -1,7 +1,7 @@
import { computed, ref } from "vue";
import uniTheme from "@/theme.json";
import { router } from "../router";
import { config } from "@/config";
import { ctx } from "../ctx";
// 主题类型定义,仅支持 light 和 dark
type Theme = "light" | "dark";
@@ -40,6 +40,19 @@ export function getStyle(key: string): string | null {
return null;
}
/**
* 获取颜色
* @param name 颜色名称
* @returns 颜色值
*/
export const getColor = (name: string) => {
if (ctx.color == null) {
return "";
}
return ctx.color[name] as string;
};
/**
* 获取 uniapp 主题配置
*/

File diff suppressed because one or more lines are too long

View File

@@ -693,5 +693,7 @@ export const remixicon = {
"user-2-fill": "f253",
"user-2-line": "f254",
"shield-user-line": "f10c",
"shield-user-fill": "f10b"
"shield-user-fill": "f10b",
"circle-line": "f3c2",
"circle-fill": "f3c1"
};

View File

@@ -1,6 +1,6 @@
{
"name": "cool-unix",
"version": "8.0.0",
"version": "8.0.2",
"license": "MIT",
"scripts": {
"build-ui": "node ./uni_modules/cool-ui/scripts/generate-types.js",
@@ -14,7 +14,7 @@
"@babel/parser": "^7.27.5",
"@babel/types": "^7.27.6",
"@cool-vue/ai": "^1.1.4",
"@cool-vue/vite-plugin": "^8.2.3",
"@cool-vue/vite-plugin": "^8.2.5",
"@dcloudio/types": "^3.4.16",
"@types/node": "^24.0.15",
"@vue/compiler-sfc": "^3.5.16",

View File

@@ -309,6 +309,12 @@
"navigationBarTitleText": "Progress 进度条"
}
},
{
"path": "status/progress-circle",
"style": {
"navigationBarTitleText": "ProgressCircle 圆形进度条"
}
},
{
"path": "status/skeleton",
"style": {

View File

@@ -0,0 +1,70 @@
<template>
<cl-page>
<view class="p-3">
<demo-item :label="t('自定义')">
<cl-progress-circle
:value="value"
:color="isColor ? 'red' : null"
:un-color="isColor ? '#f7bfbf' : null"
:size="isSize ? 80 : 120"
:show-text="isText"
:duration="isDuration ? 200 : 500"
></cl-progress-circle>
<cl-list
border
:pt="{
className: 'mt-5'
}"
>
<cl-list-item label="改个颜色">
<cl-switch v-model="isColor"></cl-switch>
</cl-list-item>
<cl-list-item label="显示文本">
<cl-switch v-model="isText"></cl-switch>
</cl-list-item>
<cl-list-item label="快一些">
<cl-switch v-model="isDuration"></cl-switch>
</cl-list-item>
<cl-list-item label="显示文本">
<cl-button type="light" size="small" icon="add-line" @tap="add"></cl-button>
<cl-button
type="light"
size="small"
icon="subtract-line"
@tap="sub"
></cl-button>
</cl-list-item>
</cl-list>
</demo-item>
</view>
</cl-page>
</template>
<script lang="ts" setup>
import { ref } from "vue";
import { t } from "@/locale";
import DemoItem from "../components/item.uvue";
import { ctx } from "@/cool";
const isSize = ref(false);
const isText = ref(true);
const isColor = ref(false);
const isDuration = ref(false);
const value = ref(70);
function add() {
if (value.value < 100) {
value.value += 10;
}
}
function sub() {
if (value.value > 0) {
value.value -= 10;
}
}
</script>

View File

@@ -13,8 +13,8 @@
<cl-progress :value="30" :stroke-width="20"></cl-progress>
</demo-item>
<demo-item :label="t('显示文本')">
<cl-progress :value="75" show-text></cl-progress>
<demo-item :label="t('显示文本')">
<cl-progress :value="75" :show-text="false"></cl-progress>
</demo-item>
</view>
</cl-page>

View File

@@ -292,11 +292,21 @@ const data = computed<Item[]>(() => {
icon: "timer-line",
path: "/pages/demo/status/countdown"
},
{
label: t("数字滚动"),
icon: "arrow-up-box-line",
path: "/pages/demo/status/rolling-number"
},
{
label: t("进度条"),
icon: "percent-line",
icon: "subtract-line",
path: "/pages/demo/status/progress"
},
{
label: t("圆形进度条"),
icon: "circle-line",
path: "/pages/demo/status/progress-circle"
},
{
label: t("骨架图"),
icon: "shadow-line",
@@ -306,11 +316,6 @@ const data = computed<Item[]>(() => {
label: t("加载更多"),
icon: "loader-4-line",
path: "/pages/demo/status/loadmore"
},
{
label: t("数字滚动"),
icon: "arrow-up-box-line",
path: "/pages/demo/status/rolling-number"
}
]
},

10
pnpm-lock.yaml generated
View File

@@ -25,8 +25,8 @@ importers:
specifier: ^1.1.4
version: 1.1.4
'@cool-vue/vite-plugin':
specifier: ^8.2.3
version: 8.2.3
specifier: ^8.2.5
version: 8.2.5
'@dcloudio/types':
specifier: ^3.4.16
version: 3.4.16
@@ -85,8 +85,8 @@ packages:
resolution: {integrity: sha512-1OKM1PnxMYzpzSTC7RjqnEcpwWKhAAns5/YJ5yi3RJY5vRRV6ZA0MbeMYvgNLLkSg5qEsUrC0lanKyX26B0R6g==}
hasBin: true
'@cool-vue/vite-plugin@8.2.3':
resolution: {integrity: sha512-jPI24xSXXWltHkxTRQzAn09qpnhfSBCOqWZtPNRCQ8fZX4BnEgw4FMXSQanMdVmfTMQ1Gbljb2nJCTqbDyxRWg==}
'@cool-vue/vite-plugin@8.2.5':
resolution: {integrity: sha512-K6HSQTu43G/VjgsUTaPJd/i8mctq961RhMtxGOIWqtxjbTwm1lPjfwn1cTZmOZtcpPvVpfLYJP9fFJ7kvIDRug==}
'@dcloudio/types@3.4.16':
resolution: {integrity: sha512-gJIr1OWtePTDDdjtp8Kh72S/ZGLunoSfHiUvRtXhBmAFNkDWuAKFO90hv62k3GYN/st04xUBQNtBfvhu/YHjww==}
@@ -1370,7 +1370,7 @@ snapshots:
transitivePeerDependencies:
- debug
'@cool-vue/vite-plugin@8.2.3':
'@cool-vue/vite-plugin@8.2.5':
dependencies:
'@vue/compiler-sfc': 3.5.17
axios: 1.10.0

View File

@@ -612,6 +612,12 @@ function onTouchCancel() {
&.cl-button--hover {
@apply bg-surface-100;
}
&.is-dark {
&.cl-button--hover {
@apply bg-surface-700;
}
}
}
&--dark {

View File

@@ -80,6 +80,7 @@
'absolute right-[24rpx] !text-surface-400 dark:!text-surface-50'
}"
@tap="close"
@touchmove.stop
v-if="isOpen && showClose"
></cl-icon>
</view>

View File

@@ -0,0 +1,262 @@
<template>
<view class="cl-progress-circle" :class="[pt.className]">
<canvas
class="cl-progress-circle__canvas"
:id="canvasId"
:style="{
height: `${props.size}px`,
width: `${props.size}px`
}"
></canvas>
<slot name="text">
<cl-text
:value="`${value}${unit}`"
:pt="{
className: parseClass(['absolute', pt.text?.className])
}"
v-if="showText"
></cl-text>
</slot>
</view>
</template>
<script lang="ts" setup>
import { getColor, isDark, parseClass, parsePt, uuid } from "@/cool";
import { computed, getCurrentInstance, onMounted, ref, watch, type PropType } from "vue";
import type { PassThroughProps } from "../../types";
defineOptions({
name: "cl-progress-circle"
});
const props = defineProps({
pt: {
type: Object,
default: () => ({})
},
// 数值 (0-100)
value: {
type: Number,
default: 0
},
// 圆形大小
size: {
type: Number,
default: 120
},
// 线条宽度
strokeWidth: {
type: Number,
default: 8
},
// 进度条颜色
color: {
type: String as PropType<string | null>,
default: null
},
// 底色
unColor: {
type: String as PropType<string | null>,
default: null
},
// 是否显示文本
showText: {
type: Boolean,
default: true
},
// 单位
unit: {
type: String,
default: "%"
},
// 起始角度 (弧度)
startAngle: {
type: Number,
default: -Math.PI / 2
},
// 是否顺时针
clockwise: {
type: Boolean,
default: true
},
// 动画时长
duration: {
type: Number,
default: 500
}
});
const { proxy } = getCurrentInstance()!;
// 透传样式类型定义
type PassThrough = {
className?: string;
text?: PassThroughProps;
};
// 解析透传样式配置
const pt = computed(() => parsePt<PassThrough>(props.pt));
// canvas组件上下文
let canvasCtx: CanvasContext | null = null;
// 绘图上下文
let drawCtx: CanvasRenderingContext2D | null = null;
// 生成唯一的canvas ID
const canvasId = `cl-progress-circle__${uuid()}`;
// 当前显示值
const value = ref(0);
// 绘制圆形进度条
function drawProgress() {
if (drawCtx == null) return;
const centerX = props.size / 2;
const centerY = props.size / 2;
const radius = (props.size - props.strokeWidth) / 2;
// 清除画布
// #ifdef APP
drawCtx!.reset();
// #endif
// #ifndef APP
drawCtx!.clearRect(0, 0, props.size, props.size);
// #endif
// 获取设备像素比
const dpr = uni.getDeviceInfo().devicePixelRatio ?? 1;
// #ifndef H5
// 设置缩放比例
drawCtx!.scale(dpr, dpr);
// #endif
// 保存当前状态
drawCtx!.save();
// 优化的圆环绘制
const drawCircle = (startAngle: number, endAngle: number, color: string) => {
if (drawCtx == null) return;
drawCtx!.beginPath();
drawCtx!.arc(centerX, centerY, radius, startAngle, endAngle, false);
drawCtx!.strokeStyle = color;
drawCtx!.lineWidth = props.strokeWidth;
drawCtx!.lineCap = "round";
drawCtx!.lineJoin = "round";
drawCtx!.stroke();
};
// 绘制底色圆环
drawCircle(
0,
2 * Math.PI,
props.unColor ?? (isDark.value ? getColor("surface-700") : getColor("surface-200"))
);
// 绘制进度圆弧
if (value.value > 0) {
const progress = Math.max(0, Math.min(100, value.value)) / 100;
const endAngle = props.startAngle + (props.clockwise ? 1 : -1) * 2 * Math.PI * progress;
drawCircle(props.startAngle, endAngle, props.color ?? getColor("primary-500"));
}
}
// 动画更新数值
function animate(targetValue: number) {
const startValue = value.value;
const startTime = Date.now();
function update() {
// 获取当前时间
const currentTime = Date.now();
// 计算动画经过的时间
const elapsed = currentTime - startTime;
// 计算动画进度
const progress = Math.min(elapsed / props.duration, 1);
// 缓动函数
const easedProgress = 1 - Math.pow(1 - progress, 3);
// 计算当前值
value.value = Math.round(startValue + (targetValue - startValue) * easedProgress);
// 绘制进度条
drawProgress();
if (progress < 1) {
if (canvasCtx != null) {
// @ts-ignore
canvasCtx!.requestAnimationFrame(() => {
update();
});
}
}
}
update();
}
// 初始化画布
function initCanvas() {
uni.createCanvasContextAsync({
id: canvasId,
component: proxy,
success: (context: CanvasContext) => {
// 设置canvas上下文
canvasCtx = context;
// 获取绘图上下文
drawCtx = context.getContext("2d")!;
// 设置宽高
drawCtx!.canvas.width = props.size;
drawCtx!.canvas.height = props.size;
// 优化渲染质量
drawCtx!.textBaseline = "middle";
drawCtx!.textAlign = "center";
drawCtx!.miterLimit = 10;
// 开始动画
animate(props.value);
}
});
}
onMounted(() => {
initCanvas();
// 监听value变化
watch(
computed(() => props.value),
(val: number) => {
animate(Math.max(0, Math.min(100, val)));
},
{
immediate: true
}
);
watch(
computed(() => [props.color, props.unColor, isDark.value]),
() => {
drawProgress();
}
);
});
defineExpose({
animate
});
</script>
<style lang="scss" scoped>
.cl-progress-circle {
@apply flex flex-col items-center justify-center relative;
}
</style>

View File

@@ -0,0 +1,21 @@
import type { PassThroughProps } from "../../types";
export type ClProgressCirclePassThrough = {
className?: string;
text?: PassThroughProps;
};
export type ClProgressCircleProps = {
className?: string;
pt?: ClProgressCirclePassThrough;
value?: number;
size?: number;
strokeWidth?: number;
color?: string | any;
unColor?: string | any;
showText?: boolean;
unit?: string;
startAngle?: number;
clockwise?: boolean;
duration?: number;
};

View File

@@ -1,14 +1,27 @@
<template>
<view class="cl-progress">
<view class="cl-progress__outer" :style="outerStyle">
<view class="cl-progress__inner" :style="innerStyle"></view>
<view class="cl-progress" :class="[pt.className]">
<view
class="cl-progress__outer"
:class="[
{
'!bg-surface-700': isDark && props.unColor == null
},
pt.outer?.className
]"
:style="outerStyle"
>
<view
class="cl-progress__inner"
:class="[pt.inner?.className]"
:style="innerStyle"
></view>
</view>
<slot name="text">
<cl-rolling-number
:model-value="value"
:pt="{
className: 'w-[100rpx] text-center'
className: parseClass(['w-[100rpx] text-center', pt.text?.className])
}"
unit="%"
v-if="showText"
@@ -20,13 +33,19 @@
<script lang="ts" setup>
import { computed, getCurrentInstance, onMounted, ref, watch } from "vue";
import { parseRpx } from "@/cool";
import { isDark, parseClass, parsePt, parseRpx } from "@/cool";
import type { PassThroughProps } from "../../types";
defineOptions({
name: "cl-progress"
});
const props = defineProps({
// 透传样式配置
pt: {
type: Object,
default: () => ({})
},
// 数值
value: {
type: Number,
@@ -56,6 +75,17 @@ const props = defineProps({
const { proxy } = getCurrentInstance()!;
// 透传样式类型定义
type PassThrough = {
className?: string;
outer?: PassThroughProps;
inner?: PassThroughProps;
text?: PassThroughProps;
};
// 解析透传样式配置
const pt = computed(() => parsePt<PassThrough>(props.pt));
// 当前值
const value = ref(0);

View File

@@ -1,5 +1,15 @@
import type { PassThroughProps } from "../../types";
export type ClProgressPassThrough = {
className?: string;
outer?: PassThroughProps;
inner?: PassThroughProps;
text?: PassThroughProps;
};
export type ClProgressProps = {
className?: string;
pt?: ClProgressPassThrough;
value?: number;
strokeWidth?: number;
showText?: boolean;

View File

@@ -54,14 +54,14 @@ defineOptions({
// 定义组件属性
const props = defineProps({
modelValue: {
type: null
},
// 透传样式配置
pt: {
type: Object,
default: () => ({})
},
modelValue: {
type: null
},
// 选中时的图标
activeIcon: {
type: String,

View File

@@ -9,8 +9,8 @@ export type ClRadioPassThrough = {
export type ClRadioProps = {
className?: string;
modelValue: any;
pt?: ClRadioPassThrough;
modelValue: any;
activeIcon?: string;
inactiveIcon?: string;
showIcon?: boolean;

View File

@@ -36,7 +36,8 @@ import type { ClPageUiProps } from "./components/cl-page-ui/props";
import type { ClPaginationProps, ClPaginationPassThrough } from "./components/cl-pagination/props";
import type { ClSelectPickerViewProps } from "./components/cl-select-picker-view/props";
import type { ClPopupProps, ClPopupPassThrough, ClPopupHeaderPassThrough } from "./components/cl-popup/props";
import type { ClProgressProps } from "./components/cl-progress/props";
import type { ClProgressProps, ClProgressPassThrough } from "./components/cl-progress/props";
import type { ClProgressCircleProps, ClProgressCirclePassThrough } from "./components/cl-progress-circle/props";
import type { ClQrcodeProps } from "./components/cl-qrcode/props";
import type { ClRadioProps, ClRadioPassThrough } from "./components/cl-radio/props";
import type { ClRateProps, ClRatePassThrough } from "./components/cl-rate/props";
@@ -101,6 +102,7 @@ declare module "vue" {
"cl-select-picker-view": (typeof import('./components/cl-select-picker-view/cl-select-picker-view.uvue')['default']) & import('vue').DefineComponent<ClSelectPickerViewProps>;
"cl-popup": (typeof import('./components/cl-popup/cl-popup.uvue')['default']) & import('vue').DefineComponent<ClPopupProps>;
"cl-progress": (typeof import('./components/cl-progress/cl-progress.uvue')['default']) & import('vue').DefineComponent<ClProgressProps>;
"cl-progress-circle": (typeof import('./components/cl-progress-circle/cl-progress-circle.uvue')['default']) & import('vue').DefineComponent<ClProgressCircleProps>;
"cl-qrcode": (typeof import('./components/cl-qrcode/cl-qrcode.uvue')['default']) & import('vue').DefineComponent<ClQrcodeProps>;
"cl-radio": (typeof import('./components/cl-radio/cl-radio.uvue')['default']) & import('vue').DefineComponent<ClRadioProps>;
"cl-rate": (typeof import('./components/cl-rate/cl-rate.uvue')['default']) & import('vue').DefineComponent<ClRateProps>;

View File

@@ -138,3 +138,7 @@ declare type ClWaterfallComponentPublicInstance = {
declare type ClQrcodeComponentPublicInstance = {
toPng: () => Promise<string>;
};
declare type ClProgressCircleComponentPublicInstance = {
animate: (value: number) => void;
};