feat: 新增首页卡片统计

This commit is contained in:
xlsea 2024-03-22 11:22:07 +08:00
parent 6b1cf17428
commit 8e405db674
24 changed files with 520 additions and 104 deletions

6
.env
View File

@ -1,8 +1,10 @@
VITE_BASE_URL=/
VITE_APP_TITLE=SoybeanAdmin
VITE_APP_TITLE=Easy Retry
VITE_APP_DESC=SoybeanAdmin is a fresh and elegant admin template
VITE_APP_DESC=A flexible, reliable, and fast platform for distributed task retry and distributed task scheduling.
VITE_APP_VERSION=v3.1.0
# the prefix of the icon name
VITE_ICON_PREFIX=icon

View File

@ -1,5 +1,5 @@
# backend service base url, prod environment
VITE_SERVICE_BASE_URL=https://mock.apifox.com/m1/3109515-0-default
VITE_SERVICE_BASE_URL=/proxy-default
# other backend service base url, prod environment
VITE_OTHER_SERVICE_BASE_URL= `{

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -2,12 +2,14 @@
defineOptions({
name: 'GlobalFooter'
});
const { VITE_APP_VERSION } = import.meta.env;
</script>
<template>
<DarkModeContainer class="h-full flex-center">
<a href="https://github.com/soybeanjs/soybean-admin/blob/main/LICENSE" target="_blank" rel="noopener noreferrer">
Copyright MIT © 2021 Soybean
<a href="https://www.easyretry.com/" target="_blank" rel="noopener noreferrer">
Copyright © 2024 Easy Retry {{ VITE_APP_VERSION }}
</a>
</DarkModeContainer>
</template>

View File

@ -16,9 +16,7 @@ const options = ref(
const onChange = (value: string) => {
localStg.set('namespaceId', value);
setTimeout(() => {
router.go(0);
}, 500);
router.go(0);
};
</script>

View File

@ -1,6 +1,7 @@
const local: App.I18n.Schema = {
system: {
title: 'SoybeanAdmin'
title: 'Easy Retry',
desc: 'A flexible, reliable, and fast platform for distributed task retry and distributed task scheduling.'
},
common: {
action: 'Action',
@ -35,6 +36,10 @@ const local: App.I18n.Schema = {
update: 'Update',
updateSuccess: 'Update Success',
userCenter: 'User Center',
success: 'Success',
fail: 'Fail',
stop: 'Stop',
running: 'Running',
yesOrNo: {
yes: 'Yes',
no: 'No'
@ -164,6 +169,7 @@ const local: App.I18n.Schema = {
confirmPasswordPlaceholder: 'Please enter password again',
codeLogin: 'Verification code login',
confirm: 'Confirm',
login: 'Login',
back: 'Back',
validateSuccess: 'Verification passed',
loginSuccess: 'Login successfully',
@ -212,8 +218,30 @@ const local: App.I18n.Schema = {
devDep: 'Development Dependency'
},
home: {
greeting: 'Good morning, {userName}, today is another day full of vitality!',
// 问候语
Greeting: '{userName}, welcome back.',
morningGreeting: 'Good morning, {userName}, today is another day full of vitality!',
bthGreeting: "Good morning, {userName}, how's work going? Don't be sedentary. Get up and walk around more often!",
noonGreeting: "Good noon, {userName}, it's lunchtime after a long morning at work!",
athGreeting: "Good afternoon, {userName}, it's easy to get sleepy in the late afternoon yet, time for a nap!",
duskGreeting:
"{userName}, it's evening, the view of the sunset outside the window is very beautiful, the most beautiful thing is the red sunset.",
eveningGreeting: 'Good evening, {userName}, how are you doing today? Please take care to rest early!',
earlyMorningGreeting: "{userName}, It's so late already. Get some rest. Good night.",
weatherDesc: 'Today is cloudy to clear, 20℃ - 25℃!',
// 卡片统计
retryTaskCount: 'Retry Task',
jobTaskCount: 'Job Task',
userCount: 'User',
retryTask: 'Retry Task',
retryTaskTip: 'Total task volume: retry/callback task volume',
jobTask: 'Job Task',
jobTaskTip: 'Success rate: total completion/total dispatch amount',
onlineServiceCount: 'Online Machine',
onlineServiceTip: 'Always online machines: the sum of clients and servers registered to the system',
workflow: 'Workflow',
workflowTip: 'Workflow Tip',
// ...
projectCount: 'Project Count',
todo: 'Todo',
message: 'Message',
@ -271,6 +299,18 @@ const local: App.I18n.Schema = {
disable: 'Disable'
}
},
machine: {
type: {
client: 'Client',
server: 'Server'
}
},
retryTask: {
status: {
maxRetryTimes: 'Max times',
pauseRetry: 'Pause'
}
},
role: {
title: 'Role List',
roleName: 'Role Name',

View File

@ -1,6 +1,7 @@
const local: App.I18n.Schema = {
system: {
title: 'Soybean 管理系统'
title: 'Easy Retry',
desc: '灵活,可靠和快速的分布式任务重试和分布式任务调度平台'
},
common: {
action: '操作',
@ -35,6 +36,10 @@ const local: App.I18n.Schema = {
update: '更新',
updateSuccess: '更新成功',
userCenter: '个人中心',
success: '成功',
fail: '失败',
stop: '停止',
running: '运行中',
yesOrNo: {
yes: '是',
no: '否'
@ -163,6 +168,7 @@ const local: App.I18n.Schema = {
passwordPlaceholder: '请输入密码',
confirmPasswordPlaceholder: '请再次输入密码',
codeLogin: '验证码登录',
login: '登录',
confirm: '确定',
back: '返回',
validateSuccess: '验证成功',
@ -212,8 +218,26 @@ const local: App.I18n.Schema = {
devDep: '开发依赖'
},
home: {
greeting: '早安,{userName}, 今天又是充满活力的一天!',
Greeting: '{userName},欢迎回来!',
morningGreeting: '早安,{userName},今天又是充满活力的一天!',
bthGreeting: '上午好,{userName},工作顺利吗,不要久坐,多起来走动走动哦!',
noonGreeting: '中午好,{userName},工作了一个上午,现在是午餐时间!',
athGreeting: '下午好,{userName},午后很容易犯困呢,是时候该打个盹了!',
duskGreeting: '{userName},傍晚了,窗外夕阳的景色很美丽呢,最美不过夕阳红~',
eveningGreeting: '晚上好,{userName},今天过得怎么样?请注意早点休息!',
earlyMorningGreeting: '{userName},已经这么晚了呀,早点休息吧,晚安~',
weatherDesc: '今日多云转晴20℃ - 25℃!',
retryTaskCount: '重试任务',
jobTaskCount: '定时任务',
userCount: '用户',
retryTask: '重试任务',
retryTaskTip: '总任务量: 重试/回调任务量',
jobTask: '定时任务',
jobTaskTip: '成功率:总完成/总调度量',
onlineServiceCount: '总在线机器',
onlineServiceTip: '总在线机器:注册到系统的客户端和服务端之和',
workflow: '工作流',
workflowTip: '工作流提示',
projectCount: '项目数',
todo: '待办',
message: '消息',
@ -271,6 +295,18 @@ const local: App.I18n.Schema = {
disable: '禁用'
}
},
machine: {
type: {
client: '客户端',
server: '服务端'
}
},
retryTask: {
status: {
maxRetryTimes: '最大重试次数',
pauseRetry: '暂停重试'
}
},
role: {
title: '角色列表',
roleName: '角色名称',

View File

@ -2,7 +2,7 @@
import { getRgbOfColor } from '@sa/utils';
import { $t } from '@/locales';
import { localStg } from '@/utils/storage';
import systemLogo from '@/assets/svg-icon/logo.svg?raw';
import systemLogo from '@/assets/svg-icon/full-logo.svg?raw';
export function setupLoading() {
const themeColor = localStg.get('themeColor') || '#646cff';
@ -18,7 +18,7 @@ export function setupLoading() {
'right-0 bottom-0 animate-delay-1500'
];
const logoWithClass = systemLogo.replace('<svg', `<svg class="size-128px text-primary"`);
const logoWithClass = systemLogo.replace('<svg', `<svg class="size-256px text-primary"`);
const dot = loadingClasses
.map(item => {
@ -34,7 +34,7 @@ export function setupLoading() {
${dot}
</div>
</div>
<h2 class="text-28px font-500 text-#646464">${$t('system.title')}</h2>
<h2 class="text-26px font-500 pt-32px text-#646464">${$t('system.desc')}</h2>
</div>`;
const app = document.getElementById('app');

