【修复】条件节点前fromNodeId传值问题

【修复】部署参数默认错误问题
【测设】部分项目代码结构
【同步】前端项目代码
This commit is contained in:
chudong
2025-05-09 18:44:33 +08:00
parent 6e2fe8cf52
commit d147bc7a82
237 changed files with 8705 additions and 8741 deletions

View File

@@ -0,0 +1,711 @@
import { defineComponent, onMounted, ref, watch, PropType, onUnmounted } from 'vue'
/**
* 渐变色停止点接口定义
* @interface ColorStop
* @property {number} offset - 渐变停止点位置0-1
* @property {string} color - 渐变颜色值
*/
interface ColorStop {
offset: number
color: string
}
/**
* 文字位置类型
*/
type TextPosition = 'front' | 'back' | 'follow'
/**
* 自定义插槽参数接口
*/
interface SlotProps {
percent: number
color: string
}
/**
* 环形进度条组件
* 支持自定义大小、颜色、进度和渐变色
* @component CircleProgress
* @example
* ```vue
* <!-- 基础用法 -->
* <CircleProgress :percent="75" />
*
* <!-- 横向进度条 -->
* <CircleProgress type="horizontal" :percent="75" />
*
* <!-- 使用渐变色 -->
* <CircleProgress
* :percent="75"
* :progress-color="[
* { offset: 0, color: '#ff0000' },
* { offset: 0.5, color: '#00ff00' },
* { offset: 1, color: '#0000ff' }
* ]"
* />
*
* <!-- 自定义样式 -->
* <CircleProgress
* :percent="75"
* :size="200"
* :stroke-width="10"
* :text-color-follow-progress="true"
* />
* ```
*/
export const CircleProgress = defineComponent({
name: 'CircleProgress',
props: {
/**
* 进度条类型
* @default 'circle'
* @type {'circle' | 'horizontal'}
*/
type: {
type: String as PropType<'circle' | 'horizontal'>,
default: 'circle',
},
/**
* 进度值,范围 0-100
* @default 0
* @type {number}
*/
percent: {
type: Number,
default: 0,
validator: (value: number) => value >= 0 && value <= 100,
},
/**
* 组件大小,单位像素
* @default 300
* @type {number}
*/
size: {
type: Number,
default: 300,
},
/**
* 进度文字大小
* @default 30
* @type {number}
*/
textSize: {
type: Number,
default: 30,
},
/**
* 进度条宽度
* @default 20
* @type {number}
*/
strokeWidth: {
type: Number,
default: 10,
},
/**
* 轨道颜色
* @default '#e5f1fa'
* @type {string}
*/
trackColor: {
type: String,
default: '#e5f1fa',
},
/**
* 进度条颜色,支持纯色或渐变色数组
* @default '#2ba0fb'
* @type {string | ColorStop[]}
*/
progressColor: {
type: [String, Array] as PropType<string | ColorStop[]>,
default: '#2ba0fb',
},
/**
* 进度文字颜色
* @default '#333'
* @type {string}
*/
textColor: {
type: String,
default: '#333',
},
/**
* 文字颜色是否跟随进度条颜色变化
* @default false
* @type {boolean}
*/
textColorFollowProgress: {
type: Boolean,
default: false,
},
/**
* 起始角度(弧度)
* @default -Math.PI / 2
* @type {number}
*/
startAngle: {
type: Number,
default: -Math.PI / 2, // 默认从12点钟方向开始
},
/**
* 是否顺时针旋转
* @default true
* @type {boolean}
*/
clockwise: {
type: Boolean,
default: true,
},
/**
* 动画过渡速度0-1之间值越大动画越快
* @default 0.1
* @type {number}
*/
animationSpeed: {
type: Number,
default: 0.1,
validator: (value: number) => value > 0 && value <= 1,
},
/**
* 组件宽度,单位像素(仅横向进度条生效)
* @default 300
* @type {number}
*/
width: {
type: Number,
default: 300,
},
/**
* 组件高度,单位像素(仅横向进度条生效)
* @default 20
* @type {number}
*/
height: {
type: Number,
default: 20,
},
/**
* 是否启用圆角
* @default true
* @type {boolean}
*/
rounded: {
type: Boolean,
default: true,
},
/**
* 进度条颜色是否跟随进度变化
* @default false
* @type {boolean}
*/
colorFollowProgress: {
type: Boolean,
default: false,
},
/**
* 横向进度条文字位置
* @default 'follow'
* @type {'front' | 'back' | 'follow'}
*/
textPosition: {
type: String as PropType<TextPosition>,
default: 'follow',
},
/**
* 自定义进度文字插槽
* @type {(props: SlotProps) => any}
*/
progressText: {
type: Function as PropType<(props: SlotProps) => JSX.Element>,
default: undefined,
},
},
setup(props) {
const canvasRef = ref<HTMLCanvasElement | null>(null) // 画布引用
const currentNum = ref(0) // 当前进度
const targetNum = ref(0) // 目标进度
const animationFrame = ref<number | null>(null) // 动画帧
/**
* 计算圆角导致的进度偏差值
* @returns {number} 角度偏差值(弧度)
* @description
* 1. 计算整个圆的长度,以进度线段中心作为圆的长度
* 2. 获取进度线段线帽的半径(线段宽度的一半)
* 3. 计算线帽旋转需要的角度偏差
* 4. 如果未启用圆角或未使用渐变色则返回0
* 5. 当圆弧长度大于圆的长度时,根据进度值计算额外偏移
*/
const roundDeviation = (): number => {
if (props.type === 'horizontal') return 0
// 如果未启用圆角或未使用渐变色返回0
if (!props.rounded || !Array.isArray(props.progressColor) || props.percent === 100) {
return 0
}
// 计算圆的半径(以进度线段中心为基准)
const radius = (props.size - props.strokeWidth) / 2
// 获取线帽半径(线段宽度的一半)
const capRadius = props.strokeWidth / 2
// 计算线帽旋转需要的角度偏差
// 使用弧长公式:弧长 = 半径 * 角度
// 因此:角度 = 弧长 / 半径
// 这里使用线帽半径作为弧长,因为线帽旋转时走过的距离等于线帽半径
const deviation = capRadius / radius
// 计算当前圆的长度
// const circleLength = 2 * Math.PI * radius
// const progressLength = circleLength * (props.percent / 100) + props.strokeWidth
// 如果当前圆弧的长度大于圆的长度且进度小于100%,则增加偏差
// if (progressLength > circleLength && props.percent <= 100) {
// deviation = deviation + (progressLength - circleLength) / radius
// }
return deviation
}
/**
* 创建渐变对象
* @param ctx - Canvas上下文
* @param centerX - 圆心X坐标
* @param centerY - 圆心Y坐标
* @param colorStops - 渐变色停止点数组
* @returns {CanvasGradient} 锥形渐变对象
* @description
* 创建一个锥形渐变,并添加颜色停止点。
* 确保渐变的起点和终点颜色正确,使渐变效果更加平滑。
*/
const createGradient = (
ctx: CanvasRenderingContext2D,
centerX: number,
centerY: number,
colorStops: ColorStop[],
): CanvasGradient => {
const deviation = roundDeviation()
console.log(deviation)
// 创建锥形渐变,起始角度为-90度12点钟方向增加一个偏差值解决进度显示不完整的问题同时排除显卡
const gradient = ctx.createConicGradient(props.startAngle - deviation, centerX, centerY)
// 添加颜色停止点
colorStops.forEach((stop) => {
gradient.addColorStop(stop.offset, stop.color)
})
// 确保渐变闭合
const firstStop = colorStops[0]
// 获取最后一个颜色停止点
const lastStop = colorStops[colorStops.length - 1]
console.log(firstStop, lastStop)
// 如果第一个颜色停止点不是0则添加一个0偏移的颜色停止点
if (firstStop && firstStop.offset !== 0) {
gradient.addColorStop(0, firstStop.color)
}
// 如果最后一个颜色停止点不是1则添加一个1偏移的颜色停止点
if (lastStop && lastStop.offset !== 1) {
gradient.addColorStop(1, lastStop.color)
}
return gradient
}
/**
* 获取当前进度的颜色或渐变
* @param ctx - Canvas上下文
* @param centerX - 圆心X坐标
* @param centerY - 圆心Y坐标
* @returns {string | CanvasGradient} 颜色值或渐变对象
* @description
* 根据progressColor属性的类型返回对应的颜色或渐变对象。
* 如果是字符串则返回纯色,如果是数组则创建渐变。
*/
const getProgressColor = (
ctx: CanvasRenderingContext2D,
centerX: number,
centerY: number,
): string | CanvasGradient => {
if (!Array.isArray(props.progressColor)) {
return props.progressColor
}
// 如果是横向进度条,使用线性渐变
if (props.type === 'horizontal') {
const gradient = ctx.createLinearGradient(0, centerY, props.width, centerY)
props.progressColor.forEach((stop) => {
gradient.addColorStop(stop.offset, stop.color)
})
return gradient
}
// 圆形进度条使用锥形渐变
return createGradient(ctx, centerX, centerY, props.progressColor)
}
/**
* 获取当前进度的颜色
* @param progress - 当前进度值0-100
* @returns {string} 当前进度的颜色
*/
const getCurrentProgressColor = (progress: number): string => {
// 如果不是渐变色数组,直接返回颜色
if (!Array.isArray(props.progressColor)) {
return typeof props.progressColor === 'string' ? props.progressColor : props.textColor
}
// 如果颜色停止点为空,返回默认颜色
const colorStops = props.progressColor as ColorStop[]
if (colorStops.length === 0) {
return props.textColor
}
// 如果进度达到100%,返回最后一个颜色
if (progress >= 100) {
const lastStop = colorStops[colorStops.length - 1]
return lastStop?.color || props.textColor
}
// 将进度转换为0-1之间的值
const normalizedProgress = progress / 100
// 找到当前进度所在的两个颜色停止点
for (let i = 0; i < colorStops.length - 1; i++) {
const currentStop = colorStops[i]
const nextStop = colorStops[i + 1]
if (
currentStop &&
nextStop &&
normalizedProgress >= currentStop.offset &&
normalizedProgress <= nextStop.offset
) {
// 计算两个颜色之间的插值
const range = nextStop.offset - currentStop.offset
const ratio = (normalizedProgress - currentStop.offset) / range
return currentStop.color
}
}
// 如果进度超出范围,返回最后一个颜色
const lastStop = colorStops[colorStops.length - 1]
return lastStop?.color || props.textColor
}
/**
* 格式化进度显示值
* @param value - 进度值
* @returns {string} 格式化后的进度值
*/
const formatProgressValue = (value: number): string => {
// 如果进度达到100直接返回100%
if (value >= 100) return '100%'
// 将进度值转换为两位小数
// const decimalValue = Math.round(value * 100) / 100
// 如果是整数,直接返回
if (Number.isInteger(value)) {
return `${value}%`
}
// 否则返回两位小数
return `${value.toFixed(2)}%`
}
/**
* 获取文字位置样式
* @returns {object} 文字位置样式对象
*/
const getTextPositionStyle = (): object => {
if (props.type !== 'horizontal') {
return {
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)',
}
}
const progress = currentNum.value / 100
const width = props.width
const textWidth = 60 // 预估文字宽度
// 根据文字位置类型返回对应的样式
switch (props.textPosition) {
// 文字在进度条前面
case 'front':
return {
left: `${textWidth / 2}px`,
top: '50%',
transform: 'translateY(-50%)',
}
// 文字在进度条后面
case 'back':
return {
right: `${textWidth / 2}px`,
top: '50%',
transform: 'translateY(-50%)',
}
// 文字跟随进度条
case 'follow':
default:
return {
left: `${Math.max(textWidth / 2, Math.min(width - textWidth / 2, width * progress))}px`, // 文字位置
top: '50%',
transform: 'translateX(-50%) translateY(-50%)',
}
}
}
/**
* 绘制圆弧
* @param ctx - Canvas上下文
* @param color - 填充颜色或渐变
* @param x - 圆心X坐标
* @param y - 圆心Y坐标
* @param radius - 半径
* @param start - 起始角度(弧度)
* @param end - 结束角度(弧度)
* @description
* 使用Canvas绘制圆弧支持纯色和渐变填充。
* 使用butt线帽和miter连接样式确保线条无圆角。
*/
const drawCircle = (
ctx: CanvasRenderingContext2D,
color: string | CanvasGradient,
x: number,
y: number,
radius: number,
start: number,
end: number,
) => {
ctx.save() // 保存当前状态
// 设置线条样式
ctx.lineCap = props.rounded ? 'round' : 'butt' // 根据rounded属性设置线帽
ctx.lineJoin = props.rounded ? 'round' : 'miter' // 根据rounded属性设置连接样式
ctx.lineWidth = props.strokeWidth // 设置线宽
ctx.strokeStyle = color // 设置线条颜色
// 创建路径
ctx.beginPath() // 开始绘制路径
ctx.arc(x, y, radius, start, end, !props.clockwise) // 绘制圆弧
// 绘制线条
ctx.stroke() // 绘制线条
ctx.closePath() // 关闭路径
ctx.restore() // 恢复状态
}
/**
* 绘制横向进度条
* @param ctx - Canvas上下文
* @param color - 填充颜色或渐变
* @param x - 起始X坐标
* @param y - 起始Y坐标
* @param width - 宽度
* @param height - 高度
* @param progress - 进度值0-1
* @description
* 使用Canvas绘制横向进度条支持纯色和渐变填充。
*/
const drawHorizontal = (
ctx: CanvasRenderingContext2D,
color: string | CanvasGradient,
x: number,
y: number,
width: number,
height: number,
progress: number,
) => {
ctx.save()
// 设置线条样式
ctx.lineCap = props.rounded ? 'round' : 'butt'
ctx.lineJoin = props.rounded ? 'round' : 'miter'
ctx.lineWidth = height
ctx.strokeStyle = color
// 计算圆角半径
const radius = props.rounded ? height / 2 : 0
// 计算实际进度宽度,考虑圆角
const actualWidth = Math.max(radius * 2, width * progress)
// 绘制进度条
ctx.beginPath()
// 从圆角中心点开始绘制
ctx.moveTo(x + radius, y + height / 2)
// 到圆角中心点结束
ctx.lineTo(x + actualWidth - radius, y + height / 2)
ctx.stroke()
ctx.closePath()
// 只在启用圆角时绘制起点和终点圆角
if (props.rounded) {
// 绘制起点圆角
ctx.beginPath()
ctx.arc(x + radius, y + height / 2, radius, -Math.PI / 2, Math.PI / 2)
ctx.fillStyle = color
ctx.fill()
ctx.closePath()
// 只在进度大于0时绘制终点圆角
if (progress > 0) {
ctx.beginPath()
ctx.arc(x + actualWidth - radius, y + height / 2, radius, Math.PI / 2, -Math.PI / 2)
ctx.fillStyle = color
ctx.fill()
ctx.closePath()
}
}
ctx.restore()
}
/**
* 执行动画绘制
* @description
* 使用requestAnimationFrame实现平滑的进度动画。
* 支持高DPI设备确保显示清晰。
* 包含背景轨道、进度条和进度文字的绘制。
*/
const animate = () => {
const canvas = canvasRef.value
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
// 设置画布的实际尺寸为显示尺寸的2倍以支持高DPI设备
const dpr = window.devicePixelRatio || 1
const displayWidth = props.type === 'horizontal' ? props.width : props.size
const displayHeight = props.type === 'horizontal' ? props.height : props.size
canvas.width = displayWidth * dpr
canvas.height = displayHeight * dpr
ctx.scale(dpr, dpr)
const draw = () => {
// 平滑过渡到目标值
const diff = targetNum.value - currentNum.value
if (Math.abs(diff) > 0.1) {
currentNum.value += diff * props.animationSpeed
animationFrame.value = requestAnimationFrame(draw)
} else {
currentNum.value = targetNum.value
}
ctx.clearRect(0, 0, displayWidth, displayHeight)
if (props.type === 'horizontal') {
// 绘制背景轨道
drawHorizontal(ctx, props.trackColor, 0, 0, displayWidth, displayHeight, 1)
// 获取当前进度的颜色或渐变
const progressColor = getProgressColor(ctx, displayWidth / 2, displayHeight / 2)
// 绘制进度条
drawHorizontal(ctx, progressColor, 0, 0, displayWidth, displayHeight, currentNum.value / 100)
} else {
// 原有的圆形进度条绘制逻辑
const centerX = props.size / 2
const centerY = props.size / 2
const radius = (props.size - props.strokeWidth) / 2
// 绘制背景轨道
drawCircle(ctx, props.trackColor, centerX, centerY, radius, 0, 2 * Math.PI)
// 获取当前进度的颜色或渐变
const progressColor = getProgressColor(ctx, centerX, centerY)
// 绘制进度条
const progressAngle = ((2 * currentNum.value) / 100) * Math.PI
const adjustedStartAngle = props.startAngle
const adjustedEndAngle = props.startAngle + progressAngle
// 绘制进度条
drawCircle(ctx, progressColor, centerX, centerY, radius, adjustedStartAngle, adjustedEndAngle)
}
}
draw()
}
// 组件挂载时初始化
onMounted(() => {
targetNum.value = props.percent
currentNum.value = props.percent
animate()
})
// 监听进度值变化
watch(
() => props.percent,
(newValue) => {
// 限制进度值不超过100
const limitedValue = Math.min(newValue, 100)
// 如果已经达到100%,不再更新
if (currentNum.value >= 100 && limitedValue >= 100) {
return
}
targetNum.value = limitedValue
// 取消之前的动画帧
if (animationFrame.value) {
cancelAnimationFrame(animationFrame.value)
animationFrame.value = null
}
animate()
},
)
// 组件卸载时清理动画帧
onUnmounted(() => {
if (animationFrame.value) {
cancelAnimationFrame(animationFrame.value)
animationFrame.value = null
}
})
return () => {
const currentColor = getCurrentProgressColor(currentNum.value)
const textContent = props.progressText
? props.progressText({ percent: Math.round(currentNum.value), color: currentColor })
: formatProgressValue(currentNum.value)
return (
<div
style={{
width: props.type === 'horizontal' ? `${props.width}px` : `${props.size}px`,
height: props.type === 'horizontal' ? `${props.height}px` : `${props.size}px`,
position: 'relative',
}}
>
<canvas
ref={canvasRef}
style={{
width: props.type === 'horizontal' ? `${props.width}px` : `${props.size}px`,
height: props.type === 'horizontal' ? `${props.height}px` : `${props.size}px`,
display: 'block',
}}
/>
<div
style={{
position: 'absolute',
...getTextPositionStyle(),
fontSize: props.type === 'horizontal' ? `${props.height * 0.8}px` : `${props.size / 7.5}px`,
fontFamily: 'Helvetica',
color: props.textColorFollowProgress ? currentColor : props.textColor,
}}
>
{textContent}
</div>
</div>
)
}
},
})
export default CircleProgress

