Compare commits
14 Commits
dc2e923e99
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f9e2a2f1a | ||
|
|
fae34b7fbd | ||
|
|
b9783b1f0c | ||
|
|
00816f223a | ||
|
|
0ac836eaa5 | ||
|
|
007b2bf39a | ||
|
|
c152ad5281 | ||
|
|
d6d59c890e | ||
|
|
d8209d7d23 | ||
|
|
553fc73ced | ||
|
|
c626ecedb2 | ||
|
|
55e732db3f | ||
|
|
ac806544c5 | ||
|
|
65589ebeb7 |
@@ -3,18 +3,16 @@
|
|||||||
*::-webkit-scrollbar {
|
*::-webkit-scrollbar {
|
||||||
width: 10px !important;
|
width: 10px !important;
|
||||||
height: 10px !important;
|
height: 10px !important;
|
||||||
cursor: var(--tui-cur-default);
|
background: #CFD2E0 !important;
|
||||||
background: #CFD2E0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-corner {
|
*::-webkit-scrollbar-corner {
|
||||||
background: #CFD2E0;
|
background: #CFD2E0 !important;
|
||||||
cursor: var(--tui-cur-default);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*::-webkit-scrollbar-thumb {
|
*::-webkit-scrollbar-thumb {
|
||||||
cursor: var(--tui-cur-default);
|
background: #525870 !important;
|
||||||
background: #525870;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
*::selection {
|
*::selection {
|
||||||
@@ -30,8 +28,8 @@ body {
|
|||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-x: hidden !important;
|
overflow-x: hidden;
|
||||||
overflow-y: scroll !important;
|
overflow-y: scroll;
|
||||||
font-family: var(--tui-font);
|
font-family: var(--tui-font);
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
|
|
||||||
@@ -199,3 +197,15 @@ textarea {
|
|||||||
.ir-smooth {
|
.ir-smooth {
|
||||||
image-rendering: smooth;
|
image-rendering: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cur-default {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cur-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cur-text {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
blue: #006EFF;
|
blue: #006EFF;
|
||||||
light-blue: #00A6FF;
|
light-blue: #00A6FF;
|
||||||
green: GREEN;
|
green: GREEN;
|
||||||
|
light-green: #3FE194;
|
||||||
orange: #E7913B;
|
orange: #E7913B;
|
||||||
gray: #666;
|
gray: #666;
|
||||||
light-gray: #AAA;
|
light-gray: #AAA;
|
||||||
|
|||||||
5
src/components/animated-number/index.ts
Normal file
5
src/components/animated-number/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import view from "./index.vue";
|
||||||
|
import Toolkit from "~/utils/Toolkit";
|
||||||
|
|
||||||
|
export const AnimatedNumber = Toolkit.withInstall(view);
|
||||||
|
export default AnimatedNumber;
|
||||||
184
src/components/animated-number/index.vue
Normal file
184
src/components/animated-number/index.vue
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<template>
|
||||||
|
<span class="tui-animated-number">{{ displayValue }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineOptions({
|
||||||
|
name: "AnimatedNumber"
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
/** 目标数值 */
|
||||||
|
value: number;
|
||||||
|
/** 动画持续时间(毫秒),默认 1000ms */
|
||||||
|
duration?: number;
|
||||||
|
/** 小数位数,undefined 表示自动 */
|
||||||
|
decimals?: number;
|
||||||
|
/** 缓动函数类型 */
|
||||||
|
easing?: "linear" | "easeOut" | "easeInOut";
|
||||||
|
/** 是否启用千分位分隔符 */
|
||||||
|
thousands?: boolean;
|
||||||
|
/** 千分位分隔符,默认逗号 */
|
||||||
|
separator?: string;
|
||||||
|
}>(), {
|
||||||
|
duration: 1000,
|
||||||
|
easing: "easeOut",
|
||||||
|
thousands: false,
|
||||||
|
separator: ","
|
||||||
|
});
|
||||||
|
|
||||||
|
const { value, duration, decimals, easing, thousands, separator } = toRefs(props);
|
||||||
|
|
||||||
|
// 当前显示的数值
|
||||||
|
const currentValue = ref(value.value);
|
||||||
|
// 格式化后的显示值
|
||||||
|
const displayValue = ref(formatNumber(value.value));
|
||||||
|
// 动画相关
|
||||||
|
let animationFrameId: number | null = null;
|
||||||
|
let startValue = value.value;
|
||||||
|
let startTime: number | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓动函数
|
||||||
|
*/
|
||||||
|
function ease(t: number, type: string): number {
|
||||||
|
switch (type) {
|
||||||
|
case "linear":
|
||||||
|
return t;
|
||||||
|
case "easeOut":
|
||||||
|
return 1 - Math.pow(1 - t, 3);
|
||||||
|
case "easeInOut":
|
||||||
|
return t < 0.5
|
||||||
|
? 4 * t * t * t
|
||||||
|
: 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||||
|
default:
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加千分位分隔符
|
||||||
|
*/
|
||||||
|
function addThousandsSeparator(numStr: string, sep: string): string {
|
||||||
|
const parts = numStr.split(".");
|
||||||
|
const integerPart = parts[0];
|
||||||
|
const decimalPart = parts[1] ? "." + parts[1] : "";
|
||||||
|
|
||||||
|
// 处理负号
|
||||||
|
const isNegative = integerPart.startsWith("-");
|
||||||
|
const absoluteInteger = isNegative ? integerPart.slice(1) : integerPart;
|
||||||
|
|
||||||
|
// 对整数部分添加千分位分隔符
|
||||||
|
const formatted = absoluteInteger.replace(/\B(?=(\d{3})+(?!\d))/g, sep);
|
||||||
|
|
||||||
|
return (isNegative ? "-" : "") + formatted + decimalPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化数字
|
||||||
|
*/
|
||||||
|
function formatNumber(num: number): string {
|
||||||
|
let result: string;
|
||||||
|
|
||||||
|
if (decimals.value !== undefined) {
|
||||||
|
result = num.toFixed(decimals.value);
|
||||||
|
} else {
|
||||||
|
// 自动检测小数位数
|
||||||
|
const str = num.toString();
|
||||||
|
if (str.includes(".")) {
|
||||||
|
// 保留原有的小数位数,但最多保留 6 位
|
||||||
|
const decimalPart = str.split(".")[1];
|
||||||
|
const decimalLength = Math.min(decimalPart.length, 6);
|
||||||
|
result = num.toFixed(decimalLength);
|
||||||
|
} else {
|
||||||
|
result = num.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用千分位分隔符
|
||||||
|
if (thousands.value) {
|
||||||
|
result = addThousandsSeparator(result, separator.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动画更新函数
|
||||||
|
*/
|
||||||
|
function animate(timestamp: number) {
|
||||||
|
if (startTime === null) {
|
||||||
|
startTime = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = timestamp - startTime;
|
||||||
|
const progress = Math.min(elapsed / duration.value, 1);
|
||||||
|
const easedProgress = ease(progress, easing.value);
|
||||||
|
|
||||||
|
// 计算当前值
|
||||||
|
currentValue.value = startValue + (value.value - startValue) * easedProgress;
|
||||||
|
displayValue.value = formatNumber(currentValue.value);
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
|
} else {
|
||||||
|
// 动画结束,确保显示精确的目标值
|
||||||
|
currentValue.value = value.value;
|
||||||
|
displayValue.value = formatNumber(value.value);
|
||||||
|
animationFrameId = null;
|
||||||
|
startTime = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始动画
|
||||||
|
*/
|
||||||
|
function startAnimation() {
|
||||||
|
// 取消之前的动画
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
|
||||||
|
startValue = currentValue.value;
|
||||||
|
startTime = null;
|
||||||
|
animationFrameId = requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听 value 变化
|
||||||
|
watch(value, (newValue) => {
|
||||||
|
if (newValue !== currentValue.value) {
|
||||||
|
startAnimation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 decimals 变化,立即更新显示格式
|
||||||
|
watch(decimals, () => {
|
||||||
|
displayValue.value = formatNumber(currentValue.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 thousands 和 separator 变化,立即更新显示格式
|
||||||
|
watch([thousands, separator], () => {
|
||||||
|
displayValue.value = formatNumber(currentValue.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件卸载时清理动画
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (animationFrameId !== null) {
|
||||||
|
cancelAnimationFrame(animationFrameId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
currentValue.value = value.value;
|
||||||
|
displayValue.value = formatNumber(value.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.tui-animated-number {
|
||||||
|
display: inline-block;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-feature-settings: "tnum";
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -26,20 +26,47 @@ const props = withDefaults(defineProps<{
|
|||||||
const { width, height, from, api } = toRefs(props);
|
const { width, height, from, api } = toRefs(props);
|
||||||
|
|
||||||
const src = ref("");
|
const src = ref("");
|
||||||
function update() {
|
const captchaId = ref("");
|
||||||
src.value = `${api.value}?from=${from.value}&width=${width.value}&height=${height.value}&r=${Toolkit.random(0, 999999)}`;
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
"update:captchaId": [id: string],
|
||||||
|
}>();
|
||||||
|
|
||||||
|
async function update() {
|
||||||
|
if (src.value && src.value.startsWith("blob:")) {
|
||||||
|
URL.revokeObjectURL(src.value);
|
||||||
|
}
|
||||||
|
const response = await fetch(`${api.value}?from=${from.value}&width=${width.value}&height=${height.value}&r=${Toolkit.random(0, 999999)}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`fetch captcha error: ${response.status}`);
|
||||||
|
}
|
||||||
|
captchaId.value = response.headers.get("X-Captcha-ID") || "";
|
||||||
|
src.value = URL.createObjectURL(await response.blob());
|
||||||
|
emit("update:captchaId", captchaId.value);
|
||||||
}
|
}
|
||||||
onMounted(update);
|
onMounted(update);
|
||||||
|
|
||||||
defineExpose({
|
watch([width, height], () => {
|
||||||
update
|
update();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (src.value && src.value.startsWith("blob:")) {
|
||||||
|
URL.revokeObjectURL(src.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
update,
|
||||||
|
getCaptchaId: () => captchaId.value,
|
||||||
|
refresh: update
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.tui-captcha {
|
.tui-captcha {
|
||||||
cursor: var(--tui-cur-pointer);
|
cursor: var(--tui-cur-pointer);
|
||||||
border: 1px solid gray;
|
|
||||||
display: block;
|
display: block;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,20 @@ import Popup from "./popup";
|
|||||||
import Captcha from "./captcha";
|
import Captcha from "./captcha";
|
||||||
import MarkdownView from "./markdown-view";
|
import MarkdownView from "./markdown-view";
|
||||||
import MarkdownEditor from "./markdown-editor";
|
import MarkdownEditor from "./markdown-editor";
|
||||||
|
import AnimatedNumber from "./animated-number";
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
Popup,
|
Popup,
|
||||||
Captcha,
|
Captcha,
|
||||||
MarkdownView,
|
MarkdownView,
|
||||||
MarkdownEditor
|
MarkdownEditor,
|
||||||
|
AnimatedNumber
|
||||||
];
|
];
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Popup,
|
Popup,
|
||||||
Captcha,
|
Captcha,
|
||||||
MarkdownView,
|
MarkdownView,
|
||||||
MarkdownEditor
|
MarkdownEditor,
|
||||||
|
AnimatedNumber
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ defineOptions({
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
#tui-popup {
|
#tui-popup {
|
||||||
border: var(--tui-border);
|
border: var(--tui-border);
|
||||||
z-index: 20;
|
z-index: 3000;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export * from "./utils/directives/Popup";
|
|||||||
export type { ScrollListener } from "./utils/Scroller";
|
export type { ScrollListener } from "./utils/Scroller";
|
||||||
export type { DraggableConfig } from "./utils/directives/Draggable";
|
export type { DraggableConfig } from "./utils/directives/Draggable";
|
||||||
export type { PopupConfig } from "./utils/directives/Popup";
|
export type { PopupConfig } from "./utils/directives/Popup";
|
||||||
|
export type { ErrorCallback } from "./utils/Network"
|
||||||
|
|
||||||
const install = function (app: App) {
|
const install = function (app: App) {
|
||||||
components.forEach(component => {
|
components.forEach(component => {
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export type Response = {
|
|||||||
export type Page = {
|
export type Page = {
|
||||||
index: number;
|
index: number;
|
||||||
size: number;
|
size: number;
|
||||||
keyword?: string;
|
|
||||||
orderMap?: { [key: string]: OrderType };
|
orderMap?: { [key: string]: OrderType };
|
||||||
|
likeMap?: { [key: string]: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum OrderType {
|
export enum OrderType {
|
||||||
|
|||||||
@@ -1,20 +1,31 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Response } from "~/types/Model";
|
import { Response } from "~/types/Model";
|
||||||
|
|
||||||
|
export type ErrorCallback = (response: Response) => void;
|
||||||
|
|
||||||
|
let globalErrorCallback: ErrorCallback | null = null;
|
||||||
|
|
||||||
|
export const setGlobalErrorCallback = (callback: ErrorCallback) => {
|
||||||
|
globalErrorCallback = callback;
|
||||||
|
};
|
||||||
|
|
||||||
axios.defaults.withCredentials = true;
|
axios.defaults.withCredentials = true;
|
||||||
axios.interceptors.response.use((response: any) => {
|
axios.interceptors.response.use((axiosResp: any) => {
|
||||||
if (!response.config.responseType) {
|
if (!axiosResp.config.responseType) {
|
||||||
// 服务端返回
|
// 服务端返回
|
||||||
const data = response.data as Response;
|
const serverResp = axiosResp.data as Response;
|
||||||
if (data.code < 40000) {
|
if (serverResp.code < 40000) {
|
||||||
// 200 或 300 HTTP 状态段视为成功
|
// 200 或 300 HTTP 状态段视为成功
|
||||||
return data.data;
|
return serverResp.data;
|
||||||
} else {
|
} else {
|
||||||
|
if (globalErrorCallback) {
|
||||||
|
globalErrorCallback(serverResp);
|
||||||
|
}
|
||||||
// 由调用方处理
|
// 由调用方处理
|
||||||
return Promise.reject(data.msg);
|
return Promise.reject(serverResp.msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return response.data;
|
return axiosResp.data;
|
||||||
}, (error: any) => {
|
}, (error: any) => {
|
||||||
// 请求错误
|
// 请求错误
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -34,4 +45,5 @@ axios.interceptors.response.use((response: any) => {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
axios,
|
axios,
|
||||||
|
setGlobalErrorCallback
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -142,15 +142,25 @@ export default class Toolkit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成随机数
|
* 生成随机数(整数)
|
||||||
*
|
*
|
||||||
* @param min 最小值
|
* @param min 最小值
|
||||||
* @param max 最大值
|
* @param max 最大值
|
||||||
*/
|
*/
|
||||||
public static random(min = 0, max = 100): number {
|
public static random(min = 0, max = 100): number {
|
||||||
return Math.floor(Math.random() * (max + 1 - min)) + min;
|
return Math.floor(Math.random() * (max + 1 - min)) + min;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机数(浮点数)
|
||||||
|
*
|
||||||
|
* @param min 最小值
|
||||||
|
* @param max 最大值
|
||||||
|
*/
|
||||||
|
public static randomDouble(min = 0, max = 1): number {
|
||||||
|
return Math.random() * (max - min) + min;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base64 数据转文件
|
* Base64 数据转文件
|
||||||
*
|
*
|
||||||
@@ -321,4 +331,66 @@ export default class Toolkit {
|
|||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static doNotNull(arg: any, func: (arg: any) => void): void {
|
||||||
|
if (arg) {
|
||||||
|
func(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static toCssSize(value: number | string): string {
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return `${value}px`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置随机间隔执行
|
||||||
|
*
|
||||||
|
* @param config 配置对象
|
||||||
|
* @param config.handler 处理函数,如果提供了 min 和 max,则会接收随机数作为参数
|
||||||
|
* @param config.handleRate 执行概率,0-1 之间,默认 1(总是执行)
|
||||||
|
* @param config.interval 间隔时间(毫秒)
|
||||||
|
* @param config.min 随机数最小值(可选),提供时会生成随机数传给 handler
|
||||||
|
* @param config.max 随机数最大值(可选),提供时会生成随机数传给 handler
|
||||||
|
* @returns 定时器 ID
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```js
|
||||||
|
* // 简单的随机执行
|
||||||
|
* setRandomInterval({
|
||||||
|
* handler: () => console.log('executed'),
|
||||||
|
* handleRate: 0.5,
|
||||||
|
* interval: 1000
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* // 带随机数参数的执行
|
||||||
|
* setRandomInterval({
|
||||||
|
* handler: (value) => console.log('random value:', value),
|
||||||
|
* handleRate: 1,
|
||||||
|
* min: 0,
|
||||||
|
* max: 100,
|
||||||
|
* interval: 1000
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public static setRandomInterval(config: {
|
||||||
|
handler: Function | ((value: number) => void);
|
||||||
|
handleRate?: number;
|
||||||
|
interval?: number;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}): NodeJS.Timeout {
|
||||||
|
const { handler, handleRate = 1, interval, min, max } = config;
|
||||||
|
return setInterval(() => {
|
||||||
|
if (Math.random() < handleRate) {
|
||||||
|
if (min !== undefined && max !== undefined) {
|
||||||
|
(handler as (value: number) => void)(this.randomDouble(min, max));
|
||||||
|
} else {
|
||||||
|
(handler as Function)();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, interval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ const VPopup: Directive = {
|
|||||||
// 显示
|
// 显示
|
||||||
el.addEventListener("mouseenter", async e => {
|
el.addEventListener("mouseenter", async e => {
|
||||||
if (!config.value) {
|
if (!config.value) {
|
||||||
console.warn("not found popup value", config);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (config.beforeShow) {
|
if (config.beforeShow) {
|
||||||
|
|||||||
Reference in New Issue
Block a user