perf: 首页

This commit is contained in:
pan 2024-03-12 17:29:30 +08:00
parent a63f991f1c
commit 10eb44ac33
20 changed files with 1302 additions and 4 deletions

View File

@ -0,0 +1,2 @@
normal 普通数字动画组件
rebound 回弹式数字动画组件

View File

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

View File

@ -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 () => (
<>
<span
style={{
color: props.color,
fontSize: props.fontSize
}}
>
{state.displayValue}
</span>
</>
);
}
});

View File

@ -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<number>,
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;
}
}
};

View File

@ -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 () => (
<>
<div
class="scroll-num"
style={{ "--i": props.i, "--delay": props.delay }}
>
<ul ref="ulRef" style={{ fontSize: "32px" }}>
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
<li>0</li>
</ul>
<svg width="0" height="0">
<filter id="blur">
<feGaussianBlur
in="SourceGraphic"
stdDeviation={`0 ${props.blur}`}
/>
</filter>
</svg>
</div>
</>
);
}
});

View File

@ -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<number>,
required: false,
default: 0,
validator(value: number) {
return value < 10 && value >= 0 && Number.isInteger(value);
}
}
};

View File

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

View File

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

View File

@ -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 050%
* @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: () => []
}
);
}
});
}

View File

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

View File

@ -0,0 +1,108 @@
<script setup lang="ts">
import { useDark, useECharts } from "@pureadmin/utils";
import { type PropType, ref, computed, watch, nextTick } from "vue";
const props = defineProps({
requireData: {
type: Array as PropType<Array<number>>,
default: () => []
},
questionData: {
type: Array as PropType<Array<number>>,
default: () => []
}
});
const { isDark } = useDark();
const theme = computed(() => (isDark.value ? "dark" : "light"));
const chartRef = ref();
const { setOptions } = useECharts(chartRef, {
theme
});
watch(
() => props,
async () => {
await nextTick(); // DOM
setOptions({
container: ".bar-card",
color: ["#41b6ff", "#e85f33"],
tooltip: {
trigger: "axis",
axisPointer: {
type: "none"
}
},
grid: {
top: "20px",
left: "50px",
right: 0
},
legend: {
data: ["需求人数", "提问数量"],
textStyle: {
color: "#606266",
fontSize: "0.875rem"
},
bottom: 0
},
xAxis: [
{
type: "category",
data: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"],
axisLabel: {
fontSize: "0.875rem"
},
axisPointer: {
type: "shadow"
}
}
],
yAxis: [
{
type: "value",
axisLabel: {
fontSize: "0.875rem"
},
splitLine: {
show: false // 线
}
// name: ": "
}
],
series: [
{
name: "需求人数",
type: "bar",
barWidth: 10,
itemStyle: {
color: "#41b6ff",
borderRadius: [10, 10, 0, 0]
},
data: props.requireData
},
{
name: "提问数量",
type: "bar",
barWidth: 10,
itemStyle: {
color: "#e86033ce",
borderRadius: [10, 10, 0, 0]
},
data: props.questionData
}
]
});
},
{
deep: true,
immediate: true
}
);
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 365px" />
</template>

View File

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

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { type PropType, ref, computed } from "vue";
import { useDark, useECharts } from "@pureadmin/utils";
const props = defineProps({
data: {
type: Array as PropType<Array<number>>,
default: () => []
},
color: {
type: String,
default: "#41b6ff"
}
});
const { isDark } = useDark();
const theme = computed(() => (isDark.value ? "dark" : "light"));
const chartRef = ref();
const { setOptions } = useECharts(chartRef, {
theme,
renderer: "svg"
});
setOptions({
container: ".line-card",
xAxis: {
type: "category",
show: false,
data: props.data
},
grid: {
top: "15px",
bottom: 0,
left: 0,
right: 0
},
yAxis: {
show: false,
type: "value"
},
series: [
{
data: props.data,
type: "line",
symbol: "none",
smooth: true,
color: props.color,
lineStyle: {
shadowOffsetY: 3,
shadowBlur: 7,
shadowColor: props.color
}
}
]
});
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 60px" />
</template>

View File

@ -0,0 +1,73 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { useDark, useECharts } from "@pureadmin/utils";
const { isDark } = useDark();
const theme = computed(() => (isDark.value ? "dark" : "light"));
const chartRef = ref();
const { setOptions } = useECharts(chartRef, {
theme,
renderer: "svg"
});
setOptions({
container: ".line-card",
title: {
text: "100%",
left: "47%",
top: "30%",
textAlign: "center",
textStyle: {
fontSize: "16",
fontWeight: 600
}
},
polar: {
radius: ["100%", "90%"],
center: ["50%", "50%"]
},
angleAxis: {
max: 100,
show: false
},
radiusAxis: {
type: "category",
show: true,
axisLabel: {
show: false
},
axisLine: {
show: false
},
axisTick: {
show: false
}
},
series: [
{
type: "bar",
roundCap: true,
barWidth: 2,
showBackground: true,
backgroundStyle: {
color: "#dfe7ef"
},
data: [100],
coordinateSystem: "polar",
color: "#7846e5",
itemStyle: {
shadowBlur: 2,
shadowColor: "#7846e5",
shadowOffsetX: 0,
shadowOffsetY: 0
}
}
]
});
</script>
<template>
<div ref="chartRef" style="width: 100%; height: 60px" />
</template>