View File

@@ -0,0 +1,55 @@
import { defineComponent } from 'vue'
import {
NConfigProvider,
NDialogProvider,
NMessageProvider,
NModalProvider,
NNotificationProvider,
zhCN,
dateZhCN,
} from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { useTheme } from '../theme'
import { useNaiveI18nSync } from '../i18n'
// 全局配置组件
export default defineComponent({
name: 'NCustomProvider',
setup(_, { slots }) {
const { locale } = useI18n() // 国际化
const { naiveLocale, naiveDateLocale } = useNaiveI18nSync(locale) // i18n 同步
const { theme, themeOverrides } = useTheme() // 主题
// 国际化配置
return () => (
<NConfigProvider
theme={theme.value}
theme-overrides={themeOverrides.value}
locale={naiveLocale.value || zhCN}
date-locale={naiveDateLocale.value || dateZhCN}
>
<NDialogProvider>
<NMessageProvider>
<NNotificationProvider>
<NModalProvider>{slots.default?.()}</NModalProvider>
</NNotificationProvider>
</NMessageProvider>
</NDialogProvider>
</NConfigProvider>
)
},
})
// 主题配置组件
const themeProvider = defineComponent({
name: 'NThemeProvider',
setup(_, { slots }) {
const { theme, themeOverrides } = useTheme() // 主题
return () => (
<NConfigProvider theme={theme.value} theme-overrides={themeOverrides.value}>
{slots.default?.()}
</NConfigProvider>
)
},
})
export { themeProvider }

