From 875fe295f4e7bd84f0c045033181d0624a72f538 Mon Sep 17 00:00:00 2001
From: valign <78541912+cuvalign@users.noreply.github.com>
Date: Fri, 10 May 2024 18:15:13 +0800
Subject: [PATCH] =?UTF-8?q?feat(mis):=20=E7=AE=A1=E7=90=86=E7=B3=BB?=
=?UTF-8?q?=E7=BB=9F=E4=BB=AA=E8=A1=A8=E7=9B=98=E8=B4=A6=E6=88=B7=E4=BF=A1?=
=?UTF-8?q?=E6=81=AF=E6=98=BE=E7=A4=BA=E4=BC=98=E5=8C=96=20(#1242)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
仪表盘中账户信息展示当前用户所在的账户信息,具体调整如下:
1. 左上角为账户名,右上角为上锁图标,下方为可用额度;
2. 当账户或者用户状态均为正常时:
1. 右上角隐藏锁的图标;
2. 有设置限额,可用额度=用户限额-已用额度;
3. 未设置限额且账户在白名单中,可用额度无限,显示为"不限";
4. 未设置限额且账户不在白名单中,可用额度=账户余额-封锁阈值;
3. 当账户或者用户有一个非正常状态时(封锁/欠费/限额):
1. 右上角用锁表示不可用,避免用文字误导;
2. 无可用额度,显示为“-”;
原设计示意:
![Image](https://github.com/PKUHPC/scow-internal-dev/assets/111728204/59365140-08dd-41c2-9a8a-cb8b5b68ed91)
兼容账户名超出的情况:
实际完成示意:
![QQ截图20240509161310](https://github.com/PKUHPC/SCOW/assets/78541912/1d7e5a12-3c34-4f44-aaa9-84a5cc53cb4a)
---
.changeset/grumpy-scissors-flash.md | 7 ++++
apps/mis-server/src/services/user.ts | 8 ++++
apps/mis-web/src/components/StatCard.tsx | 33 +++++++++++++++
apps/mis-web/src/i18n/zh_cn.ts | 6 +--
apps/mis-web/src/models/UserSchemaModel.ts | 2 +
.../dashboard/AccountInfoSection.tsx | 42 ++++++++-----------
apps/mis-web/src/pages/dashboard.tsx | 9 ++--
protos/server/user.proto | 2 +
8 files changed, 76 insertions(+), 33 deletions(-)
create mode 100644 .changeset/grumpy-scissors-flash.md
diff --git a/.changeset/grumpy-scissors-flash.md b/.changeset/grumpy-scissors-flash.md
new file mode 100644
index 0000000000..d0e64f007c
--- /dev/null
+++ b/.changeset/grumpy-scissors-flash.md
@@ -0,0 +1,7 @@
+---
+"@scow/grpc-api": minor
+"@scow/mis-server": patch
+"@scow/mis-web": patch
+---
+
+管理系统仪表盘账户信息显示卡片中可用余额逻辑和 UI 优化
diff --git a/apps/mis-server/src/services/user.ts b/apps/mis-server/src/services/user.ts
index dbb0e547ff..99df31406f 100644
--- a/apps/mis-server/src/services/user.ts
+++ b/apps/mis-server/src/services/user.ts
@@ -106,6 +106,11 @@ export const userServiceServer = plugin((server) => {
};
}
+ const tenant = await em.findOne(Tenant, { name: tenantName });
+ if (!tenant) {
+ throw { code:Status.NOT_FOUND, message: `Tenant ${tenantName} is not found.` };
+ }
+
return [{
accountStatuses: user.accounts.getItems().reduce((prev, curr) => {
const account = curr.account.getEntity();
@@ -115,6 +120,9 @@ export const userServiceServer = plugin((server) => {
jobChargeLimit: curr.jobChargeLimit ? decimalToMoney(curr.jobChargeLimit) : undefined,
usedJobCharge: curr.usedJobCharge ? decimalToMoney(curr.usedJobCharge) : undefined,
balance: decimalToMoney(curr.account.getEntity().balance),
+ isInWhitelist: Boolean(account.whitelist),
+ blockThresholdAmount:account.blockThresholdAmount ?
+ decimalToMoney(account.blockThresholdAmount) : decimalToMoney(tenant.defaultAccountBlockThreshold),
} as AccountStatus;
return prev;
}, {}),
diff --git a/apps/mis-web/src/components/StatCard.tsx b/apps/mis-web/src/components/StatCard.tsx
index 7d2e4c714a..8bb5f4c665 100644
--- a/apps/mis-web/src/components/StatCard.tsx
+++ b/apps/mis-web/src/components/StatCard.tsx
@@ -16,10 +16,14 @@ import { styled } from "styled-components";
type Props = React.PropsWithChildren<{
title: React.ReactNode;
+ icon?: React.ReactNode;
}>;
const Title = styled.h3`
font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
`;
const ChildrenContainer = styled.div`
@@ -29,7 +33,18 @@ const ChildrenContainer = styled.div`
flex: 1;
`;
+const Header = styled.header`
+ display: flex;
+ justify-content: flex-start;
+ align-items: baseline;
+`;
+
+const IconContainer = styled.div`
+ flex-shrink: 0;
+ margin-left: clamp(1em, 12%, 5em);
+`;
+// 仅StorageSection使用,但StorageSection已注释
export const StatCard: React.FC = ({ children, title }) => {
return (
= ({ children, title }) => {
);
};
+export const AccountStatCard: React.FC = ({ children, title, icon }) => {
+ return (
+
+
+
+ {children}
+
+
+ );
+};
diff --git a/apps/mis-web/src/i18n/zh_cn.ts b/apps/mis-web/src/i18n/zh_cn.ts
index 1785bd0d90..ccba590294 100644
--- a/apps/mis-web/src/i18n/zh_cn.ts
+++ b/apps/mis-web/src/i18n/zh_cn.ts
@@ -117,11 +117,7 @@ export default {
account: {
title: "账户信息",
state: "状态",
- balance: "可用余额",
- status: {
- blocked: "封锁",
- normal: "正常",
- },
+ balance: "可用额度",
alert: "您不属于任何一个账户。",
},
job: {
diff --git a/apps/mis-web/src/models/UserSchemaModel.ts b/apps/mis-web/src/models/UserSchemaModel.ts
index 0eca389069..645ff238d0 100644
--- a/apps/mis-web/src/models/UserSchemaModel.ts
+++ b/apps/mis-web/src/models/UserSchemaModel.ts
@@ -83,6 +83,8 @@ export const AccountStatus = Type.Object({
jobChargeLimit: Type.Optional(Money),
usedJobCharge: Type.Optional(Money),
balance: Type.Optional(Money),
+ isInWhitelist: Type.Optional(Type.Boolean()),
+ blockThresholdAmount:Type.Optional(Money),
});
export type AccountStatus = Static;
diff --git a/apps/mis-web/src/pageComponents/dashboard/AccountInfoSection.tsx b/apps/mis-web/src/pageComponents/dashboard/AccountInfoSection.tsx
index b6796ce937..2517c3953b 100644
--- a/apps/mis-web/src/pageComponents/dashboard/AccountInfoSection.tsx
+++ b/apps/mis-web/src/pageComponents/dashboard/AccountInfoSection.tsx
@@ -15,7 +15,7 @@ import { moneyToNumber } from "@scow/lib-decimal";
import { Alert, Col, Row, Statistic, StatisticProps } from "antd";
import React from "react";
import { Section } from "src/components/Section";
-import { StatCard } from "src/components/StatCard";
+import { AccountStatCard } from "src/components/StatCard";
import { useI18nTranslateToString } from "src/i18n";
import { UserStatus } from "src/models/User";
import type { AccountInfo } from "src/pages/dashboard";
@@ -27,6 +27,7 @@ interface Props {
info: Record;
}
+// max-width: calc(100%/2 - 8px); 考虑换行后限制最大宽度
const CardContainer = styled.div`
flex: 1;
min-width: 300px;
@@ -39,7 +40,7 @@ const Container = styled.div`
`;
const Info: React.FC = (props) => (
-
+
);
@@ -52,9 +53,8 @@ export const AccountInfoSection: React.FC = ({ info }) => {
const t = useI18nTranslateToString();
const statusTexts = {
- blocked: [t("dashboard.account.status.blocked"), "red", LockOutlined],
- normal: [t("dashboard.account.status.normal"), "green", UnlockOutlined],
-
+ blocked: ["red", LockOutlined, "1"],
+ normal: ["green", UnlockOutlined, "0"],
} as const;
return (
@@ -67,37 +67,29 @@ export const AccountInfoSection: React.FC = ({ info }) => {
{
accounts.map(([accountName, {
accountBlocked, userStatus, balance,
- jobChargeLimit, usedJobCharge,
+ jobChargeLimit, usedJobCharge, isInWhitelist, blockThresholdAmount,
}]) => {
- const [text, textColor, Icon] = accountBlocked || userStatus === UserStatus.BLOCKED
- ? statusTexts.blocked
- : statusTexts.normal;
-
+ const isBlocked = accountBlocked || userStatus === UserStatus.BLOCKED;
+ const [ textColor, Icon, opacity] = isBlocked ? statusTexts.blocked : statusTexts.normal;
const availableLimit = jobChargeLimit && usedJobCharge
- ? moneyToNumber(jobChargeLimit) - moneyToNumber(usedJobCharge)
+ ? (moneyToNumber(jobChargeLimit) - moneyToNumber(usedJobCharge)).toFixed(2)
: undefined;
-
- const minOne = availableLimit ? Math.min(availableLimit, balance) : balance;
-
+ const whitelistCharge = isInWhitelist ? "不限" : undefined;
+ const normalCharge = (balance - blockThresholdAmount).toFixed(2);
+ const showAvailableBalance = availableLimit ?? whitelistCharge ?? normalCharge;
return (
-
+ }>
- }
- value={text}
- />
¥}
+ value={isBlocked ? "-" : showAvailableBalance}
/>
-
+
);
})
diff --git a/apps/mis-web/src/pages/dashboard.tsx b/apps/mis-web/src/pages/dashboard.tsx
index 152e0fe52c..7f2ae26fe6 100644
--- a/apps/mis-web/src/pages/dashboard.tsx
+++ b/apps/mis-web/src/pages/dashboard.tsx
@@ -32,10 +32,12 @@ import { UserStore } from "src/stores/UserStore";
import { ensureNotUndefined } from "src/utils/checkNull";
-export type AccountInfo = Omit & {
+export type AccountInfo = Omit & {
balance: number;
jobChargeLimit: Money | null;
usedJobCharge: Money | null;
+ blockThresholdAmount: number
}
type Props = {
@@ -106,15 +108,16 @@ export const getServerSideProps: GetServerSideProps = async ({ req }) =>
const accounts = Object.entries(status.accountStatuses).reduce((prev, [accountName, info]) => {
- const { balance, ...validated } = ensureNotUndefined(info, ["balance"]);
+ const { balance, blockThresholdAmount, ...validated }
+ = ensureNotUndefined(info, ["balance", "blockThresholdAmount"]);
prev[accountName] = {
...validated,
balance: moneyToNumber(balance),
-
// 不能使用undefined,NextJs中:`undefined` cannot be serialized as JSON
jobChargeLimit: validated.jobChargeLimit ?? null,
usedJobCharge: validated.usedJobCharge ?? null,
+ blockThresholdAmount:moneyToNumber(blockThresholdAmount),
};
return prev;
diff --git a/protos/server/user.proto b/protos/server/user.proto
index bbbf11c8d5..a759258255 100644
--- a/protos/server/user.proto
+++ b/protos/server/user.proto
@@ -35,6 +35,8 @@ message AccountStatus {
optional common.Money job_charge_limit = 3;
optional common.Money used_job_charge = 4;
common.Money balance = 5;
+ optional bool is_in_whitelist = 6;
+ common.Money block_threshold_amount = 7;
}
message GetUserStatusResponse {