From 10eb44ac330952346059fad86414daa311ffd525 Mon Sep 17 00:00:00 2001 From: pan <13329870472@163.com> Date: Tue, 12 Mar 2024 17:29:30 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E9=A6=96=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ReCountTo/README.md | 2 + src/components/ReCountTo/index.ts | 11 + src/components/ReCountTo/src/normal/index.tsx | 179 ++++++++++++ src/components/ReCountTo/src/normal/props.ts | 31 ++ .../ReCountTo/src/rebound/index.tsx | 72 +++++ src/components/ReCountTo/src/rebound/props.ts | 14 + .../ReCountTo/src/rebound/rebound.css | 77 +++++ src/components/ReFlicker/index.css | 39 +++ src/components/ReFlicker/index.ts | 44 +++ src/main.ts | 6 +- src/views/welcome/components/chart/bar.vue | 108 +++++++ src/views/welcome/components/chart/index.ts | 3 + src/views/welcome/components/chart/line.vue | 62 ++++ src/views/welcome/components/chart/round.vue | 73 +++++ .../welcome/components/table/columns.tsx | 104 +++++++ src/views/welcome/components/table/empty.svg | 1 + src/views/welcome/components/table/index.vue | 71 +++++ src/views/welcome/data.ts | 134 +++++++++ src/views/welcome/index.vue | 269 +++++++++++++++++- src/views/welcome/utils.ts | 6 + 20 files changed, 1302 insertions(+), 4 deletions(-) create mode 100644 src/components/ReCountTo/README.md create mode 100644 src/components/ReCountTo/index.ts create mode 100644 src/components/ReCountTo/src/normal/index.tsx create mode 100644 src/components/ReCountTo/src/normal/props.ts create mode 100644 src/components/ReCountTo/src/rebound/index.tsx create mode 100644 src/components/ReCountTo/src/rebound/props.ts create mode 100644 src/components/ReCountTo/src/rebound/rebound.css create mode 100644 src/components/ReFlicker/index.css create mode 100644 src/components/ReFlicker/index.ts create mode 100644 src/views/welcome/components/chart/bar.vue create mode 100644 src/views/welcome/components/chart/index.ts create mode 100644 src/views/welcome/components/chart/line.vue create mode 100644 src/views/welcome/components/chart/round.vue create mode 100644 src/views/welcome/components/table/columns.tsx create mode 100644 src/views/welcome/components/table/empty.svg create mode 100644 src/views/welcome/components/table/index.vue create mode 100644 src/views/welcome/data.ts create mode 100644 src/views/welcome/utils.ts diff --git a/src/components/ReCountTo/README.md b/src/components/ReCountTo/README.md new file mode 100644 index 0000000..b5048f3 --- /dev/null +++ b/src/components/ReCountTo/README.md @@ -0,0 +1,2 @@ +normal 普通数字动画组件 +rebound 回弹式数字动画组件 diff --git a/src/components/ReCountTo/index.ts b/src/components/ReCountTo/index.ts new file mode 100644 index 0000000..1817218 --- /dev/null +++ b/src/components/ReCountTo/index.ts @@ -0,0 +1,11 @@ +import reNormalCountTo from "./src/normal"; +import reboundCountTo from "./src/rebound"; +import { withInstall } from "@pureadmin/utils"; + +/** 普通数字动画组件 */ +const ReNormalCountTo = withInstall(reNormalCountTo); + +/** 回弹式数字动画组件 */ +const ReboundCountTo = withInstall(reboundCountTo); + +export { ReNormalCountTo, ReboundCountTo }; diff --git a/src/components/ReCountTo/src/normal/index.tsx b/src/components/ReCountTo/src/normal/index.tsx new file mode 100644 index 0000000..5c7a06c --- /dev/null +++ b/src/components/ReCountTo/src/normal/index.tsx @@ -0,0 +1,179 @@ +import { + defineComponent, + reactive, + computed, + watch, + onMounted, + unref +} from "vue"; +import { countToProps } from "./props"; +import { isNumber } from "@pureadmin/utils"; + +export default defineComponent({ + name: "ReNormalCountTo", + props: countToProps, + emits: ["mounted", "callback"], + setup(props, { emit }) { + const state = reactive<{ + localStartVal: number; + printVal: number | null; + displayValue: string; + paused: boolean; + localDuration: number | null; + startTime: number | null; + timestamp: number | null; + rAF: any; + remaining: number | null; + color: string; + fontSize: string; + }>({ + localStartVal: props.startVal, + displayValue: formatNumber(props.startVal), + printVal: null, + paused: false, + localDuration: props.duration, + startTime: null, + timestamp: null, + remaining: null, + rAF: null, + color: null, + fontSize: "16px" + }); + + const getCountDown = computed(() => { + return props.startVal > props.endVal; + }); + + watch([() => props.startVal, () => props.endVal], () => { + if (props.autoplay) { + start(); + } + }); + + function start() { + const { startVal, duration, color, fontSize } = props; + state.localStartVal = startVal; + state.startTime = null; + state.localDuration = duration; + state.paused = false; + state.color = color; + state.fontSize = fontSize; + state.rAF = requestAnimationFrame(count); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars + function pauseResume() { + if (state.paused) { + resume(); + state.paused = false; + } else { + pause(); + state.paused = true; + } + } + + function pause() { + cancelAnimationFrame(state.rAF); + } + + function resume() { + state.startTime = null; + state.localDuration = +(state.remaining as number); + state.localStartVal = +(state.printVal as number); + requestAnimationFrame(count); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars,no-unused-vars + function reset() { + state.startTime = null; + cancelAnimationFrame(state.rAF); + state.displayValue = formatNumber(props.startVal); + } + + function count(timestamp: number) { + const { useEasing, easingFn, endVal } = props; + if (!state.startTime) state.startTime = timestamp; + state.timestamp = timestamp; + const progress = timestamp - state.startTime; + state.remaining = (state.localDuration as number) - progress; + if (useEasing) { + if (unref(getCountDown)) { + state.printVal = + state.localStartVal - + easingFn( + progress, + 0, + state.localStartVal - endVal, + state.localDuration as number + ); + } else { + state.printVal = easingFn( + progress, + state.localStartVal, + endVal - state.localStartVal, + state.localDuration as number + ); + } + } else { + if (unref(getCountDown)) { + state.printVal = + state.localStartVal - + (state.localStartVal - endVal) * + (progress / (state.localDuration as number)); + } else { + state.printVal = + state.localStartVal + + (endVal - state.localStartVal) * + (progress / (state.localDuration as number)); + } + } + if (unref(getCountDown)) { + state.printVal = state.printVal < endVal ? endVal : state.printVal; + } else { + state.printVal = state.printVal > endVal ? endVal : state.printVal; + } + state.displayValue = formatNumber(state.printVal); + if (progress < (state.localDuration as number)) { + state.rAF = requestAnimationFrame(count); + } else { + emit("callback"); + } + } + + function formatNumber(num: number | string) { + const { decimals, decimal, separator, suffix, prefix } = props; + num = Number(num).toFixed(decimals); + num += ""; + const x = num.split("."); + let x1 = x[0]; + const x2 = x.length > 1 ? decimal + x[1] : ""; + const rgx = /(\d+)(\d{3})/; + if (separator && !isNumber(separator)) { + while (rgx.test(x1)) { + x1 = x1.replace(rgx, "$1" + separator + "$2"); + } + } + return prefix + x1 + x2 + suffix; + } + + onMounted(() => { + if (props.autoplay) { + start(); + } + emit("mounted"); + }); + + return () => ( + <> + + {state.displayValue} + + + ); + } +}); diff --git a/src/components/ReCountTo/src/normal/props.ts b/src/components/ReCountTo/src/normal/props.ts new file mode 100644 index 0000000..142fd3a --- /dev/null +++ b/src/components/ReCountTo/src/normal/props.ts @@ -0,0 +1,31 @@ +import type { PropType } from "vue"; +import propTypes from "@/utils/propTypes"; +export const countToProps = { + startVal: propTypes.number.def(0), + endVal: propTypes.number.def(2020), + duration: propTypes.number.def(1300), + autoplay: propTypes.bool.def(true), + decimals: { + type: Number as PropType, + required: false, + default: 0, + validator(value: number) { + return value >= 0; + } + }, + color: propTypes.string.def(), + fontSize: propTypes.string.def(), + decimal: propTypes.string.def("."), + separator: propTypes.string.def(","), + prefix: propTypes.string.def(""), + suffix: propTypes.string.def(""), + useEasing: propTypes.bool.def(true), + easingFn: { + type: Function as PropType< + (t: number, b: number, c: number, d: number) => number + >, + default(t: number, b: number, c: number, d: number) { + return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b; + } + } +}; diff --git a/src/components/ReCountTo/src/rebound/index.tsx b/src/components/ReCountTo/src/rebound/index.tsx new file mode 100644 index 0000000..ad16f7a --- /dev/null +++ b/src/components/ReCountTo/src/rebound/index.tsx @@ -0,0 +1,72 @@ +import "./rebound.css"; +import { + defineComponent, + ref, + unref, + onBeforeMount, + onBeforeUnmount +} from "vue"; +import { reboundProps } from "./props"; + +export default defineComponent({ + name: "ReboundCountTo", + props: reboundProps, + setup(props) { + const ulRef = ref(); + const timer = ref(null); + + onBeforeMount(() => { + const ua = navigator.userAgent.toLowerCase(); + const testUA = regexp => regexp.test(ua); + const isSafari = testUA(/safari/g) && !testUA(/chrome/g); + + // Safari浏览器的兼容代码 + isSafari && + (timer.value = setTimeout(() => { + ulRef.value.setAttribute( + "style", + ` + animation: none; + transform: translateY(calc(var(--i) * -9.09%)) + ` + ); + }, props.delay * 1000)); + }); + + onBeforeUnmount(() => { + clearTimeout(unref(timer)); + }); + + return () => ( + <> +
+ + + + + + + +
+ + ); + } +}); diff --git a/src/components/ReCountTo/src/rebound/props.ts b/src/components/ReCountTo/src/rebound/props.ts new file mode 100644 index 0000000..f3dcdc8 --- /dev/null +++ b/src/components/ReCountTo/src/rebound/props.ts @@ -0,0 +1,14 @@ +import type { PropType } from "vue"; +import propTypes from "@/utils/propTypes"; +export const reboundProps = { + delay: propTypes.number.def(1), + blur: propTypes.number.def(2), + i: { + type: Number as PropType, + required: false, + default: 0, + validator(value: number) { + return value < 10 && value >= 0 && Number.isInteger(value); + } + } +}; diff --git a/src/components/ReCountTo/src/rebound/rebound.css b/src/components/ReCountTo/src/rebound/rebound.css new file mode 100644 index 0000000..9fc5932 --- /dev/null +++ b/src/components/ReCountTo/src/rebound/rebound.css @@ -0,0 +1,77 @@ +.scroll-num { + width: var(--width, 20px); + height: var(--height, calc(var(--width, 20px) * 1.8)); + color: var(--color, #333); + font-size: var(--height, calc(var(--width, 20px) * 1.1)); + line-height: var(--height, calc(var(--width, 20px) * 1.8)); + text-align: center; + overflow: hidden; + animation: enhance-bounce-in-down 1s calc(var(--delay) * 1s) forwards; +} + +ul { + animation: + move 0.3s linear infinite, + bounce-in-down 1s calc(var(--delay) * 1s) forwards; +} + +@keyframes move { + from { + transform: translateY(-90%); + filter: url(#blur); + } + + to { + transform: translateY(1%); + filter: url(#blur); + } +} + +@keyframes bounce-in-down { + from { + transform: translateY(calc(var(--i) * -9.09% - 7%)); + filter: none; + } + + 25% { + transform: translateY(calc(var(--i) * -9.09% + 3%)); + } + + 50% { + transform: translateY(calc(var(--i) * -9.09% - 1%)); + } + + 70% { + transform: translateY(calc(var(--i) * -9.09% + 0.6%)); + } + + 85% { + transform: translateY(calc(var(--i) * -9.09% - 0.3%)); + } + + to { + transform: translateY(calc(var(--i) * -9.09%)); + } +} + +@keyframes enhance-bounce-in-down { + 25% { + transform: translateY(8%); + } + + 50% { + transform: translateY(-4%); + } + + 70% { + transform: translateY(2%); + } + + 85% { + transform: translateY(-1%); + } + + to { + transform: translateY(0); + } +} diff --git a/src/components/ReFlicker/index.css b/src/components/ReFlicker/index.css new file mode 100644 index 0000000..4c40af4 --- /dev/null +++ b/src/components/ReFlicker/index.css @@ -0,0 +1,39 @@ +.point { + width: var(--point-width); + height: var(--point-height); + background: var(--point-background); + position: relative; + border-radius: var(--point-border-radius); +} + +.point-flicker:after { + background: var(--point-background); +} + +.point-flicker:before, +.point-flicker:after { + content: ""; + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; + border-radius: var(--point-border-radius); + animation: flicker 1.2s ease-out infinite; +} + +@keyframes flicker { + 0% { + transform: scale(0.5); + opacity: 1; + } + + 30% { + opacity: 1; + } + + 100% { + transform: scale(var(--point-scale)); + opacity: 0; + } +} diff --git a/src/components/ReFlicker/index.ts b/src/components/ReFlicker/index.ts new file mode 100644 index 0000000..b8d29b7 --- /dev/null +++ b/src/components/ReFlicker/index.ts @@ -0,0 +1,44 @@ +import "./index.css"; +import { h, defineComponent, type Component } from "vue"; + +export interface attrsType { + width?: string; + height?: string; + borderRadius?: number | string; + background?: string; + scale?: number | string; +} + +/** + * 圆点、方形闪烁动画组件 + * @param width 可选 string 宽 + * @param height 可选 string 高 + * @param borderRadius 可选 number | string 传0为方形、传50%或者不传为圆形 + * @param background 可选 string 闪烁颜色 + * @param scale 可选 number | string 闪烁范围,默认2,值越大闪烁范围越大 + * @returns Component + */ +export function useRenderFlicker(attrs?: attrsType): Component { + return defineComponent({ + name: "ReFlicker", + render() { + return h( + "div", + { + class: "point point-flicker", + style: { + "--point-width": attrs?.width ?? "12px", + "--point-height": attrs?.height ?? "12px", + "--point-background": + attrs?.background ?? "var(--el-color-primary)", + "--point-border-radius": attrs?.borderRadius ?? "50%", + "--point-scale": attrs?.scale ?? "2" + } + }, + { + default: () => [] + } + ); + } + }); +} diff --git a/src/main.ts b/src/main.ts index d7ea35b..5237cdd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import { setupStore } from "@/store"; import { useI18n } from "@/plugins/i18n"; import { getPlatformConfig } from "./config"; import { MotionPlugin } from "@vueuse/motion"; -// import { useEcharts } from "@/plugins/echarts"; +import { useEcharts } from "@/plugins/echarts"; import { createApp, type Directive } from "vue"; import { useElementPlus } from "@/plugins/elementPlus"; import { injectResponsiveStorage } from "@/utils/responsive"; @@ -61,7 +61,7 @@ getPlatformConfig(app).then(async config => { .use(useI18n) .use(useElementPlus) .use(Table) - .use(PureDescriptions); - // .use(useEcharts); + .use(PureDescriptions) + .use(useEcharts); app.mount("#app"); }); diff --git a/src/views/welcome/components/chart/bar.vue b/src/views/welcome/components/chart/bar.vue new file mode 100644 index 0000000..6d9af15 --- /dev/null +++ b/src/views/welcome/components/chart/bar.vue @@ -0,0 +1,108 @@ + + + diff --git a/src/views/welcome/components/chart/index.ts b/src/views/welcome/components/chart/index.ts new file mode 100644 index 0000000..42cd5e6 --- /dev/null +++ b/src/views/welcome/components/chart/index.ts @@ -0,0 +1,3 @@ +export { default as barChart } from "./bar.vue"; +export { default as lineChart } from "./line.vue"; +export { default as roundChart } from "./round.vue"; diff --git a/src/views/welcome/components/chart/line.vue b/src/views/welcome/components/chart/line.vue new file mode 100644 index 0000000..fa72ec1 --- /dev/null +++ b/src/views/welcome/components/chart/line.vue @@ -0,0 +1,62 @@ + + + diff --git a/src/views/welcome/components/chart/round.vue b/src/views/welcome/components/chart/round.vue new file mode 100644 index 0000000..769f2b2 --- /dev/null +++ b/src/views/welcome/components/chart/round.vue @@ -0,0 +1,73 @@ + + + diff --git a/src/views/welcome/components/table/columns.tsx b/src/views/welcome/components/table/columns.tsx new file mode 100644 index 0000000..c6d0b8e --- /dev/null +++ b/src/views/welcome/components/table/columns.tsx @@ -0,0 +1,104 @@ +import { tableData } from "../../data"; +import { delay } from "@pureadmin/utils"; +import { ref, onMounted, reactive } from "vue"; +import type { PaginationProps } from "@pureadmin/table"; +import ThumbUp from "@iconify-icons/ri/thumb-up-line"; +import Hearts from "@iconify-icons/ri/hearts-line"; +import Empty from "./empty.svg?component"; + +export function useColumns() { + const dataList = ref([]); + const loading = ref(true); + const columns: TableColumnList = [ + { + sortable: true, + label: "序号", + prop: "id" + }, + { + sortable: true, + label: "需求人数", + prop: "requiredNumber", + filterMultiple: false, + filterClassName: "pure-table-filter", + filters: [ + { text: "≥16000", value: "more" }, + { text: "<16000", value: "less" } + ], + filterMethod: (value, { requiredNumber }) => { + return value === "more" + ? requiredNumber >= 16000 + : requiredNumber < 16000; + } + }, + { + sortable: true, + label: "提问数量", + prop: "questionNumber" + }, + { + sortable: true, + label: "解决数量", + prop: "resolveNumber" + }, + { + sortable: true, + label: "用户满意度", + minWidth: 100, + prop: "satisfaction", + cellRenderer: ({ row }) => ( +
+ + {row.satisfaction}% + 98 ? Hearts : ThumbUp} + color="#e85f33" + /> + +
+ ) + }, + { + sortable: true, + label: "统计日期", + prop: "date" + }, + { + label: "操作", + fixed: "right", + slot: "operation" + } + ]; + + /** 分页配置 */ + const pagination = reactive({ + pageSize: 10, + currentPage: 1, + layout: "prev, pager, next", + total: 0, + align: "center" + }); + + function onCurrentChange(page: number) { + console.log("onCurrentChange", page); + loading.value = true; + delay(300).then(() => { + loading.value = false; + }); + } + + onMounted(() => { + dataList.value = tableData; + pagination.total = dataList.value.length; + loading.value = false; + }); + + return { + Empty, + loading, + columns, + dataList, + pagination, + onCurrentChange + }; +} diff --git a/src/views/welcome/components/table/empty.svg b/src/views/welcome/components/table/empty.svg new file mode 100644 index 0000000..5c8b211 --- /dev/null +++ b/src/views/welcome/components/table/empty.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/views/welcome/components/table/index.vue b/src/views/welcome/components/table/index.vue new file mode 100644 index 0000000..ab8c179 --- /dev/null +++ b/src/views/welcome/components/table/index.vue @@ -0,0 +1,71 @@ + + + + + + + diff --git a/src/views/welcome/data.ts b/src/views/welcome/data.ts new file mode 100644 index 0000000..3bb5021 --- /dev/null +++ b/src/views/welcome/data.ts @@ -0,0 +1,134 @@ +import { dayjs, cloneDeep, getRandomIntBetween } from "./utils"; +import GroupLine from "@iconify-icons/ri/group-line"; +import Question from "@iconify-icons/ri/question-answer-line"; +import CheckLine from "@iconify-icons/ri/chat-check-line"; +import Smile from "@iconify-icons/ri/star-smile-line"; + +const days = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]; + +/** 需求人数、提问数量、解决数量、用户满意度 */ +const chartData = [ + { + icon: GroupLine, + bgColor: "#effaff", + color: "#41b6ff", + duration: 2200, + name: "需求人数", + value: 36000, + percent: "+88%", + data: [2101, 5288, 4239, 4962, 6752, 5208, 7450] // 平滑折线图数据 + }, + { + icon: Question, + bgColor: "#fff5f4", + color: "#e85f33", + duration: 1600, + name: "提问数量", + value: 16580, + percent: "+70%", + data: [2216, 1148, 1255, 788, 4821, 1973, 4379] + }, + { + icon: CheckLine, + bgColor: "#eff8f4", + color: "#26ce83", + duration: 1500, + name: "解决数量", + value: 16499, + percent: "+99%", + data: [861, 1002, 3195, 1715, 3666, 2415, 3645] + }, + { + icon: Smile, + bgColor: "#f6f4fe", + color: "#7846e5", + duration: 100, + name: "用户满意度", + value: 100, + percent: "+100%", + data: [100] + } +]; + +/** 分析概览 */ +const barChartData = [ + { + requireData: [2101, 5288, 4239, 4962, 6752, 5208, 7450], + questionData: [2216, 1148, 1255, 1788, 4821, 1973, 4379] + }, + { + requireData: [2101, 3280, 4400, 4962, 5752, 6889, 7600], + questionData: [2116, 3148, 3255, 3788, 4821, 4970, 5390] + } +]; + +/** 解决概率 */ +const progressData = [ + { + week: "周一", + percentage: 85, + duration: 110, + color: "#41b6ff" + }, + { + week: "周二", + percentage: 86, + duration: 105, + color: "#41b6ff" + }, + { + week: "周三", + percentage: 88, + duration: 100, + color: "#41b6ff" + }, + { + week: "周四", + percentage: 89, + duration: 95, + color: "#41b6ff" + }, + { + week: "周五", + percentage: 94, + duration: 90, + color: "#26ce83" + }, + { + week: "周六", + percentage: 96, + duration: 85, + color: "#26ce83" + }, + { + week: "周日", + percentage: 100, + duration: 80, + color: "#26ce83" + } +].reverse(); + +/** 数据统计 */ +const tableData = Array.from({ length: 30 }).map((_, index) => { + return { + id: index + 1, + requiredNumber: getRandomIntBetween(13500, 19999), + questionNumber: getRandomIntBetween(12600, 16999), + resolveNumber: getRandomIntBetween(13500, 17999), + satisfaction: getRandomIntBetween(95, 100), + date: dayjs().subtract(index, "day").format("YYYY-MM-DD") + }; +}); + +/** 最新动态 */ +const latestNewsData = cloneDeep(tableData) + .slice(0, 14) + .map((item, index) => { + return Object.assign(item, { + date: `${dayjs().subtract(index, "day").format("YYYY-MM-DD")} ${ + days[dayjs().subtract(index, "day").day()] + }` + }); + }); + +export { chartData, barChartData, progressData, tableData, latestNewsData }; diff --git a/src/views/welcome/index.vue b/src/views/welcome/index.vue index b82b011..8e811aa 100644 --- a/src/views/welcome/index.vue +++ b/src/views/welcome/index.vue @@ -1,9 +1,276 @@ + + diff --git a/src/views/welcome/utils.ts b/src/views/welcome/utils.ts new file mode 100644 index 0000000..7708a7e --- /dev/null +++ b/src/views/welcome/utils.ts @@ -0,0 +1,6 @@ +export { default as dayjs } from "dayjs"; +export { useDark, cloneDeep, randomGradient } from "@pureadmin/utils"; + +export function getRandomIntBetween(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min; +}