View File

@@ -0,0 +1,39 @@
import { defineComponent } from 'vue'
import { NSwitch, NTooltip } from 'naive-ui'
import { useTheme } from '../theme/index'
/**
* @description 暗黑模式切换组件
*/
export const DarkModeSwitch = defineComponent({
name: 'DarkModeSwitch',
setup() {
const { isDark, cutDarkMode } = useTheme()
return () => (
<div>
<NTooltip trigger="hover">
{{
trigger: () => (
<NSwitch
value={isDark.value}
onUpdateValue={() => cutDarkMode()}
rail-style={() => ({
background: isDark.value ? '#333' : '#eee',
transition: 'background .3s',
})}
>
{{
checked: () => '🌙',
unchecked: () => '☀️',
}}
</NSwitch>
),
default: () => (isDark.value ? '切换到亮色模式' : '切换到暗色模式'),
}}
</NTooltip>
</div>
)
},
})
export default DarkModeSwitch

View File

@@ -0,0 +1,244 @@
/**
* 一个可自定义的环形进度条组件,支持渐变色效果。
*
* @component CircleProgressCSS
*
* @example 基础用法
* ```tsx
* <CircleProgressCSS
* percent={75}
* size={200}
* strokeWidth={20}
* progressColor="#2ba0fb"
* />
* ```
*
* @example 使用渐变色
* ```tsx
* <CircleProgressCSS
* percent={75}
* progressColor={[
* { offset: 0, color: '#ff0000' },
* { offset: 1, color: '#00ff00' }
* ]}
* />
* ```
*
* @props
* @prop {number} percent - 进度百分比(0-100)
* @prop {number} [size=200] - 圆环大小(像素)
* @prop {number} [strokeWidth=20] - 进度条宽度
* @prop {string} [textSize='24px'] - 百分比文字大小
* @prop {string} [trackColor='#e5f1fa'] - 背景轨道颜色
* @prop {string} [textColor='#333'] - 百分比文字颜色
* @prop {string} [holeColor='var(--n-color-modal)'] - 中心圆孔颜色
* @prop {string|ColorStop[]} [progressColor='#2ba0fb'] - 进度条颜色或渐变色配置
* @prop {boolean} [rounded=true] - 是否使用圆角
* @prop {boolean} [animated=true] - 是否启用动画效果
*
* @interface ColorStop
* @property {number} offset - 渐变色位置(0-1)
* @property {string} color - 十六进制颜色值(#RRGGBB)
*/
import { defineComponent, ref, computed, watch, PropType, CSSProperties } from 'vue'
import style from './index.module.css'
interface ColorStop {
offset: number
color: string
}
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
const match = hex
.trim()
.toLowerCase()
.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i)
if (!match) return null
return {
r: parseInt(match[1] as string, 16),
g: parseInt(match[2] as string, 16),
b: parseInt(match[3] as string, 16),
}
}
function polarToCartesian(centerX: number, centerY: number, radius: number, angleInDegrees: number) {
const angleInRadians = (angleInDegrees - 90) * (Math.PI / 180)
return {
x: centerX + radius * Math.cos(angleInRadians),
y: centerY + radius * Math.sin(angleInRadians),
}
}
function easeOutCubic(t: number): number {
return 1 - Math.pow(1 - t, 3)
}
export default defineComponent({
name: 'CircleProgressCSS',
props: {
percent: {
type: Number,
required: true,
validator: (v: number) => v >= 0 && v <= 100,
},
size: {
type: Number,
default: 200,
},
strokeWidth: {
type: Number,
default: 20,
},
textSize: {
type: String,
default: '24px',
},
trackColor: {
type: String,
default: '#e5f1fa',
},
textColor: {
type: String,
default: '#333',
},
holeColor: {
type: String,
default: 'var(--n-color-modal)',
},
progressColor: {
type: [String, Array] as PropType<string | ColorStop[]>,
default: '#2ba0fb',
},
rounded: {
type: Boolean,
default: true,
},
animated: {
type: Boolean,
default: true,
},
},
setup(props) {
const animatedPercent = ref(props.percent)
// 判断是否支持conic-gradient
const supportsConicGradient = () => CSS.supports('background-image', 'conic-gradient(red, yellow)')
watch(
() => props.percent,
(newVal) => {
if (!props.animated) {
animatedPercent.value = newVal
return
}
const duration = 300
const frameRate = 60
const frameCount = Math.round(duration / (1000 / frameRate))
const start = animatedPercent.value
const delta = newVal - start
let frame = 0
const animate = () => {
frame++
const progress = frame / frameCount
animatedPercent.value = start + delta * easeOutCubic(progress)
if (frame < frameCount) {
requestAnimationFrame(animate)
}
}
animate()
},
{ immediate: true },
)
const conicGradient = computed(() => {
if (!supportsConicGradient() && Array.isArray(props.progressColor) && props.progressColor.length > 0) {
return (props.progressColor[0] as ColorStop).color
}
if (typeof props.progressColor === 'string') {
return `conic-gradient(${props.progressColor} 0% 100%)`
}
const stops = (props.progressColor as ColorStop[]).map((s) => `${s.color} ${s.offset * 100}%`).join(', ')
return `conic-gradient(${stops})`
})
const getStartColor = (): string => {
if (animatedPercent.value === 0) return 'transparent'
if (typeof props.progressColor === 'string') return props.progressColor
return (props.progressColor[0] as ColorStop).color
}
const getEndColor = (): string => {
if (typeof props.progressColor === 'string') return props.progressColor
if (!supportsConicGradient() && Array.isArray(props.progressColor) && props.progressColor.length > 0) {
return (props.progressColor[0] as ColorStop).color
}
if (animatedPercent.value === 0 || animatedPercent.value === 100) return 'transparent'
const percent = animatedPercent.value / 100
const colorStops = props.progressColor as ColorStop[]
let prev = colorStops[0]
let next = colorStops[colorStops.length - 1]
for (let i = 0; i < colorStops.length - 1; i++) {
if (percent >= (colorStops[i] as ColorStop).offset && percent <= (colorStops[i + 1] as ColorStop).offset) {
prev = colorStops[i]
next = colorStops[i + 1]
break
}
}
const range = (next as ColorStop).offset - (prev as ColorStop).offset
const localPercent = range === 0 ? 0 : (percent - (prev as ColorStop).offset) / range
const prevRGB = hexToRgb((prev as ColorStop).color)
const nextRGB = hexToRgb((next as ColorStop).color)
if (!prevRGB || !nextRGB) return (next as ColorStop).color
const r = Math.round(prevRGB.r + (nextRGB.r - prevRGB.r) * localPercent)
const g = Math.round(prevRGB.g + (nextRGB.g - prevRGB.g) * localPercent)
const b = Math.round(prevRGB.b + (nextRGB.b - prevRGB.b) * localPercent)
return `rgb(${r}, ${g}, ${b})`
}
const capPosition = computed(() => {
const angle = (animatedPercent.value / 100) * 360
const center = props.size / 2
const radius = center - props.strokeWidth / 2
const pos = polarToCartesian(center, center, radius, angle)
return {
left: `${pos.x}px`,
top: `${pos.y}px`,
}
})
const containerStyle = computed<CSSProperties>(() => ({
width: `${props.size}px`,
height: `${props.size}px`,
'--track-color': props.trackColor,
'--text-color': props.textColor,
'--text-size': props.textSize,
'--percent': (animatedPercent.value / 100).toFixed(2),
'--progress-gradient': conicGradient.value,
'--stroke-width': `${props.strokeWidth}px`,
'--cap-start-color': getStartColor(),
'--cap-end-color': getEndColor(),
'--hole-color': props.holeColor,
}))
const displayText = computed(() => `${Math.round(animatedPercent.value)}%`)
return () => (
<div class={style['circle-progress']} style={containerStyle.value}>
<div class={style['circle-track']} />
<div class={style['circle-fill']} />
<div class={style['circle-hole']} />
<div class={[style['circle-cap'], style['start-cap']].join(' ')} />
<div class={[style['circle-cap'], style['end-cap']].join(' ')} style={capPosition.value} />
<div class={style['circle-text']}>{displayText.value}</div>
</div>
)
},
})