View File

@ -0,0 +1,9 @@
import { request } from '../request';
/** Version */
export function fetchCardCount() {
return request<Api.Dashboard.CardCount>({
url: '/dashboard/task-retry-job',
method: 'get'
});
}

View File

@ -1,3 +1,5 @@
export * from './auth';
export * from './route';
export * from './system';
export * from './dashboard';
export * from './system-manage';

View File

@ -0,0 +1,9 @@
import { request } from '../request';
/** Version */
export function fetchVersion() {
return request<string>({
url: '/system/version',
method: 'get'
});
}

View File

@ -1,7 +1,7 @@
/** Default theme settings */
export const themeSettings: App.Theme.ThemeSetting = {
themeScheme: 'light',
themeColor: '#646cff',
themeColor: '#22aae3',
otherColor: {
info: '#2080f0',
success: '#52c41a',

42
src/typings/api.d.ts vendored
View File

@ -95,6 +95,48 @@ declare namespace Api {
}
}
/**
* namespace Dashboard
*
* backend api module: "dashboard"
*/
namespace Dashboard {
type CardCount = {
jobTask: JobTask;
retryTask: RetryTask;
retryTaskBarList: RetryTaskBarList[];
onLineService: OnlineService;
};
type OnlineService = {
total: number;
clientTotal: number;
serverTotal: number;
};
type RetryTaskBarList = {
x: string;
taskTotal: number;
};
type RetryTask = {
totalNum: number;
runningNum: number;
finishNum: number;
maxCountNum: number;
suspendNum: number;
};
type JobTask = {
successNum: number;
failNum: number;
cancelNum: number;
stopNum: number;
totalNum: number;
successRate: number;
};
}
/**
* namespace SystemManage
*

38
src/typings/app.d.ts vendored
View File

@ -247,6 +247,7 @@ declare namespace App {
type Schema = {
system: {
title: string;
desc: string;
};
common: {
action: string;
@ -281,6 +282,10 @@ declare namespace App {
update: string;
updateSuccess: string;
userCenter: string;
success: string;
fail: string;
stop: string;
running: string;
yesOrNo: {
yes: string;
no: string;
@ -346,6 +351,7 @@ declare namespace App {
passwordPlaceholder: string;
confirmPasswordPlaceholder: string;
codeLogin: string;
login: string;
confirm: string;
back: string;
validateSuccess: string;
@ -395,7 +401,25 @@ declare namespace App {
devDep: string;
};
home: {
greeting: string;
Greeting: string;
morningGreeting: string;
bthGreeting: string;
noonGreeting: string;
athGreeting: string;
duskGreeting: string;
eveningGreeting: string;
earlyMorningGreeting: string;
retryTaskCount: string;
jobTaskCount: string;
userCount: string;
retryTask: string;
retryTaskTip: string;
jobTask: string;
jobTaskTip: string;
onlineServiceCount: string;
onlineServiceTip: string;
workflow: string;
workflowTip: string;
weatherDesc: string;
projectCount: string;
todo: string;
@ -454,6 +478,18 @@ declare namespace App {
disable: string;
};
};
machine: {
type: {
client: string;
server: string;
};
};
retryTask: {
status: {
maxRetryTimes: string;
pauseRetry: string;
};
};
role: {
title: string;
roleName: string;

View File

@ -16,14 +16,21 @@ declare module 'vue' {
FullScreen: typeof import('./../components/common/full-screen.vue')['default']
IconAntDesignEnterOutlined: typeof import('~icons/ant-design/enter-outlined')['default']
IconAntDesignReloadOutlined: typeof import('~icons/ant-design/reload-outlined')['default']
IconAntDesignSettingOutlined: typeof import('~icons/ant-design/setting-outlined')['default']
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
IconIcRoundDelete: typeof import('~icons/ic/round-delete')['default']
IconIcRoundPlus: typeof import('~icons/ic/round-plus')['default']
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
IconLocalBanner: typeof import('~icons/local/banner')['default']
IconLocalLogo: typeof import('~icons/local/logo')['default']
IconMdiArrowDownThin: typeof import('~icons/mdi/arrow-down-thin')['default']
IconMdiArrowUpThin: typeof import('~icons/mdi/arrow-up-thin')['default']
IconMdiDrag: typeof import('~icons/mdi/drag')['default']
IconMdiKeyboardEsc: typeof import('~icons/mdi/keyboard-esc')['default']
IconMdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
IconMdiRefresh: typeof import('~icons/mdi/refresh')['default']
IconUilSearch: typeof import('~icons/uil/search')['default']
LangSwitch: typeof import('./../components/common/lang-switch.vue')['default']
LookForward: typeof import('./../components/custom/look-forward.vue')['default']
@ -34,6 +41,9 @@ declare module 'vue' {
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NDataTable: typeof import('naive-ui')['NDataTable']
NDescriptions: typeof import('naive-ui')['NDescriptions']
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
NDivider: typeof import('naive-ui')['NDivider']
NDrawer: typeof import('naive-ui')['NDrawer']
@ -42,6 +52,7 @@ declare module 'vue' {
NEmpty: typeof import('naive-ui')['NEmpty']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NInput: typeof import('naive-ui')['NInput']
@ -54,13 +65,21 @@ declare module 'vue' {
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NPopover: typeof import('naive-ui')['NPopover']
NProgress: typeof import('naive-ui')['NProgress']
NRadio: typeof import('naive-ui')['NRadio']
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSkeleton: typeof import('naive-ui')['NSkeleton']
NSpace: typeof import('naive-ui')['NSpace']
NSpin: typeof import('naive-ui')['NSpin']
NStatistic: typeof import('naive-ui')['NStatistic']
NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab']
NTabs: typeof import('naive-ui')['NTabs']
NTag: typeof import('naive-ui')['NTag']
NThing: typeof import('naive-ui')['NThing']
NTooltip: typeof import('naive-ui')['NTooltip']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']

View File

@ -15,6 +15,8 @@ declare namespace Env {
readonly VITE_APP_TITLE: string;
/** The description of the application */
readonly VITE_APP_DESC: string;
/** The version of the application */
readonly VITE_APP_VERSION: string;
/** The router history mode */
readonly VITE_ROUTER_HISTORY_MODE?: RouterHistoryMode;
/** The prefix of the iconify icon */

View File

@ -3,14 +3,11 @@ import { computed } from 'vue';
import type { Component } from 'vue';
import { getColorPalette, mixColor } from '@sa/utils';
import { $t } from '@/locales';
import GlobalFooter from '@/layouts/modules/global-footer/index.vue';
import { useAppStore } from '@/store/modules/app';
import { useThemeStore } from '@/store/modules/theme';
import { loginModuleRecord } from '@/constants/app';
import PwdLogin from './modules/pwd-login.vue';
import CodeLogin from './modules/code-login.vue';
import Register from './modules/register.vue';
import ResetPwd from './modules/reset-pwd.vue';
import BindWechat from './modules/bind-wechat.vue';
interface Props {
/** The login module */
@ -21,6 +18,8 @@ const props = withDefaults(defineProps<Props>(), {
module: 'pwd-login'
});
const { VITE_APP_VERSION } = import.meta.env;
const appStore = useAppStore();
const themeStore = useThemeStore();
@ -30,13 +29,7 @@ interface LoginModule {
component: Component;
}
const modules: LoginModule[] = [
{ key: 'pwd-login', label: loginModuleRecord['pwd-login'], component: PwdLogin },
{ key: 'code-login', label: loginModuleRecord['code-login'], component: CodeLogin },
{ key: 'register', label: loginModuleRecord.register, component: Register },
{ key: 'reset-pwd', label: loginModuleRecord['reset-pwd'], component: ResetPwd },
{ key: 'bind-wechat', label: loginModuleRecord['bind-wechat'], component: BindWechat }
];
const modules: LoginModule[] = [{ key: 'pwd-login', label: loginModuleRecord['pwd-login'], component: PwdLogin }];
const activeModule = computed(() => {
const findItem = modules.find(item => item.key === props.module);
@ -63,7 +56,10 @@ const bgColor = computed(() => {
<div class="w-400px <sm:w-300px">
<header class="flex-y-center justify-between">
<SystemLogo class="text-64px text-primary <sm:text-48px" />
<h3 class="text-28px text-primary font-500 <sm:text-22px">{{ $t('system.title') }}</h3>
<h3 class="flex text-28px text-primary font-500 <sm:text-22px">
{{ $t('system.title') }}
<span class="mt-3px pl-12px text-16px color-#00000072 font-600">{{ VITE_APP_VERSION }}</span>
</h3>
<div class="i-flex-vertical">
<ThemeSchemaSwitch
:theme-schema="themeStore.themeScheme"
@ -80,12 +76,18 @@ const bgColor = computed(() => {
</div>
</header>
<main class="pt-24px">
<h3 class="text-18px text-primary font-medium">{{ $t(activeModule.label) }}</h3>
<div class="pt-24px">
<!-- <h3 class="text-18px text-primary font-medium">{{ $t(activeModule.label) }}</h3> -->
<div class="pt-12px">
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
<component :is="activeModule.component" />
</Transition>
</div>
<div class="pt-12px text-center">
<ButtonIcon tooltip-content="Mail" class="color-#272636 dark:color-#f0f2f5" icon="simple-icons:maildotru" />
<ButtonIcon class="color-#c71d23" tooltip-content="Gitee" icon="simple-icons:gitee" />
<ButtonIcon tooltip-content="Github" class="color-#010409 dark:color-#e6edf3" icon="simple-icons:github" />
</div>
<GlobalFooter />
</main>
</div>
</NCard>

View File

@ -2,8 +2,6 @@
import { computed, reactive } from 'vue';
import { Md5 } from 'ts-md5';
import { $t } from '@/locales';
import { loginModuleRecord } from '@/constants/app';
import { useRouterPush } from '@/hooks/common/router';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { useAuthStore } from '@/store/modules/auth';
@ -12,7 +10,6 @@ defineOptions({
});
const authStore = useAuthStore();
const { toggleLoginModule } = useRouterPush();
const { formRef, validate } = useNaiveForm();
interface FormModel {
@ -56,21 +53,9 @@ async function handleSubmit() {
/>
</NFormItem>
<NSpace vertical :size="24">
<div class="flex-y-center justify-between">
<NCheckbox>{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>
<NButton quaternary>{{ $t('page.login.pwdLogin.forgetPassword') }}</NButton>
</div>
<NButton type="primary" size="large" round block :loading="authStore.loginLoading" @click="handleSubmit">
{{ $t('common.confirm') }}
{{ $t('page.login.common.login') }}
</NButton>
<div class="flex-y-center justify-between gap-12px">
<NButton class="flex-1" block @click="toggleLoginModule('code-login')">
{{ $t(loginModuleRecord['code-login']) }}
</NButton>
<NButton class="flex-1" block @click="toggleLoginModule('register')">
{{ $t(loginModuleRecord.register) }}
</NButton>
</div>
</NSpace>
</NForm>
</template>

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { fetchCardCount } from '@/service/api';
import HeaderBanner from './modules/header-banner.vue';
import CardData from './modules/card-data.vue';
import LineChart from './modules/line-chart.vue';
@ -11,12 +12,23 @@ import CreativityBanner from './modules/creativity-banner.vue';
const appStore = useAppStore();
const gap = computed(() => (appStore.isMobile ? 0 : 16));
const cardCount = ref<Api.Dashboard.CardCount>();
const getCardData = async () => {
const { data: cardData, error } = await fetchCardCount();
if (!error) {
cardCount.value = cardData;
}
};
getCardData();
</script>
<template>
<NSpace vertical :size="16">
<HeaderBanner />
<CardData />
<CardData class="h-165px" :model-value="cardCount!" />
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:14">
<NCard :bordered="false" class="card-wrapper">

View File

@ -7,62 +7,138 @@ defineOptions({
name: 'CardData'
});
interface Props {
modelValue: Api.Dashboard.CardCount;
}
const props = defineProps<Props>();
interface CardData {
key: string;
title: string;
tip: string;
value: number;
unit: string;
color: {
start: string;
end: string;
};
icon: string;
bottom: { label: string; value: number }[];
}
console.log(props.modelValue);
const cardData = computed<CardData[]>(() => [
{
key: 'visitCount',
title: $t('page.home.visitCount'),
value: 9725,
key: 'retryTask',
title: $t('page.home.retryTask'),
tip: $t('page.home.retryTaskTip'),
value: props.modelValue?.retryTask.totalNum ?? 0,
unit: '',
color: {
start: '#ec4786',
end: '#b955a4'
start: '#40e9c5',
end: '#BEE3DB'
},
icon: 'ant-design:bar-chart-outlined'
icon: 'ant-design:schedule-outlined',
bottom: [
{
label: $t('common.success'),
value: props.modelValue?.retryTask.finishNum ?? 0
},
{
label: $t('common.running'),
value: props.modelValue?.retryTask.runningNum ?? 0
},
{
label: $t('page.manage.retryTask.status.maxRetryTimes'),
value: props.modelValue?.retryTask.maxCountNum ?? 0
},
{
label: $t('page.manage.retryTask.status.pauseRetry'),
value: props.modelValue?.retryTask.suspendNum ?? 0
}
]
},
{
key: 'turnover',
title: $t('page.home.turnover'),
value: 1026,
unit: '$',
key: 'jobTask',
title: $t('page.home.jobTask'),
tip: $t('page.home.jobTaskTip'),
value: props.modelValue?.jobTask.totalNum ?? 0,
color: {
start: '#865ec0',
end: '#5144b4'
start: '#f5b386',
end: '#FFD6BA'
},
icon: 'ant-design:money-collect-outlined'
icon: 'ant-design:profile-outlined',
bottom: [
{
label: $t('common.success'),
value: props.modelValue?.jobTask.successNum ?? 0
},
{
label: $t('common.fail'),
value: props.modelValue?.jobTask.failNum ?? 0
},
{
label: $t('common.stop'),
value: props.modelValue?.jobTask.stopNum ?? 0
},
{
label: $t('common.cancel'),
value: props.modelValue?.jobTask.cancelNum ?? 0
}
]
},
{
key: 'downloadCount',
title: $t('page.home.downloadCount'),
value: 970925,
key: 'onlineServiceCount',
title: $t('page.home.onlineServiceCount'),
tip: $t('page.home.onlineServiceTip'),
value: props.modelValue?.onLineService.total ?? 0,
unit: '',
color: {
start: '#56cdf3',
end: '#719de3'
start: '#b686d4',
end: '#c5a5d8'
},
icon: 'carbon:document-download'
icon: 'ant-design:database-outlined',
bottom: [
{
label: $t('page.manage.machine.type.client'),
value: props.modelValue?.onLineService.clientTotal ?? 0
},
{
label: $t('page.manage.machine.type.server'),
value: props.modelValue?.onLineService.serverTotal ?? 0
}
]
},
{
key: 'dealCount',
title: $t('page.home.dealCount'),
value: 9527,
key: 'workflow',
title: $t('page.home.workflow'),
tip: $t('page.home.workflowTip'),
value: 7,
unit: '',
color: {
start: '#fcbc25',
end: '#f68057'
start: '#ec6f6f',
end: '#f99797'
},
icon: 'ant-design:trademark-circle-outlined'
icon: 'ant-design:database-outlined',
bottom: [
{
label: $t('common.success'),
value: 185
},
{
label: $t('common.fail'),
value: 37
},
{
label: $t('common.stop'),
value: 5
},
{
label: $t('common.cancel'),
value: 13
}
]
}
]);
@ -89,21 +165,45 @@ function getGradientColor(color: CardData['color']) {
<NGrid cols="s:1 m:2 l:4" responsive="screen" :x-gap="16" :y-gap="16">
<NGi v-for="item in cardData" :key="item.key">
<GradientBg :gradient-color="getGradientColor(item.color)" class="flex-1">
<h3 class="text-16px">{{ item.title }}</h3>
<div class="flex justify-between pt-12px">
<SvgIcon :icon="item.icon" class="text-32px" />
<CountTo
:prefix="item.unit"
:start-value="1"
:end-value="item.value"
class="text-30px text-white dark:text-dark"
/>
</div>
</GradientBg>
<NSpin :show="false">
<GradientBg :gradient-color="getGradientColor(item.color)" class="h-165px flex-1">
<div class="flex justify-between">
<div class="align-center flex">
<SvgIcon :icon="item.icon" class="text-26px" />
<h3 class="ml-2 text-18px">{{ item.title }}</h3>
</div>
<NPopover trigger="hover">
<template #trigger>
<NButton text>
<SvgIcon icon="ant-design:info-circle-outlined" class="text-20px color-white" />
</NButton>
</template>
{{ item.tip }}
</NPopover>
</div>
<div class="flex pt-12px">
<CountTo :start-value="0" :end-value="item.value" class="text-30px text-white" />
</div>
<NProgress type="line" color="#728bf9" rail-color="#ebebeb" :percentage="30" indicator-text-color="#fff" />
<NDivider />
<template v-for="(bottomItem, bottomIndex) in item.bottom" :key="bottomIndex">
<NDivider v-if="bottomIndex !== 0" vertical />
{{ bottomItem.label }}
<CountTo :start-value="0" :end-value="bottomItem.value" class="text-white" />
</template>
</GradientBg>
</NSpin>
</NGi>
</NGrid>
</NCard>
</template>
<style scoped></style>
<style scoped>
.n-divider {
margin: 16px 0 6px;
}
.n-divider--vertical {
margin: 0 1px 0 5px;
}
</style>

View File

@ -22,41 +22,65 @@ interface StatisticData {
const statisticData = computed<StatisticData[]>(() => [
{
id: 0,
label: $t('page.home.projectCount'),
value: '25'
label: $t('page.home.userCount'),
value: '2'
},
{
id: 1,
label: $t('page.home.todo'),
value: '4/16'
label: $t('page.home.jobTaskCount'),
value: '8'
},
{
id: 2,
label: $t('page.home.message'),
value: '12'
label: $t('page.home.retryTaskCount'),
value: '3'
}
]);
const timeFix = () => {
const time = new Date();
const hour = time.getHours();
let text = '';
if (hour > 5 && hour <= 8) {
text = 'morning';
} else if (hour > 8 && hour <= 11) {
text = 'bth';
} else if (hour > 11 && hour <= 14) {
text = 'noon';
} else if (hour > 14 && hour <= 17) {
text = 'ath';
} else if (hour > 17 && hour <= 19) {
text = 'dusk';
} else if (hour > 19 && hour <= 21) {
text = 'evening';
} else if (hour > 21 && hour <= 5) {
text = 'earlyMorning';
}
return text;
};
</script>
<template>
<NCard :bordered="false" class="card-wrapper">
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:18">
<NGi class="flex" span="24 s:24 m:18">
<div class="flex-y-center">
<div class="size-72px shrink-0 overflow-hidden rd-1/2">
<img src="@/assets/imgs/soybean.jpg" class="size-full" />
</div>
<div class="pl-12px">
<h3 class="text-18px font-semibold">
{{ $t('page.home.greeting', { userName: authStore.userInfo.username }) }}
<h3 class="text-22px font-semibold">
{{ $t(`page.home.${timeFix()}Greeting`, { userName: authStore.userInfo.username }) }}
</h3>
<p class="text-#999 leading-30px">{{ $t('page.home.weatherDesc') }}</p>
<!-- <p class="text-#999 leading-30px">{{ $t('system.title') + ' - ' + $t('system.desc') }}</p> -->
</div>
</div>
</NGi>
<NGi span="24 s:24 m:6">
<NSpace :size="24" justify="end">
<NStatistic v-for="item in statisticData" :key="item.id" class="whitespace-nowrap" v-bind="item" />
<NStatistic
v-for="item in statisticData"
:key="item.id"
class="whitespace-nowrap text-center"
v-bind="item"
/>
</NSpace>
</NGi>
</NGrid>