View File

@ -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 }) => (
<div class="flex justify-center w-full">
<span class="flex items-center w-[60px]">
<span class="ml-auto mr-2">{row.satisfaction}%</span>
<iconifyIconOffline
icon={row.satisfaction > 98 ? Hearts : ThumbUp}
color="#e85f33"
/>
</span>
</div>
)
},
{
sortable: true,
label: "统计日期",
prop: "date"
},
{
label: "操作",
fixed: "right",
slot: "operation"
}
];
/** 分页配置 */
const pagination = reactive<PaginationProps>({
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
};
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" class="empty-icon" viewBox="0 0 1024 1024"><path d="M855.6 427.2H168.5c-12.7 0-24.4 6.9-30.6 18L4.4 684.7C1.5 689.9 0 695.8 0 701.8v287.1c0 19.4 15.7 35.1 35.1 35.1H989c19.4 0 35.1-15.7 35.1-35.1V701.8c0-6-1.5-11.8-4.4-17.1L886.2 445.2c-6.2-11.1-17.9-18-30.6-18M673.4 695.6c-16.5 0-30.8 11.5-34.3 27.7-12.7 58.5-64.8 102.3-127.2 102.3s-114.5-43.8-127.2-102.3c-3.5-16.1-17.8-27.7-34.3-27.7H119c-26.4 0-43.3-28-31.1-51.4l81.7-155.8c6.1-11.6 18-18.8 31.1-18.8h622.4c13 0 25 7.2 31.1 18.8l81.7 155.8c12.2 23.4-4.7 51.4-31.1 51.4zm146.5-486.1c-1-1.8-2.1-3.7-3.2-5.5-9.8-16.6-31.1-22.2-47.8-12.6L648.5 261c-17 9.8-22.7 31.6-12.6 48.4.9 1.4 1.7 2.9 2.5 4.4 9.5 17 31.2 22.8 48 13L807 257.3c16.7-9.7 22.4-31 12.9-47.8m-444.5 51.6L255 191.6c-16.7-9.6-38-4-47.8 12.6-1.1 1.8-2.1 3.6-3.2 5.5-9.5 16.8-3.8 38.1 12.9 47.8L337.3 327c16.9 9.7 38.6 4 48-13.1.8-1.5 1.7-2.9 2.5-4.4 10.2-16.8 4.5-38.6-12.4-48.4M512 239.3h2.5c19.5.3 35.5-15.5 35.5-35.1v-139c0-19.3-15.6-34.9-34.8-35.1h-6.4C489.6 30.3 474 46 474 65.2v139c0 19.5 15.9 35.4 35.5 35.1z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,71 @@
<script setup lang="ts">
import { useColumns } from "./columns";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
const { loading, columns, dataList, pagination, Empty, onCurrentChange } =
useColumns();
</script>
<template>
<pure-table
row-key="id"
alignWhole="center"
showOverflowTooltip
:loading="loading"
:loading-config="{ background: 'transparent' }"
:data="
dataList.slice(
(pagination.currentPage - 1) * pagination.pageSize,
pagination.currentPage * pagination.pageSize
)
"
:columns="columns"
:pagination="pagination"
@page-current-change="onCurrentChange"
>
<template #empty>
<el-empty description="暂无数据" :image-size="60">
<template #image>
<Empty />
</template>
</el-empty>
</template>
<template #operation="{ row }">
<el-button
plain
circle
size="small"
:title="`查看序号为${row.id}的详情`"
:icon="useRenderIcon('ri:search-line')"
/>
</template>
</pure-table>
</template>
<style lang="scss">
.pure-table-filter {
.el-table-filter__list {
min-width: 80px;
padding: 0;
li {
line-height: 28px;
}
}
}
</style>
<style lang="scss" scoped>
:deep(.el-table) {
--el-table-border: none;
--el-table-border-color: transparent;
.el-empty__description {
margin: 0;
}
.el-scrollbar__bar {
display: none;
}
}
</style>

134
src/views/welcome/data.ts Normal file
View File

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

View File

@ -1,9 +1,276 @@
<script setup lang="ts">
import { ref, markRaw } from "vue";
import ReCol from "@/components/ReCol";
import { useDark, randomGradient } from "./utils";
import PureTable from "./components/table/index.vue";
import { ReNormalCountTo } from "@/components/ReCountTo";
import { useRenderFlicker } from "@/components/ReFlicker";
import { barChart, lineChart, roundChart } from "./components/chart";
import Segmented, { type OptionsType } from "@/components/ReSegmented";
import { chartData, barChartData, progressData, latestNewsData } from "./data";
defineOptions({
name: "Welcome"
});
const { isDark } = useDark();
let curWeek = ref(1); // 01
const optionsBasis: Array<OptionsType> = [
{
label: "上周"
},
{
label: "本周"
}
];
</script>
<template>
<h1>Pure-Admin-Thin国际化版本</h1>
<div>
<el-row :gutter="24" justify="space-around">
<re-col
v-for="(item, index) in chartData"
:key="index"
v-motion
class="mb-[18px]"
:value="6"
:md="12"
:sm="12"
:xs="24"
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 80 * (index + 1)
}
}"
>
<el-card class="line-card" shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">
{{ item.name }}
</span>
<div
class="w-8 h-8 flex justify-center items-center rounded-md"
:style="{
backgroundColor: isDark ? 'transparent' : item.bgColor
}"
>
<IconifyIconOffline
:icon="item.icon"
:color="item.color"
width="18"
/>
</div>
</div>
<div class="flex justify-between items-start mt-3">
<div class="w-1/2">
<ReNormalCountTo
:duration="item.duration"
:fontSize="'1.6em'"
:startVal="100"
:endVal="item.value"
/>
<p class="font-medium text-green-500">{{ item.percent }}</p>
</div>
<lineChart
v-if="item.data.length > 1"
class="!w-1/2"
:color="item.color"
:data="item.data"
/>
<roundChart v-else class="!w-1/2" />
</div>
</el-card>
</re-col>
<re-col
v-motion
class="mb-[18px]"
:value="18"
:xs="24"
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 400
}
}"
>
<el-card class="bar-card" shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">分析概览</span>
<Segmented v-model="curWeek" :options="optionsBasis" />
</div>
<div class="flex justify-between items-start mt-3">
<barChart
:requireData="barChartData[curWeek].requireData"
:questionData="barChartData[curWeek].questionData"
/>
</div>
</el-card>
</re-col>
<re-col
v-motion
class="mb-[18px]"
:value="6"
:xs="24"
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 480
}
}"
>
<el-card shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">解决概率</span>
</div>
<div
v-for="(item, index) in progressData"
:key="index"
:class="[
'flex',
'justify-between',
'items-start',
index === 0 ? 'mt-8' : 'mt-[2.15rem]'
]"
>
<el-progress
:text-inside="true"
:percentage="item.percentage"
:stroke-width="21"
:color="item.color"
striped
striped-flow
:duration="item.duration"
/>
<span class="text-nowrap ml-2 text-text_color_regular text-sm">
{{ item.week }}
</span>
</div>
</el-card>
</re-col>
<re-col
v-motion
class="mb-[18px]"
:value="18"
:xs="24"
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 560
}
}"
>
<el-card shadow="never" class="h-[580px]">
<div class="flex justify-between">
<span class="text-md font-medium">数据统计</span>
</div>
<PureTable class="mt-3" />
</el-card>
</re-col>
<re-col
v-motion
class="mb-[18px]"
:value="6"
:xs="24"
:initial="{
opacity: 0,
y: 100
}"
:enter="{
opacity: 1,
y: 0,
transition: {
delay: 640
}
}"
>
<el-card shadow="never">
<div class="flex justify-between">
<span class="text-md font-medium">最新动态</span>
</div>
<el-scrollbar max-height="504" class="mt-3">
<el-timeline>
<el-timeline-item
v-for="(item, index) in latestNewsData"
:key="index"
center
placement="top"
:icon="
markRaw(
useRenderFlicker({
background: randomGradient({
randomizeHue: true
})
})
)
"
:timestamp="item.date"
>
<p class="text-text_color_regular text-sm">
{{
`新增 ${item.requiredNumber} 条问题,${item.resolveNumber} 条已解决`
}}
</p>
</el-timeline-item>
</el-timeline>
</el-scrollbar>
</el-card>
</re-col>
</el-row>
</div>
</template>
<style lang="scss" scoped>
:deep(.el-card) {
--el-card-border-color: none;
/* 解决概率进度条宽度 */
.el-progress--line {
width: 85%;
}
/* 解决概率进度条字体大小 */
.el-progress-bar__innerText {
font-size: 15px;
}
/* 隐藏 el-scrollbar 滚动条 */
.el-scrollbar__bar {
display: none;
}
/* el-timeline 每一项上下、左右边距 */
.el-timeline-item {
margin: 0 6px;
}
}
.main-content {
margin: 20px 20px 0 !important;
}
</style>

View File

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