View File

@@ -0,0 +1,86 @@
/**
* 渐变进度条组件
*
* @description
* 一个支持渐变色的横向进度条组件,可自定义进度文本和样式
*
* @example
* ```tsx
* // 基础用法
* <HorizontalProgress :value="50" />
*
* // 自定义颜色
* <HorizontalProgress
* :value="75"
* color="linear-gradient(to right, #108ee9, #87d068)"
* />
*
* // 自定义进度文本
* <HorizontalProgress :value="30">
* <template #default>30%</template>
* </HorizontalProgress>
* ```
*
* @property {number} value - 进度值(0-100)
* @property {string} progressTextStyle - 进度文本的自定义样式
* @property {string} color - 进度条的渐变色,支持 CSS 渐变语法
*
* @slots
* default - 默认插槽,用于自定义进度条内的内容
*
* @requires vue
* @requires naive-ui
* @requires @vueuse/core
*/
import { defineComponent, ref, onMounted, useTemplateRef } from 'vue'
import style from './index.module.css'
import { useThemeVars } from 'naive-ui'
import { useResizeObserver, ResizeObserverEntry } from '@vueuse/core'
export default defineComponent({
props: {
value: {
type: Number,
required: true,
},
progressTextStyle: {
type: String,
default: '',
},
color: {
type: String,
default: 'linear-gradient(to left, #ff0000 0%, #ff7f00 50%, #20a53a 100%)',
},
},
setup(props, { slots }) {
const proContainer = useTemplateRef<HTMLElement | null>('proContainer')
const provWidth = ref(0)
const themeVars = useThemeVars()
onMounted(() => {
if (proContainer.value) {
provWidth.value = proContainer.value.clientWidth
}
})
useResizeObserver(proContainer, (entries) => {
const entry = entries[0] as ResizeObserverEntry
const { width } = entry.contentRect
provWidth.value = width
})
return () => (
<div
class={style['pro-container']}
style={{ width: '100%', backgroundColor: themeVars.value.progressRailColor }}
ref="proContainer"
>
<div class={style['probg']} style={{ width: `${props.value}%` }}>
<div class={style['prov']} style={{ width: `${provWidth.value}px`, background: props.color }}></div>
<div class={style['proText']} style={props.progressTextStyle}>
{slots.default ? slots.default() : ''}
</div>
</div>
</div>
)
},
})

