From fc55965555a52b953ad5eb3263945f0c5fb686ec Mon Sep 17 00:00:00 2001 From: Johnton Chen <504071088@qq.com> Date: Wed, 29 Jul 2026 16:15:58 +0800 Subject: [PATCH] feat(company): add subscription expiry management and expired company guard Add validity period fields to company CRUD, display expiry info in table and profile, and block access for expired companies via a full-screen guard component. Co-Authored-By: Claude Opus 4.7 --- config/proxy.ts | 3 +- src/app.tsx | 7 +++ src/components/CompanyExpireGuard/index.tsx | 50 +++++++++++++++++++++ src/locales/en_US/pages.ts | 12 +++++ src/locales/zh_CN/pages.ts | 12 +++++ src/pages/company/Popup.tsx | 30 ++++++++++++- src/pages/company/index.tsx | 34 +++++++++++++- src/pages/profile/index.tsx | 18 +++++++- src/services/api/api.ts | 2 + src/services/typings.d.ts | 3 ++ 10 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 src/components/CompanyExpireGuard/index.tsx diff --git a/config/proxy.ts b/config/proxy.ts index 5bfce15..ba63b71 100644 --- a/config/proxy.ts +++ b/config/proxy.ts @@ -15,7 +15,8 @@ export default { '/api': { // The address to proxy // target: 'http://2333z061l7.51mypc.cn', - target: 'http://172.20.177.121:8080', + // target: 'http://172.20.177.121:8080', + target: 'http://172.20.177.121:8082', changeOrigin: true, pathRewrite: { '^/api': '', diff --git a/src/app.tsx b/src/app.tsx index 20dd5b6..7786dad 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -17,6 +17,7 @@ import ServerErrorPage from './pages/500'; import { errorConfig } from './requestErrorConfig'; import { getDeviceId } from './utils/fp'; import './utils/sentry'; +import CompanyExpireGuard from './components/CompanyExpireGuard'; import GlobalErrorBoundary from './components/GlobalErrorBoundary'; import PermissionGuard from './components/PermissionGuard'; import SSEController from './components/SSEController'; @@ -197,6 +198,12 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => { // Add a loading state childrenRender: (children) => { if (initialState?.loading) return ; + + const { company } = currentUser ?? {}; + + if (company?.special !== 0 && company?.validityStatus === 2) { + return ; + } return ( <> {currentUser && ( diff --git a/src/components/CompanyExpireGuard/index.tsx b/src/components/CompanyExpireGuard/index.tsx new file mode 100644 index 0000000..d399b26 --- /dev/null +++ b/src/components/CompanyExpireGuard/index.tsx @@ -0,0 +1,50 @@ +import { Button, Result, Space } from 'antd'; +import { logoutAndToLoginPage } from '@/access'; +import useUserInfo from '@/hooks/useUserInfo'; +import { $t } from '@/utils/i18n'; + +const CompanyExpireGuard: React.FC = () => { + const { userInfo } = useUserInfo(); + const isExpired = userInfo?.company?.validityStatus === 2; + + if (!isExpired) return null; + + return ( +
+ + + + + } + /> +
+ ); +}; + +export default CompanyExpireGuard; diff --git a/src/locales/en_US/pages.ts b/src/locales/en_US/pages.ts index d87314e..076002c 100644 --- a/src/locales/en_US/pages.ts +++ b/src/locales/en_US/pages.ts @@ -46,6 +46,8 @@ export default { 'table.contactEmail': 'Contact Email', 'table.dataSourceCount': 'Data Sources', 'table.userCount': 'Users', + 'table.validityEndTime': 'Expire At (UTC+0)', + 'table.validityEndTime.tips': 'Company subscription expiry date ', 'table.createTime': 'Create Time', 'table.name': 'Name', 'table.ipAddress': 'IP Address', @@ -87,6 +89,13 @@ export default { 'pages.totalAlertCount': 'Total Alerts', 'pages.totalCompanyCount': 'Total Companies', 'pages.activeCompanyCount': 'Active Companies', + 'pages.company.expired': 'Expired', + 'pages.company.permanent': 'Permanent', + 'pages.company.expireTitle': 'Company Subscription Expired', + 'pages.company.expireSubTitle': + "Your company's license has expired. All features are temporarily locked. Please contact your sales representative to renew.", + 'pages.company.expireInDays': 'Subscription expires in {days} days', + 'pages.company.expireToday': 'Expires Today', 'pages.totalDataSourceCount': 'Total Data Sources', 'pages.activeDataSourceCount': 'Active Data Sources', 'pages.mt4DataSourceCount': 'MT4 Data Sources', @@ -329,6 +338,9 @@ export default { 'form.role.placeholder2': 'System roles cannot modify permission levels', 'form.role.badgeColor': 'Badge Color', 'form.role.menuIds': 'Bound Permissions (Menus)', + 'form.company.validityEndTime': 'Expiry At (UTC+0)', + 'form.company.validityEndTime.placeholder': 'Select expiry date', + 'form.company.validityEndTime.tips': 'Blank for Permanent', 'form.role.permissionGuide': 'Permission Guide', 'form.role.otherPermissions': 'Other Menu Permissions', diff --git a/src/locales/zh_CN/pages.ts b/src/locales/zh_CN/pages.ts index 7f1c0b8..af72acb 100644 --- a/src/locales/zh_CN/pages.ts +++ b/src/locales/zh_CN/pages.ts @@ -46,6 +46,8 @@ export default { 'table.contactEmail': '联系邮箱', 'table.dataSourceCount': '数据源', 'table.userCount': '用户数', + 'table.validityEndTime': '订阅到期时间(UTC+0)', + 'table.validityEndTime.tips': '公司订阅服务到期时间', 'table.createTime': '创建时间', 'table.name': '名称', 'table.ipAddress': 'IP地址', @@ -87,6 +89,13 @@ export default { 'pages.totalAlertCount': '总告警数', 'pages.totalCompanyCount': '总公司数', 'pages.activeCompanyCount': '活跃公司数', + 'pages.company.expired': '已过期', + 'pages.company.permanent': '永久有效', + 'pages.company.expireTitle': '公司订阅已过期', + 'pages.company.expireSubTitle': + '您的公司在该系统的使用授权已到期,各项功能已暂时锁定。请联系商务代表进行续费操作以恢复访问。', + 'pages.company.expireInDays': '订阅即将到期,剩余 {days}天', + 'pages.company.expireToday': '今天到期', 'pages.totalDataSourceCount': '数据源总数', 'pages.activeDataSourceCount': '活跃数据源', 'pages.mt4DataSourceCount': 'MT4 数据源', @@ -321,6 +330,9 @@ export default { 'form.role.placeholder2': '系统角色权限级别不可修改', 'form.role.badgeColor': '徽章颜色', 'form.role.menuIds': '绑定权限(菜单)', + 'form.company.validityEndTime': '到期时间(UTC+0)', + 'form.company.validityEndTime.placeholder': '请选择到期时间', + 'form.company.validityEndTime.tips': '留空为永久有效', 'form.role.permissionGuide': '权限怎么选', 'form.role.otherPermissions': '其他菜单权限', diff --git a/src/pages/company/Popup.tsx b/src/pages/company/Popup.tsx index 8c7dc45..24359ab 100644 --- a/src/pages/company/Popup.tsx +++ b/src/pages/company/Popup.tsx @@ -1,5 +1,6 @@ import { ModalForm, + ProFormDatePicker, ProFormDependency, type ProFormInstance, ProFormSelect, @@ -8,6 +9,7 @@ import { } from '@ant-design/pro-components'; import { useAccess } from '@umijs/max'; import { Divider, Space, Typography } from 'antd'; +import type { Dayjs } from 'dayjs'; import { useRef } from 'react'; import useUserInfo from '@/hooks/useUserInfo'; import { @@ -16,6 +18,7 @@ import { getEmailConfigList, } from '@/services/api'; import { $t } from '@/utils/i18n'; +import { utcInstance as dayjs } from '@/utils/timeFormat'; const { Text } = Typography; type CompanyListItem = Partial; @@ -25,7 +28,7 @@ export default (props: { onSubmit: (values: CompanyListItem) => void; onDone: () => void; }) => { - const { isGlobalCompany } = useAccess(); + const { isGlobalCompany, isSuperAdmin } = useAccess(); const { userInfo } = useUserInfo(); const { modalVisit, formValues, onSubmit, onDone } = props; @@ -43,6 +46,13 @@ export default (props: { if (!isGlobalCompany) { values.companyId = userInfo?.companyId; } + + if (isSuperAdmin) { + values.validityEndTime = values.validityEndTime + ? dayjs(values.validityEndTime).endOf('day').format('YYYY-MM-DD') + : ''; + values.validityStartTime = values.validityEndTime ? '2026-01-01' : ''; + } return onSubmit(values); }} modalProps={{ @@ -70,6 +80,7 @@ export default (props: { return { ...values, ...companyMailHostConfig, + validityEndTime: values.validityEndTime || undefined, }; } return values || {}; @@ -106,6 +117,23 @@ export default (props: { }, ]} /> + {isSuperAdmin && ( +