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) 兼容账户名超出的情况: fe048395c30084236836df3698a613a 实际完成示意: ![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 ( + +
+ + {title} + + {icon} +
+ + {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 {