View File

@@ -0,0 +1,102 @@
.pro-container{
border-radius: 50px;
}
.probg {
height: 12px;
border: none;
border-radius: 50px;
position: relative;
overflow: hidden;
transition: width 0.3s ease; /* 添加过渡效果 */
}
.prov {
height: 100%;
position: absolute;
z-index: 12;
top: 0;
border-radius: 50px;
left: 0;
overflow: hidden; /* 隐藏多余部分 */
transition: width 0.3s ease; /* 添加过渡效果 */
}
.proText{
height: 100%;
position: absolute;
z-index: 13;
font-size: 12px;
line-height: 12px;
margin-left: 20px;
color: #fff;
}
.circle-progress {
position: relative;
border-radius: 50%;
overflow: hidden;
}
.circle-track {
position: absolute;
width: 100%;
height: 100%;
background-color: var(--track-color);
border-radius: 50%;
}
.circle-fill {
position: absolute;
width: 100%;
height: 100%;
background-image: var(--progress-gradient);
mask-image: conic-gradient(black calc(var(--percent) * 100%), transparent 0);
-webkit-mask-image: conic-gradient(black calc(var(--percent) * 100%), transparent 0);
mask-composite: intersect;
-webkit-mask-composite: destination-in;
transition: all 0.3s ease;
border-radius: 50%;
}
.circle-hole {
position: absolute;
top: var(--stroke-width);
left: var(--stroke-width);
right: var(--stroke-width);
bottom: var(--stroke-width);
background-color: var(--hole-color);
border-radius: 50%;
z-index: 1;
}
.circle-cap {
position: absolute;
width: var(--stroke-width);
height: var(--stroke-width);
border-radius: 50%;
z-index: 2;
}
/* 起点圆点12点方向 */
.start-cap {
top: 0;
left: 50%;
transform: translate(-50%, 0);
background-color: var(--cap-start-color, #2ba0fb);
}
/* 终点圆点(动态位置) */
.end-cap {
transform: translate(-50%, -50%);
background-color: var(--cap-end-color, #2ba0fb);
}
.circle-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--text-color);
z-index: 2;
}

