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 <noreply@anthropic.com>
This commit is contained in:
@@ -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': '',
|
||||
|
||||
@@ -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 <PageLoading />;
|
||||
|
||||
const { company } = currentUser ?? {};
|
||||
|
||||
if (company?.special !== 0 && company?.validityStatus === 2) {
|
||||
return <CompanyExpireGuard />;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{currentUser && (
|
||||
|
||||
50
src/components/CompanyExpireGuard/index.tsx
Normal file
50
src/components/CompanyExpireGuard/index.tsx
Normal file
@@ -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 (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 9999,
|
||||
background: '#fff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Result
|
||||
status="warning"
|
||||
title={$t('pages.company.expireTitle')}
|
||||
subTitle={$t('pages.company.expireSubTitle')}
|
||||
extra={
|
||||
<Space>
|
||||
<Button type="primary" onClick={() => window.location.reload()}>
|
||||
{$t('pages.refresh')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
logoutAndToLoginPage();
|
||||
}}
|
||||
>
|
||||
{$t('pages.logout')}
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompanyExpireGuard;
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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': '其他菜单权限',
|
||||
|
||||
|
||||
@@ -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<API.CompanyListItem>;
|
||||
@@ -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 && (
|
||||
<ProFormDatePicker
|
||||
hidden={formValues?.id === void 0}
|
||||
name="validityEndTime"
|
||||
label={`${$t('form.company.validityEndTime')} (${$t('form.company.validityEndTime.tips')})`}
|
||||
placeholder={$t('form.company.validityEndTime.placeholder')}
|
||||
convertValue={(value: string | Dayjs | null | undefined) =>
|
||||
value ? value : undefined
|
||||
}
|
||||
fieldProps={{
|
||||
style: { width: '100%' },
|
||||
disabledDate: (current: Dayjs) =>
|
||||
current?.isBefore(dayjs().startOf('day')),
|
||||
}}
|
||||
// extra={<Text type="danger">{$t('pages.company.expired')}</Text>}
|
||||
/>
|
||||
)}
|
||||
|
||||
{formValues?.id !== void 0 && (
|
||||
<>
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
type ProColumns,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { Badge, Button, Col, Row } from 'antd';
|
||||
import { useAccess } from '@umijs/max';
|
||||
import { Badge, Button, Col, Row, Space, Typography } from 'antd';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import HeadStatisticCard, {
|
||||
type CardInfo,
|
||||
@@ -13,9 +14,13 @@ import HeadStatisticCard, {
|
||||
import MyTag from '@/components/MyTag';
|
||||
import { companyHandle, getCompanyPages } from '@/services/api';
|
||||
import { $t } from '@/utils/i18n';
|
||||
import { utcInstance as dayjs } from '@/utils/timeFormat';
|
||||
import PopUp from './Popup';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
const Company: React.FC = () => {
|
||||
const { isSuperAdmin } = useAccess();
|
||||
const [modalVisit, setModalVisit] = useState(false);
|
||||
|
||||
const [cardInfo, setCardInfo] = useState({
|
||||
@@ -92,6 +97,28 @@ const Company: React.FC = () => {
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: $t('table.validityEndTime'),
|
||||
dataIndex: 'validityEndTime',
|
||||
tooltip: $t('table.validityEndTime.tips'),
|
||||
hideInTable: !isSuperAdmin,
|
||||
render: (_, { validityEndTime, validityStatus }) => {
|
||||
return (
|
||||
<Space>
|
||||
{!validityEndTime ? (
|
||||
<Text type="secondary">{$t('pages.company.permanent')}</Text>
|
||||
) : (
|
||||
<span>{dayjs(validityEndTime).format('YYYY-MM-DD')}</span>
|
||||
)}
|
||||
{validityStatus === 2 && (
|
||||
<Text type="danger" style={{ fontSize: 11 }}>
|
||||
{$t('pages.company.expired')}
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: $t('table.createTime'),
|
||||
dataIndex: 'createTime',
|
||||
@@ -122,7 +149,10 @@ const Company: React.FC = () => {
|
||||
size="small"
|
||||
key="edit"
|
||||
onClick={() => {
|
||||
setFormValues(item);
|
||||
setFormValues({
|
||||
...item,
|
||||
validityEndTime: item.validityEndTime || undefined,
|
||||
});
|
||||
setModalVisit(true);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
ProForm,
|
||||
ProFormText,
|
||||
} from '@ant-design/pro-components';
|
||||
import { history } from '@umijs/max';
|
||||
import { history, useAccess } from '@umijs/max';
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
Space,
|
||||
Tag,
|
||||
} from 'antd';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { clearAccessToken } from '@/access';
|
||||
import PasswordLevel from '@/components/PasswordLevel';
|
||||
import useUserInfo from '@/hooks/useUserInfo';
|
||||
@@ -24,6 +24,7 @@ import { $t } from '@/utils/i18n';
|
||||
|
||||
const Profile: React.FC = () => {
|
||||
const { userInfo, setUserInfo } = useUserInfo();
|
||||
const { isGlobalCompany } = useAccess();
|
||||
const userCompany = userInfo?.company;
|
||||
const { message: antdMessage, modal } = App.useApp();
|
||||
|
||||
@@ -71,6 +72,19 @@ const Profile: React.FC = () => {
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGlobalCompany) {
|
||||
items.push({
|
||||
key: '6',
|
||||
span: 'filled',
|
||||
label: $t('pages.company.permanent'),
|
||||
children: userCompany?.validityEndTime
|
||||
? userCompany.validityEndTime
|
||||
: $t('pages.company.permanent'),
|
||||
});
|
||||
}
|
||||
}, [userInfo]);
|
||||
|
||||
const changePasswordSuccessHadle = async () => {
|
||||
let countdown = 5;
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
|
||||
@@ -83,6 +83,8 @@ export async function companyHandle(
|
||||
name?: string;
|
||||
email?: string;
|
||||
status?: number;
|
||||
validityStartTime?: string | null;
|
||||
validityEndTime?: string | null;
|
||||
},
|
||||
options?: ObjType,
|
||||
) {
|
||||
|
||||
3
src/services/typings.d.ts
vendored
3
src/services/typings.d.ts
vendored
@@ -113,6 +113,9 @@ declare namespace API {
|
||||
fromName: string | null;
|
||||
mailHostConfigId: number | string | null;
|
||||
mailStatus: number | null;
|
||||
validityStartTime?: string | null;
|
||||
validityEndTime?: string | null;
|
||||
validityStatus?: number | null;
|
||||
};
|
||||
|
||||
// "DataSource" Type
|
||||
|
||||
Reference in New Issue
Block a user