mirror of
https://gitee.com/mirrors/AllinSSL.git
synced 2026-03-15 10:52:01 +08:00
【修复】条件节点前fromNodeId传值问题
【修复】部署参数默认错误问题 【测设】部分项目代码结构 【同步】前端项目代码
This commit is contained in:
711
frontend/packages/vue/naive-ui/src/components/circleProgress.tsx
Normal file
711
frontend/packages/vue/naive-ui/src/components/circleProgress.tsx
Normal 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
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -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
|
||||
20
frontend/packages/vue/naive-ui/src/components/themeMode.tsx
Normal file
20
frontend/packages/vue/naive-ui/src/components/themeMode.tsx
Normal 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
|
||||
46
frontend/packages/vue/naive-ui/src/components/themeTips.tsx
Normal file
46
frontend/packages/vue/naive-ui/src/components/themeTips.tsx
Normal 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>
|
||||
)
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user