View File

@@ -0,0 +1,44 @@
import { defineComponent, PropType } from 'vue'
import { NDropdown, NIcon, NButton } from 'naive-ui'
import { Language } from '@vicons/ionicons5'
import { useLocalStorage } from '@vueuse/core'
import { localeList } from '../i18n'
import type { DropdownOption } from 'naive-ui'
interface Props {
type?: 'button' | 'link'
}
export default defineComponent({
props: {
type: {
type: String as PropType<'button' | 'link'>,
default: 'button',
},
},
setup(props: Props) {
const locale = useLocalStorage('locales-active', 'zhCN')
const dropdownOptions: DropdownOption[] = localeList.map((item) => ({
label: item.name,
key: item.type,
}))
return () => (
<NDropdown options={dropdownOptions} onSelect={(key: string) => (locale.value = key)} value={locale.value}>
<div style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }}>
{props.type === 'button' ? (
<NButton quaternary strong circle type="primary">
<NIcon size={20}>
<Language />
</NIcon>
</NButton>
) : (
<NIcon size={20}>
<Language />
</NIcon>
)}
</div>
</NDropdown>
)
},
})

View File

@@ -0,0 +1,38 @@
import { defineComponent } from 'vue'
import { NSelect, NSpace, NText } from 'naive-ui'
import { useTheme } from '../theme/index'
import DarkModeSwitch from './darkModeSwitch'
/**
* @description 主题管理组件
*/
export const ThemeManage = defineComponent({
name: 'ThemeManage',
setup() {
const { themeActive, getThemeList, setTheme } = useTheme()
// 获取主题列表
const themeList = getThemeList()
// 主题选项
const themeOptions = themeList.map((item) => ({
label: item.title,
value: item.name,
}))
return () => (
<NSpace>
<NText></NText>
<NSelect
style={{ width: '200px' }}
value={themeActive.value}
options={themeOptions}
onUpdateValue={(value: string) => setTheme(value)}
/>
<DarkModeSwitch />
</NSpace>
)
},
})
export default ThemeManage

