Jelajahi Sumber

添加圆形进度条

icssoa 8 bulan lalu
induk
melakukan
0c4fc52b9a

+ 1 - 1
.cool/eps.ts

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

TEMPAT SAMPAH
.cool/icons/remixicon.zip


File diff ditekan karena terlalu besar
+ 0 - 0
.cool/remixicon/RemixIcon_Collection_2507261544.remixicon


+ 1 - 0
cool/ctx/index.ts

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

+ 14 - 1
cool/theme/index.ts

@@ -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";
@@ -41,6 +41,19 @@ export function getStyle(key: string): string | null {
 }
 
 /**
+ * 获取颜色
+ * @param name 颜色名称
+ * @returns 颜色值
+ */
+export const getColor = (name: string) => {
+	if (ctx.color == null) {
+		return "";
+	}
+
+	return ctx.color[name] as string;
+};
+
+/**
  * 获取 uniapp 主题配置
  */
 export function getConfig(key: string): string {

File diff ditekan karena terlalu besar
+ 0 - 0
icons/remixicon/index.scss


+ 3 - 1
icons/remixicon/index.ts

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

+ 2 - 2
package.json

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

+ 6 - 0
pages.json

@@ -310,6 +310,12 @@
 					}
 				},
 				{
+					"path": "status/progress-circle",
+					"style": {
+						"navigationBarTitleText": "ProgressCircle 圆形进度条"
+					}
+				},
+				{
 					"path": "status/skeleton",
 					"style": {
 						"navigationBarTitleText": "Skeleton 骨架图"

+ 70 - 0
pages/demo/status/progress-circle.uvue

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

+ 2 - 2
pages/demo/status/progress.uvue

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

+ 11 - 6
pages/index/home.uvue

@@ -293,11 +293,21 @@ const data = computed<Item[]>(() => {
 					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",
 					path: "/pages/demo/status/skeleton"
@@ -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"
 				}
 			]
 		},

+ 5 - 5
pnpm-lock.yaml

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

+ 6 - 0
uni_modules/cool-ui/components/cl-button/cl-button.uvue

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

+ 1 - 0
uni_modules/cool-ui/components/cl-popup/cl-popup.uvue

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

+ 262 - 0
uni_modules/cool-ui/components/cl-progress-circle/cl-progress-circle.uvue

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

+ 21 - 0
uni_modules/cool-ui/components/cl-progress-circle/props.ts

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

+ 35 - 5
uni_modules/cool-ui/components/cl-progress/cl-progress.uvue

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

+ 10 - 0
uni_modules/cool-ui/components/cl-progress/props.ts

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

+ 3 - 3
uni_modules/cool-ui/components/cl-radio/cl-radio.uvue

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

+ 1 - 1
uni_modules/cool-ui/components/cl-radio/props.ts

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

+ 3 - 1
uni_modules/cool-ui/index.d.ts

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

+ 4 - 0
uni_modules/cool-ui/types/component.d.ts

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

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini