修改
This commit is contained in:
@@ -6,9 +6,9 @@ export default defineFakeRoute([
|
||||
url: "/login",
|
||||
method: "post",
|
||||
response: ({ body }) => {
|
||||
if (body.username === "admin") {
|
||||
if (body.account === "admin" || body.username === "admin") {
|
||||
return {
|
||||
success: true,
|
||||
code: 0,
|
||||
data: {
|
||||
avatar: "https://avatars.githubusercontent.com/u/44761321",
|
||||
username: "admin",
|
||||
@@ -17,23 +17,27 @@ export default defineFakeRoute([
|
||||
roles: ["admin"],
|
||||
// 按钮级别权限
|
||||
permissions: ["*:*:*"],
|
||||
accessToken: "eyJhbGciOiJIUzUxMiJ9.admin",
|
||||
refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh",
|
||||
expires: "2030/10/30 00:00:00"
|
||||
accessToken:
|
||||
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.mock-admin-payload.mock-admin-signature",
|
||||
refreshToken:
|
||||
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.mock-admin-refresh.mock-admin-refresh-signature",
|
||||
expireAtUtc: "2030-10-30T00:00:00Z"
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
code: 0,
|
||||
data: {
|
||||
avatar: "https://avatars.githubusercontent.com/u/52823142",
|
||||
username: "common",
|
||||
nickname: "小林",
|
||||
roles: ["common"],
|
||||
permissions: ["permission:btn:add", "permission:btn:edit"],
|
||||
accessToken: "eyJhbGciOiJIUzUxMiJ9.common",
|
||||
refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh",
|
||||
expires: "2030/10/30 00:00:00"
|
||||
accessToken:
|
||||
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.mock-common-payload.mock-common-signature",
|
||||
refreshToken:
|
||||
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.mock-common-refresh.mock-common-refresh-signature",
|
||||
expireAtUtc: "2030-10-30T00:00:00Z"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,17 +8,20 @@ export default defineFakeRoute([
|
||||
response: ({ body }) => {
|
||||
if (body.refreshToken) {
|
||||
return {
|
||||
success: true,
|
||||
code: 0,
|
||||
data: {
|
||||
accessToken: "eyJhbGciOiJIUzUxMiJ9.newAdmin",
|
||||
refreshToken: "eyJhbGciOiJIUzUxMiJ9.newAdminRefresh",
|
||||
accessToken:
|
||||
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.mock-refresh-payload.mock-refresh-signature",
|
||||
refreshToken:
|
||||
"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.mock-refresh-token.mock-refresh-signature",
|
||||
// `expires`选择这种日期格式是为了方便调试,后端直接设置时间戳或许更方便(每次都应该递增)。如果后端返回的是时间戳格式,前端开发请来到这个目录`src/utils/auth.ts`,把第`38`行的代码换成expires = data.expires即可。
|
||||
expires: "2030/10/30 23:59:59"
|
||||
expireAtUtc: "2030-10-30T23:59:59Z"
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
code: 1,
|
||||
message: "refresh token missing",
|
||||
data: {}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ export interface SendLoginCodeParams {
|
||||
account: string;
|
||||
}
|
||||
|
||||
export interface PageParams {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface AuthData {
|
||||
token?: string;
|
||||
accessToken?: string;
|
||||
@@ -24,6 +29,20 @@ export interface AuthData {
|
||||
permissions?: Array<string>;
|
||||
}
|
||||
|
||||
type ResultTable = {
|
||||
code: number;
|
||||
data?: {
|
||||
/** 列表数据 */
|
||||
list: Array<any>;
|
||||
/** 总条目数 */
|
||||
total?: number;
|
||||
/** 每页显示条目个数 */
|
||||
pageSize?: number;
|
||||
/** 当前页数 */
|
||||
page?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type UserResult = ApiResult<AuthData>;
|
||||
export type RefreshTokenResult = ApiResult<AuthData>;
|
||||
export type SendLoginCodeResult = ApiResult<null>;
|
||||
@@ -43,3 +62,9 @@ export const refreshTokenApi = (data?: object) => {
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserListApi = (data: PageParams) => {
|
||||
return http.request<ResultTable>("post", "user/list", {
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
2
src/components/ReCountTo/README.md
Normal file
2
src/components/ReCountTo/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
normal 普通数字动画组件
|
||||
rebound 回弹式数字动画组件
|
||||
11
src/components/ReCountTo/index.ts
Normal file
11
src/components/ReCountTo/index.ts
Normal 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 };
|
||||
179
src/components/ReCountTo/src/normal/index.tsx
Normal file
179
src/components/ReCountTo/src/normal/index.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import {
|
||||
watch,
|
||||
unref,
|
||||
computed,
|
||||
reactive,
|
||||
onMounted,
|
||||
defineComponent
|
||||
} 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
|
||||
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
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
32
src/components/ReCountTo/src/normal/props.ts
Normal file
32
src/components/ReCountTo/src/normal/props.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
72
src/components/ReCountTo/src/rebound/index.tsx
Normal file
72
src/components/ReCountTo/src/rebound/index.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import "./rebound.css";
|
||||
import {
|
||||
ref,
|
||||
unref,
|
||||
onBeforeMount,
|
||||
defineComponent,
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
15
src/components/ReCountTo/src/rebound/props.ts
Normal file
15
src/components/ReCountTo/src/rebound/props.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
77
src/components/ReCountTo/src/rebound/rebound.css
Normal file
77
src/components/ReCountTo/src/rebound/rebound.css
Normal 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);
|
||||
}
|
||||
}
|
||||
39
src/components/ReFlicker/index.css
Normal file
39
src/components/ReFlicker/index.css
Normal 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;
|
||||
}
|
||||
}
|
||||
44
src/components/ReFlicker/index.ts
Normal file
44
src/components/ReFlicker/index.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import "./index.css";
|
||||
import { type Component, h, defineComponent } 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: () => []
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
28
src/router/modules/activity.ts
Normal file
28
src/router/modules/activity.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export default {
|
||||
path: "/activity",
|
||||
redirect: "/activity/list",
|
||||
meta: {
|
||||
icon: "ri:gift-2-line",
|
||||
// showLink: false,
|
||||
title: "活动管理",
|
||||
rank: 3
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/activity/list",
|
||||
name: "活动列表",
|
||||
component: () => import("@/views/user/index.vue"),
|
||||
meta: {
|
||||
title: "玩家列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/activity/404",
|
||||
name: "风险玩家",
|
||||
component: () => import("@/views/user/index.vue"),
|
||||
meta: {
|
||||
title: "风险玩家"
|
||||
}
|
||||
}
|
||||
]
|
||||
} satisfies RouteConfigsTable;
|
||||
44
src/router/modules/user.ts
Normal file
44
src/router/modules/user.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export default {
|
||||
path: "/user",
|
||||
redirect: "/user/list",
|
||||
meta: {
|
||||
icon: "ri:account-circle-2-line",
|
||||
// showLink: false,
|
||||
title: "玩家管理",
|
||||
rank: 2
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: "/user/list",
|
||||
name: "玩家列表",
|
||||
component: () => import("@/views/user/index.vue"),
|
||||
meta: {
|
||||
title: "玩家列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/user/dangerous",
|
||||
name: "风险玩家",
|
||||
component: () => import("@/views/user/index.vue"),
|
||||
meta: {
|
||||
title: "风险玩家"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/user/banned",
|
||||
name: "封禁列表",
|
||||
component: () => import("@/views/user/index.vue"),
|
||||
meta: {
|
||||
title: "封禁列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/user/club",
|
||||
name: "玩家公会",
|
||||
component: () => import("@/views/user/index.vue"),
|
||||
meta: {
|
||||
title: "公会列表"
|
||||
}
|
||||
}
|
||||
]
|
||||
} satisfies RouteConfigsTable;
|
||||
@@ -29,6 +29,12 @@ const defaultConfig: AxiosRequestConfig = {
|
||||
}
|
||||
};
|
||||
|
||||
const whiteList = ["/refresh-token", "/login", "/send-login-code"];
|
||||
|
||||
function resolveAccessToken(tokenData: ReturnType<typeof getToken>) {
|
||||
return tokenData?.accessToken || tokenData?.token || "";
|
||||
}
|
||||
|
||||
class PureHttp {
|
||||
constructor() {
|
||||
this.httpInterceptorsRequest();
|
||||
@@ -51,6 +57,7 @@ class PureHttp {
|
||||
private static retryOriginalRequest(config: PureHttpRequestConfig) {
|
||||
return new Promise(resolve => {
|
||||
PureHttp.requests.push((token: string) => {
|
||||
config.headers = config.headers ?? {};
|
||||
config.headers["Authorization"] = formatToken(token);
|
||||
resolve(config);
|
||||
});
|
||||
@@ -61,24 +68,23 @@ class PureHttp {
|
||||
private httpInterceptorsRequest(): void {
|
||||
PureHttp.axiosInstance.interceptors.request.use(
|
||||
async (config: PureHttpRequestConfig): Promise<any> => {
|
||||
config.headers = config.headers ?? {};
|
||||
// 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调
|
||||
if (typeof config.beforeRequestCallback === "function") {
|
||||
config.beforeRequestCallback(config);
|
||||
return config;
|
||||
}
|
||||
if (PureHttp.initConfig.beforeRequestCallback) {
|
||||
PureHttp.initConfig.beforeRequestCallback(config);
|
||||
return config;
|
||||
}
|
||||
/** 请求白名单,放置一些不需要`token`的接口(通过设置请求白名单,防止`token`过期后再请求造成的死循环问题) */
|
||||
const whiteList = ["/refresh-token", "/login"];
|
||||
return whiteList.some(url => config.url.endsWith(url))
|
||||
return whiteList.some(url => config.url?.endsWith(url))
|
||||
? config
|
||||
: new Promise(resolve => {
|
||||
const data = getToken();
|
||||
if (data) {
|
||||
const accessToken = resolveAccessToken(data);
|
||||
if (data && accessToken) {
|
||||
const now = new Date().getTime();
|
||||
const expired = parseInt(data.expires) - now <= 0;
|
||||
const expired = Number(data.expires) - now <= 0;
|
||||
if (expired) {
|
||||
if (!PureHttp.isRefreshing) {
|
||||
PureHttp.isRefreshing = true;
|
||||
@@ -86,7 +92,8 @@ class PureHttp {
|
||||
useUserStoreHook()
|
||||
.handRefreshToken({ refreshToken: data.refreshToken })
|
||||
.then(res => {
|
||||
const token = res.data.accessToken;
|
||||
const token =
|
||||
res.data.accessToken || res.data.token || "";
|
||||
config.headers["Authorization"] = formatToken(token);
|
||||
PureHttp.requests.forEach(cb => cb(token));
|
||||
PureHttp.requests = [];
|
||||
@@ -97,9 +104,7 @@ class PureHttp {
|
||||
}
|
||||
resolve(PureHttp.retryOriginalRequest(config));
|
||||
} else {
|
||||
config.headers["Authorization"] = formatToken(
|
||||
data.accessToken
|
||||
);
|
||||
config.headers["Authorization"] = formatToken(accessToken);
|
||||
resolve(config);
|
||||
}
|
||||
} else {
|
||||
|
||||
297
src/views/user/data.ts
Normal file
297
src/views/user/data.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import dayjs from "dayjs";
|
||||
import { clone } from "@pureadmin/utils";
|
||||
|
||||
const date = dayjs(new Date()).format("YYYY-MM-DD");
|
||||
|
||||
const tableData = [
|
||||
{
|
||||
date,
|
||||
name: "Tom",
|
||||
address: "No. 189, Grove St, Los Angeles"
|
||||
},
|
||||
{
|
||||
date,
|
||||
name: "Jack",
|
||||
address: "No. 189, Grove St, Los Angeles"
|
||||
},
|
||||
{
|
||||
date,
|
||||
name: "Dick",
|
||||
address: "No. 189, Grove St, Los Angeles"
|
||||
},
|
||||
{
|
||||
date,
|
||||
name: "Harry",
|
||||
address: "No. 189, Grove St, Los Angeles"
|
||||
},
|
||||
{
|
||||
date,
|
||||
name: "Sam",
|
||||
address: "No. 189, Grove St, Los Angeles"
|
||||
},
|
||||
{
|
||||
date,
|
||||
name: "Lucy",
|
||||
address: "No. 189, Grove St, Los Angeles"
|
||||
},
|
||||
{
|
||||
date,
|
||||
name: "Mary",
|
||||
address: "No. 189, Grove St, Los Angeles"
|
||||
},
|
||||
{
|
||||
date,
|
||||
name: "Mike",
|
||||
address: "No. 189, Grove St, Los Angeles"
|
||||
}
|
||||
];
|
||||
|
||||
const cloneData = clone(tableData, true);
|
||||
|
||||
const tableDataMore = cloneData.map(item =>
|
||||
Object.assign(item, {
|
||||
state: "California",
|
||||
city: "Los Angeles",
|
||||
"post-code": "CA 90036"
|
||||
})
|
||||
);
|
||||
|
||||
const tableDataImage = cloneData.map((item, index) =>
|
||||
Object.assign(item, {
|
||||
image: `https://pure-admin.github.io/pure-admin-table/imgs/${index + 1}.jpg`
|
||||
})
|
||||
);
|
||||
|
||||
const tableDataSortable = cloneData.map((item, index) =>
|
||||
Object.assign(item, {
|
||||
date: `${dayjs(new Date()).format("YYYY-MM")}-${index + 1}`
|
||||
})
|
||||
);
|
||||
|
||||
const tableDataExpand = [
|
||||
{
|
||||
date: "2016-05-03",
|
||||
name: "Tom",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114",
|
||||
family: [
|
||||
{
|
||||
name: "Jerry",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
},
|
||||
{
|
||||
name: "Spike",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
},
|
||||
{
|
||||
name: "Tyke",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
date: "2016-05-02",
|
||||
name: "Tom",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114",
|
||||
family: [
|
||||
{
|
||||
name: "Jerry",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
},
|
||||
{
|
||||
name: "Spike",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
},
|
||||
{
|
||||
name: "Tyke",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
date: "2016-05-04",
|
||||
name: "Tom",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114",
|
||||
family: [
|
||||
{
|
||||
name: "Jerry",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
},
|
||||
{
|
||||
name: "Spike",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
},
|
||||
{
|
||||
name: "Tyke",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
date: "2016-05-01",
|
||||
name: "Tom",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114",
|
||||
family: [
|
||||
{
|
||||
name: "Jerry",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
},
|
||||
{
|
||||
name: "Spike",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
},
|
||||
{
|
||||
name: "Tyke",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
date: "2016-05-08",
|
||||
name: "Tom",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114",
|
||||
family: [
|
||||
{
|
||||
name: "Jerry",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
},
|
||||
{
|
||||
name: "Spike",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
},
|
||||
{
|
||||
name: "Tyke",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
date: "2016-05-06",
|
||||
name: "Tom",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114",
|
||||
family: [
|
||||
{
|
||||
name: "Jerry",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
},
|
||||
{
|
||||
name: "Spike",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
},
|
||||
{
|
||||
name: "Tyke",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
date: "2016-05-07",
|
||||
name: "Tom",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114",
|
||||
family: [
|
||||
{
|
||||
name: "Jerry",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
},
|
||||
{
|
||||
name: "Spike",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
},
|
||||
{
|
||||
name: "Tyke",
|
||||
state: "California",
|
||||
city: "San Francisco",
|
||||
address: "3650 21st St, San Francisco",
|
||||
zip: "CA 94114"
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
export {
|
||||
tableData,
|
||||
tableDataMore,
|
||||
tableDataImage,
|
||||
tableDataExpand,
|
||||
tableDataSortable
|
||||
};
|
||||
201
src/views/user/hook.tsx
Normal file
201
src/views/user/hook.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import dayjs from "dayjs";
|
||||
import Detail from "./detail.vue";
|
||||
import { message } from "@/utils/message";
|
||||
import userAvatar from "@/assets/user.jpg";
|
||||
import { addDialog } from "@/components/ReDialog";
|
||||
import type { PaginationProps } from "@pureadmin/table";
|
||||
import { type Ref, reactive, ref, onMounted, toRaw } from "vue";
|
||||
import { getKeyList, useCopyToClipboard } from "@pureadmin/utils";
|
||||
import { getUserListApi } from "@/api/user";
|
||||
// import { getSystemLogsList, getSystemLogsDetail } from "@/api/system";
|
||||
|
||||
export function useRole(tableRef: Ref) {
|
||||
const form = reactive({
|
||||
module: "",
|
||||
requestTime: ""
|
||||
});
|
||||
const dataList = ref([]);
|
||||
const loading = ref(true);
|
||||
const selectedNum = ref(0);
|
||||
const { copied, update } = useCopyToClipboard();
|
||||
|
||||
const pagination = reactive<PaginationProps>({
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
background: true
|
||||
});
|
||||
|
||||
const columns: TableColumnList = [
|
||||
{
|
||||
label: "ID",
|
||||
prop: "id"
|
||||
},
|
||||
{
|
||||
label: "用户头像",
|
||||
prop: "avatar",
|
||||
cellRenderer: ({ row }) => (
|
||||
<el-image
|
||||
fit="cover"
|
||||
preview-teleported={true}
|
||||
src={row.avatar || userAvatar}
|
||||
preview-src-list={Array.of(row.avatar || userAvatar)}
|
||||
class="size-6 rounded-full align-middle"
|
||||
/>
|
||||
),
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
label: "昵称",
|
||||
prop: "nickName"
|
||||
},
|
||||
{
|
||||
label: "国家",
|
||||
prop: "country",
|
||||
minWidth: 100
|
||||
},
|
||||
{
|
||||
label: "等级",
|
||||
prop: "level",
|
||||
minWidth: 90
|
||||
},
|
||||
{
|
||||
label: "上星",
|
||||
prop: "star",
|
||||
minWidth: 90
|
||||
},
|
||||
{
|
||||
label: "高光",
|
||||
prop: "highlight",
|
||||
minWidth: 90
|
||||
},
|
||||
{
|
||||
label: "VIP",
|
||||
prop: "vip",
|
||||
minWidth: 90
|
||||
},
|
||||
{
|
||||
label: "操作",
|
||||
fixed: "right",
|
||||
slot: "operation"
|
||||
}
|
||||
];
|
||||
|
||||
function handleOffline(row) {
|
||||
message(`${row.username}已被强制下线`, { type: "success" });
|
||||
onSearch();
|
||||
}
|
||||
|
||||
function handleSizeChange(val: number) {
|
||||
console.log(`${val} items per page`);
|
||||
}
|
||||
|
||||
function handleCurrentChange(val: number) {
|
||||
console.log(`current page: ${val}`);
|
||||
}
|
||||
|
||||
/** 当CheckBox选择项发生变化时会触发该事件 */
|
||||
function handleSelectionChange(val) {
|
||||
selectedNum.value = val.length;
|
||||
// 重置表格高度
|
||||
tableRef.value.setAdaptive();
|
||||
}
|
||||
|
||||
/** 取消选择 */
|
||||
function onSelectionCancel() {
|
||||
selectedNum.value = 0;
|
||||
// 用于多选表格,清空用户的选择
|
||||
tableRef.value.getTableRef().clearSelection();
|
||||
}
|
||||
|
||||
/** 拷贝请求接口,表格单元格被双击时触发 */
|
||||
function handleCellDblclick({ url }, { property }) {
|
||||
if (property !== "url") return;
|
||||
update(url);
|
||||
copied.value
|
||||
? message(`${url} 已拷贝`, { type: "success" })
|
||||
: message("拷贝失败", { type: "warning" });
|
||||
}
|
||||
|
||||
/** 批量删除 */
|
||||
function onbatchDel() {
|
||||
// 返回当前选中的行
|
||||
const curSelected = tableRef.value.getTableRef().getSelectionRows();
|
||||
// 接下来根据实际业务,通过选中行的某项数据,比如下面的id,调用接口进行批量删除
|
||||
message(`已删除序号为 ${getKeyList(curSelected, "id")} 的数据`, {
|
||||
type: "success"
|
||||
});
|
||||
tableRef.value.getTableRef().clearSelection();
|
||||
onSearch();
|
||||
}
|
||||
|
||||
/** 清空日志 */
|
||||
function clearAll() {
|
||||
// 根据实际业务,调用接口删除所有日志数据
|
||||
message("已删除所有日志数据", {
|
||||
type: "success"
|
||||
});
|
||||
onSearch();
|
||||
}
|
||||
|
||||
function onDetail(row) {
|
||||
// getSystemLogsDetail({ id: row.id }).then(res => {
|
||||
// addDialog({
|
||||
// title: "系统日志详情",
|
||||
// fullscreen: true,
|
||||
// hideFooter: true,
|
||||
// contentRenderer: () => Detail,
|
||||
// props: {
|
||||
// data: [res]
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
console.log(row);
|
||||
}
|
||||
|
||||
async function onSearch() {
|
||||
loading.value = true;
|
||||
|
||||
const { code, data } = await getUserListApi({ page: 1, pageSize: 10 });
|
||||
if (code === 0) {
|
||||
dataList.value = data.list;
|
||||
pagination.total = data.total;
|
||||
pagination.pageSize = data.pageSize;
|
||||
pagination.currentPage = data.page;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 500);
|
||||
}
|
||||
|
||||
const resetForm = formEl => {
|
||||
if (!formEl) return;
|
||||
formEl.resetFields();
|
||||
onSearch();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
onSearch();
|
||||
});
|
||||
|
||||
return {
|
||||
form,
|
||||
loading,
|
||||
columns,
|
||||
dataList,
|
||||
pagination,
|
||||
selectedNum,
|
||||
onSearch,
|
||||
onDetail,
|
||||
clearAll,
|
||||
resetForm,
|
||||
onbatchDel,
|
||||
handleSizeChange,
|
||||
onSelectionCancel,
|
||||
handleCellDblclick,
|
||||
handleCurrentChange,
|
||||
handleSelectionChange,
|
||||
handleOffline
|
||||
};
|
||||
}
|
||||
186
src/views/user/index.vue
Normal file
186
src/views/user/index.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useRole } from "./hook";
|
||||
import { getPickerShortcuts } from "./utils";
|
||||
import { PureTableBar } from "@/components/RePureTableBar";
|
||||
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
|
||||
|
||||
import View from "~icons/ep/view";
|
||||
import Delete from "~icons/ep/delete";
|
||||
import Refresh from "~icons/ep/refresh";
|
||||
|
||||
import Plane from "~icons/ri/plane-line";
|
||||
|
||||
defineOptions({
|
||||
name: "SystemLog"
|
||||
});
|
||||
|
||||
const formRef = ref();
|
||||
const tableRef = ref();
|
||||
|
||||
const {
|
||||
form,
|
||||
loading,
|
||||
columns,
|
||||
dataList,
|
||||
pagination,
|
||||
selectedNum,
|
||||
onSearch,
|
||||
onDetail,
|
||||
clearAll,
|
||||
resetForm,
|
||||
onbatchDel,
|
||||
handleSizeChange,
|
||||
onSelectionCancel,
|
||||
handleCellDblclick,
|
||||
handleCurrentChange,
|
||||
handleSelectionChange,
|
||||
handleOffline
|
||||
} = useRole(tableRef);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:inline="true"
|
||||
:model="form"
|
||||
class="search-form bg-bg_color w-full pl-8 pt-3 overflow-auto"
|
||||
>
|
||||
<el-form-item label="所属模块" prop="module">
|
||||
<el-input
|
||||
v-model="form.module"
|
||||
placeholder="请输入所属模块"
|
||||
clearable
|
||||
class="w-42.5!"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="请求时间" prop="requestTime">
|
||||
<el-date-picker
|
||||
v-model="form.requestTime"
|
||||
:shortcuts="getPickerShortcuts()"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期时间"
|
||||
end-placeholder="结束日期时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:icon="useRenderIcon('ri:search-line')"
|
||||
:loading="loading"
|
||||
@click="onSearch"
|
||||
>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<PureTableBar title="玩家列表" :columns="columns" @refresh="onSearch">
|
||||
<template v-slot="{ size, dynamicColumns }">
|
||||
<div
|
||||
v-if="selectedNum > 0"
|
||||
v-motion-fade
|
||||
class="bg-(--el-fill-color-light) w-full h-11.5 mb-2 pl-4 flex items-center"
|
||||
>
|
||||
<div class="flex-auto">
|
||||
<span
|
||||
style="font-size: var(--el-font-size-base)"
|
||||
class="text-[rgba(42,46,54,0.5)] dark:text-[rgba(220,220,242,0.5)]"
|
||||
>
|
||||
已选 {{ selectedNum }} 项
|
||||
</span>
|
||||
<el-button type="primary" text @click="onSelectionCancel">
|
||||
取消选择
|
||||
</el-button>
|
||||
</div>
|
||||
<el-popconfirm title="是否确认删除?" @confirm="onbatchDel">
|
||||
<template #reference>
|
||||
<el-button type="danger" text class="mr-1!"> 批量删除 </el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
<pure-table
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
align-whole="center"
|
||||
table-layout="auto"
|
||||
:loading="loading"
|
||||
:size="size"
|
||||
adaptive
|
||||
:adaptiveConfig="{ offsetBottom: 108 }"
|
||||
:data="dataList"
|
||||
:columns="dynamicColumns"
|
||||
:pagination="{ ...pagination, size }"
|
||||
:header-cell-style="{
|
||||
background: 'var(--el-fill-color-light)',
|
||||
color: 'var(--el-text-color-primary)'
|
||||
}"
|
||||
@selection-change="handleSelectionChange"
|
||||
@page-size-change="handleSizeChange"
|
||||
@page-current-change="handleCurrentChange"
|
||||
@cell-dblclick="handleCellDblclick"
|
||||
>
|
||||
<template #operation="{ row }">
|
||||
<el-popconfirm
|
||||
:title="`是否强制下线${row.username}`"
|
||||
@confirm="handleOffline(row)"
|
||||
>
|
||||
<template #reference>
|
||||
<el-button
|
||||
class="reset-margin"
|
||||
link
|
||||
type="primary"
|
||||
:size="size"
|
||||
:icon="useRenderIcon(Plane)"
|
||||
>
|
||||
强退
|
||||
</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
<el-button
|
||||
class="reset-margin outline-hidden!"
|
||||
link
|
||||
type="primary"
|
||||
:size="size"
|
||||
:icon="useRenderIcon(Delete)"
|
||||
@click="onDetail(row)"
|
||||
>
|
||||
封号
|
||||
</el-button>
|
||||
<el-button
|
||||
class="reset-margin outline-hidden!"
|
||||
link
|
||||
type="primary"
|
||||
:size="size"
|
||||
:icon="useRenderIcon(View)"
|
||||
@click="onDetail(row)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
</template>
|
||||
</pure-table>
|
||||
</template>
|
||||
</PureTableBar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-dropdown-menu__item i) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin: 24px 24px 0 !important;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
129
src/views/user/utils.ts
Normal file
129
src/views/user/utils.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
/** 日期、时间选择器快捷选项,常搭配 [DatePicker](https://element-plus.org/zh-CN/component/date-picker.html) 和 [DateTimePicker](https://element-plus.org/zh-CN/component/datetime-picker.html) 的`shortcuts`属性使用 */
|
||||
export const getPickerShortcuts = (): Array<{
|
||||
text: string;
|
||||
value: Date | Function;
|
||||
}> => {
|
||||
return [
|
||||
{
|
||||
text: "今天",
|
||||
value: () => {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const todayEnd = new Date();
|
||||
todayEnd.setHours(23, 59, 59, 999);
|
||||
return [today, todayEnd];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "昨天",
|
||||
value: () => {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
yesterday.setHours(0, 0, 0, 0);
|
||||
const yesterdayEnd = new Date();
|
||||
yesterdayEnd.setDate(yesterdayEnd.getDate() - 1);
|
||||
yesterdayEnd.setHours(23, 59, 59, 999);
|
||||
return [yesterday, yesterdayEnd];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "前天",
|
||||
value: () => {
|
||||
const beforeYesterday = new Date();
|
||||
beforeYesterday.setDate(beforeYesterday.getDate() - 2);
|
||||
beforeYesterday.setHours(0, 0, 0, 0);
|
||||
const beforeYesterdayEnd = new Date();
|
||||
beforeYesterdayEnd.setDate(beforeYesterdayEnd.getDate() - 2);
|
||||
beforeYesterdayEnd.setHours(23, 59, 59, 999);
|
||||
return [beforeYesterday, beforeYesterdayEnd];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "本周",
|
||||
value: () => {
|
||||
const today = new Date();
|
||||
const startOfWeek = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate() - today.getDay() + (today.getDay() === 0 ? -6 : 1)
|
||||
);
|
||||
startOfWeek.setHours(0, 0, 0, 0);
|
||||
const endOfWeek = new Date(
|
||||
startOfWeek.getTime() +
|
||||
6 * 24 * 60 * 60 * 1000 +
|
||||
23 * 60 * 60 * 1000 +
|
||||
59 * 60 * 1000 +
|
||||
59 * 1000 +
|
||||
999
|
||||
);
|
||||
return [startOfWeek, endOfWeek];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "上周",
|
||||
value: () => {
|
||||
const today = new Date();
|
||||
const startOfLastWeek = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
today.getDate() - today.getDay() - 7 + (today.getDay() === 0 ? -6 : 1)
|
||||
);
|
||||
startOfLastWeek.setHours(0, 0, 0, 0);
|
||||
const endOfLastWeek = new Date(
|
||||
startOfLastWeek.getTime() +
|
||||
6 * 24 * 60 * 60 * 1000 +
|
||||
23 * 60 * 60 * 1000 +
|
||||
59 * 60 * 1000 +
|
||||
59 * 1000 +
|
||||
999
|
||||
);
|
||||
return [startOfLastWeek, endOfLastWeek];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "本月",
|
||||
value: () => {
|
||||
const today = new Date();
|
||||
const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
startOfMonth.setHours(0, 0, 0, 0);
|
||||
const endOfMonth = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth() + 1,
|
||||
0
|
||||
);
|
||||
endOfMonth.setHours(23, 59, 59, 999);
|
||||
return [startOfMonth, endOfMonth];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "上个月",
|
||||
value: () => {
|
||||
const today = new Date();
|
||||
const startOfLastMonth = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth() - 1,
|
||||
1
|
||||
);
|
||||
startOfLastMonth.setHours(0, 0, 0, 0);
|
||||
const endOfLastMonth = new Date(
|
||||
today.getFullYear(),
|
||||
today.getMonth(),
|
||||
0
|
||||
);
|
||||
endOfLastMonth.setHours(23, 59, 59, 999);
|
||||
return [startOfLastMonth, endOfLastMonth];
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "本年",
|
||||
value: () => {
|
||||
const today = new Date();
|
||||
const startOfYear = new Date(today.getFullYear(), 0, 1);
|
||||
startOfYear.setHours(0, 0, 0, 0);
|
||||
const endOfYear = new Date(today.getFullYear(), 11, 31);
|
||||
endOfYear.setHours(23, 59, 59, 999);
|
||||
return [startOfYear, endOfYear];
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
108
src/views/welcome/components/charts/ChartBar.vue
Normal file
108
src/views/welcome/components/charts/ChartBar.vue
Normal 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>
|
||||
62
src/views/welcome/components/charts/ChartLine.vue
Normal file
62
src/views/welcome/components/charts/ChartLine.vue
Normal 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>
|
||||
73
src/views/welcome/components/charts/ChartRound.vue
Normal file
73
src/views/welcome/components/charts/ChartRound.vue
Normal 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>
|
||||
3
src/views/welcome/components/charts/index.ts
Normal file
3
src/views/welcome/components/charts/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as ChartBar } from "./ChartBar.vue";
|
||||
export { default as ChartLine } from "./ChartLine.vue";
|
||||
export { default as ChartRound } from "./ChartRound.vue";
|
||||
104
src/views/welcome/components/table/columns.tsx
Normal file
104
src/views/welcome/components/table/columns.tsx
Normal 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 "~icons/ri/thumb-up-line";
|
||||
import Hearts from "~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-15">
|
||||
<span class="ml-auto mr-2">{row.satisfaction}%</span>
|
||||
<iconify-icon-offline
|
||||
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
|
||||
};
|
||||
}
|
||||
1
src/views/welcome/components/table/empty.svg
Normal file
1
src/views/welcome/components/table/empty.svg
Normal 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 |
71
src/views/welcome/components/table/index.vue
Normal file
71
src/views/welcome/components/table/index.vue
Normal 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
134
src/views/welcome/data.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { dayjs, cloneDeep, getRandomIntBetween } from "./utils";
|
||||
import GroupLine from "~icons/ri/group-line";
|
||||
import Question from "~icons/ri/question-answer-line";
|
||||
import CheckLine from "~icons/ri/chat-check-line";
|
||||
import Smile from "~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 };
|
||||
@@ -1,9 +1,283 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, markRaw } from "vue";
|
||||
import ReCol from "@/components/ReCol";
|
||||
import { useDark, randomGradient } from "./utils";
|
||||
import WelcomeTable from "./components/table/index.vue";
|
||||
import { ReNormalCountTo } from "@/components/ReCountTo";
|
||||
import { useRenderFlicker } from "@/components/ReFlicker";
|
||||
import { ChartBar, ChartLine, ChartRound } from "./components/charts";
|
||||
import Segmented, { type OptionsType } from "@/components/ReSegmented";
|
||||
import { chartData, barChartData, progressData, latestNewsData } from "./data";
|
||||
|
||||
defineOptions({
|
||||
name: "Welcome"
|
||||
});
|
||||
|
||||
const { isDark } = useDark();
|
||||
|
||||
let curWeek = ref(1); // 0上周、1本周
|
||||
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-4.5"
|
||||
: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="size-8 flex-c rounded-md"
|
||||
:style="{
|
||||
backgroundColor: isDark ? 'transparent' : item.bgColor
|
||||
}"
|
||||
>
|
||||
<IconifyIconOffline
|
||||
:icon="item.icon"
|
||||
:color="item.color"
|
||||
width="18"
|
||||
height="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>
|
||||
<ChartLine
|
||||
v-if="item.data.length > 1"
|
||||
class="w-1/2!"
|
||||
:color="item.color"
|
||||
:data="item.data"
|
||||
/>
|
||||
<ChartRound v-else class="w-1/2!" />
|
||||
</div>
|
||||
</el-card>
|
||||
</re-col>
|
||||
|
||||
<re-col
|
||||
v-motion
|
||||
class="mb-4.5"
|
||||
: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">
|
||||
<ChartBar
|
||||
:requireData="barChartData[curWeek].requireData"
|
||||
:questionData="barChartData[curWeek].questionData"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</re-col>
|
||||
|
||||
<re-col
|
||||
v-motion
|
||||
class="mb-4.5"
|
||||
: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-4.5"
|
||||
:value="18"
|
||||
:xs="24"
|
||||
:initial="{
|
||||
opacity: 0,
|
||||
y: 100
|
||||
}"
|
||||
:enter="{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
delay: 560
|
||||
}
|
||||
}"
|
||||
>
|
||||
<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">
|
||||
<WelcomeTable />
|
||||
</el-scrollbar>
|
||||
</el-card>
|
||||
</re-col>
|
||||
|
||||
<re-col
|
||||
v-motion
|
||||
class="mb-4.5"
|
||||
: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;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-timeline.is-start) {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin: 20px 20px 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
6
src/views/welcome/utils.ts
Normal file
6
src/views/welcome/utils.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user