View File

@@ -0,0 +1,20 @@
import { defineComponent } from 'vue'
import { NSpace, NText } from 'naive-ui'
import DarkModeSwitch from './darkModeSwitch'
/**
* @description 主题模式切换组件
*/
export const ThemeMode = defineComponent({
name: 'ThemeMode',
setup() {
return () => (
<NSpace align="center">
<NText></NText>
<DarkModeSwitch />
</NSpace>
)
},
})
export default ThemeMode

View File

@@ -0,0 +1,46 @@
import { defineComponent, PropType } from 'vue'
import { NDropdown, NIcon, NButton } from 'naive-ui'
import { Sunny, Moon } from '@vicons/ionicons5'
import { useTheme } from '../theme/index'
import type { DropdownOption } from 'naive-ui'
interface Props {
type?: 'button' | 'link'
}
export default defineComponent({
props: {
type: {
type: String as PropType<'button' | 'link'>,
default: 'button',
},
},
setup(props: Props) {
const { isDark, cutDarkMode, themeActive } = useTheme()
const dropdownOptions: DropdownOption[] = [
{
label: '亮色模式',
key: 'defaultLight',
},
{
label: '暗色模式',
key: 'defaultDark',
},
]
return () => (
<NDropdown options={dropdownOptions} onSelect={() => cutDarkMode(true, this)} value={themeActive.value}>
<div style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }}>
{props.type === 'button' ? (
<NButton quaternary strong circle type="primary">
<NIcon size={20}>{isDark.value ? <Moon /> : <Sunny />}</NIcon>
</NButton>
) : (
<NIcon size={20}>{isDark.value ? <Moon /> : <Sunny />}</NIcon>
)}
</div>
</NDropdown>
)
},
})