'init_again'
Change-Id: Ib7ecdb9f5baeab1e4681152a57b936edf7475b35
diff --git a/src/pages/404.tsx b/src/pages/404.tsx
new file mode 100644
index 0000000..0263687
--- /dev/null
+++ b/src/pages/404.tsx
@@ -0,0 +1,18 @@
+import { history } from '@umijs/max';
+import { Button, Result } from 'antd';
+import React from 'react';
+
+const NoFoundPage: React.FC = () => (
+ <Result
+ status="404"
+ title="404"
+ subTitle="Sorry, the page you visited does not exist."
+ extra={
+ <Button type="primary" onClick={() => history.push('/')}>
+ Back Home
+ </Button>
+ }
+ />
+);
+
+export default NoFoundPage;
diff --git a/src/pages/Monitor/Cache/List.tsx b/src/pages/Monitor/Cache/List.tsx
new file mode 100644
index 0000000..af6147a
--- /dev/null
+++ b/src/pages/Monitor/Cache/List.tsx
@@ -0,0 +1,240 @@
+import React, { useEffect, useState, useRef } from 'react';
+import { clearCacheAll, clearCacheKey, clearCacheName, getCacheValue, listCacheKey, listCacheName } from '@/services/monitor/cachelist';
+import { Button, Card, Col, Form, FormInstance, Input, message, Row, Table } from 'antd';
+import styles from './index.less';
+import { FormattedMessage } from '@umijs/max';
+import { ReloadOutlined } from '@ant-design/icons';
+import { ProForm } from '@ant-design/pro-components';
+
+const { TextArea } = Input;
+
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2022/06/27
+ *
+ * */
+
+
+
+const CacheList: React.FC = () => {
+ const [cacheNames, setCacheNames] = useState<any>([]);
+ const [currentCacheName, setCurrentCacheName] = useState<any>([]);
+ const [cacheKeys, setCacheKeys] = useState<any>([]);
+ const [form] = Form.useForm();
+
+ const getCacheNames = () => {
+ listCacheName().then((res) => {
+ if (res.code === 200) {
+ setCacheNames(res.data);
+ }
+ });
+ }
+
+ useEffect(() => {
+ getCacheNames();
+ }, []);
+
+ const getCacheKeys = (cacheName: string) => {
+ listCacheKey(cacheName).then(res => {
+ if (res.code === 200) {
+ let index = 0;
+ const keysData = res.data.map((item: any) => {
+ return {
+ index: index++,
+ cacheKey: item
+ }
+ })
+ setCacheKeys(keysData);
+ }
+ });
+ };
+
+ const onClearAll = async () => {
+ clearCacheAll().then(res => {
+ if(res.code === 200) {
+ message.success("清理全部缓存成功");
+ }
+ });
+ };
+
+ const onClearAllFailed = (errorInfo: any) => {
+ message.error('Failed:', errorInfo);
+ };
+
+ const refreshCacheNames = () => {
+ getCacheNames();
+ message.success("刷新缓存列表成功");
+ };
+
+ const refreshCacheKeys = () => {
+ getCacheKeys(currentCacheName);
+ message.success("刷新键名列表成功");
+ };
+
+ const columns = [
+ {
+ title: '缓存名称',
+ dataIndex: 'cacheName',
+ key: 'cacheName',
+ render: (_: any, record: any) => {
+ return record.cacheName.replace(":", "");
+ }
+ },
+ {
+ title: '备注',
+ dataIndex: 'remark',
+ key: 'remark',
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '40px',
+ valueType: 'option',
+ render: (_: any, record: API.Monitor.CacheContent) => [
+ <Button
+ type="link"
+ size="small"
+ key="remove"
+ danger
+ onClick={() => {
+ clearCacheName(record.cacheName).then(res => {
+ if(res.code === 200) {
+ message.success("清理缓存名称[" + record.cacheName + "]成功");
+ getCacheKeys(currentCacheName);
+ }
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ ]
+ }
+ ];
+
+ const cacheKeysColumns = [
+ {
+ title: '序号',
+ dataIndex: 'index',
+ key: 'index'
+ },
+ {
+ title: '缓存键名',
+ dataIndex: 'cacheKey',
+ key: 'cacheKey',
+ render: (_: any, record: any) => {
+ return record.cacheKey.replace(currentCacheName, "");
+ }
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '40px',
+ valueType: 'option',
+ render: (_: any, record: API.Monitor.CacheContent) => [
+ <Button
+ type="link"
+ size="small"
+ key="remove"
+ danger
+ onClick={() => {
+ console.log(record)
+ clearCacheKey(record.cacheKey).then(res => {
+ if(res.code === 200) {
+ message.success("清理缓存键名[" + record.cacheKey + "]成功");
+ getCacheKeys(currentCacheName);
+ }
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ ]
+ }
+ ];
+
+ return (
+ <div>
+ <Row gutter={[24, 24]}>
+ <Col span={8}>
+ <Card title="缓存列表" extra={<Button icon={<ReloadOutlined />} onClick={()=>{ refreshCacheNames()}} type="link" />} className={styles.card}>
+ <Table
+ rowKey="cacheName"
+ dataSource={cacheNames}
+ columns={columns}
+ onRow={(record: API.Monitor.CacheContent) => {
+ return {
+ onClick: () => {
+ setCurrentCacheName(record.cacheName);
+ getCacheKeys(record.cacheName);
+ },
+ };
+ }}
+ />
+ </Card>
+ </Col>
+ <Col span={8}>
+ <Card title="键名列表" extra={<Button icon={<ReloadOutlined />} onClick={()=>{ refreshCacheKeys()}} type="link" />} className={styles.card}>
+ <Table
+ rowKey="index"
+ dataSource={cacheKeys}
+ columns={cacheKeysColumns}
+ onRow={(record: any) => {
+ return {
+ onClick: () => {
+ getCacheValue(currentCacheName, record.cacheKey).then(res => {
+ if (res.code === 200) {
+ form.resetFields();
+ form.setFieldsValue({
+ cacheName: res.data.cacheName,
+ cacheKey: res.data.cacheKey,
+ cacheValue: res.data.cacheValue,
+ remark: res.data.remark,
+ });
+ }
+ });
+ },
+ };
+ }}
+ />
+ </Card>
+ </Col>
+ <Col span={8}>
+ <Card title="缓存内容" extra={<Button icon={<ReloadOutlined />} onClick={()=>{ onClearAll()}} type="link" >清理全部</Button>} className={styles.card}>
+ <ProForm
+ name="basic"
+ form={form}
+ labelCol={{ span: 8 }}
+ wrapperCol={{ span: 16 }}
+ onFinish={onClearAll}
+ onFinishFailed={onClearAllFailed}
+ autoComplete="off"
+ >
+ <Form.Item
+ label="缓存名称"
+ name="cacheName"
+ >
+ <Input />
+ </Form.Item>
+ <Form.Item
+ label="缓存键名"
+ name="cacheKey"
+ >
+ <Input />
+ </Form.Item>
+ <Form.Item
+ label="缓存内容"
+ name="cacheValue"
+ >
+ <TextArea autoSize={{ minRows: 2 }} />
+ </Form.Item>
+ </ProForm>
+ </Card>
+ </Col>
+ </Row>
+ </div>
+ );
+};
+
+export default CacheList;
diff --git a/src/pages/Monitor/Cache/List/index.less b/src/pages/Monitor/Cache/List/index.less
new file mode 100644
index 0000000..0c5aace
--- /dev/null
+++ b/src/pages/Monitor/Cache/List/index.less
@@ -0,0 +1,10 @@
+
+
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2021/09/16
+ *
+ * */
+
diff --git a/src/pages/Monitor/Cache/index.less b/src/pages/Monitor/Cache/index.less
new file mode 100644
index 0000000..7112c50
--- /dev/null
+++ b/src/pages/Monitor/Cache/index.less
@@ -0,0 +1,33 @@
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2021/09/16
+ *
+ * */
+
+
+.card {
+ margin-bottom: 12px;
+}
+
+
+.miniChart {
+ position: relative;
+ width: 100%;
+ .chartContent {
+ position: absolute;
+ bottom: -28px;
+ width: 100%;
+ > div {
+ margin: 0 -5px;
+ overflow: hidden;
+ }
+ }
+ .chartLoading {
+ position: absolute;
+ top: 16px;
+ left: 50%;
+ margin-left: -7px;
+ }
+}
diff --git a/src/pages/Monitor/Cache/index.tsx b/src/pages/Monitor/Cache/index.tsx
new file mode 100644
index 0000000..4c247ee
--- /dev/null
+++ b/src/pages/Monitor/Cache/index.tsx
@@ -0,0 +1,201 @@
+import React, { useEffect, useState } from 'react';
+import { Card, Col, Row, Table } from 'antd';
+import { DataItem } from '@antv/g2plot/esm/interface/config';
+import { Gauge, Pie } from '@ant-design/plots';
+import styles from './index.less';
+import { getCacheInfo } from '@/services/monitor/cache';
+
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2021/09/16
+ *
+ * */
+
+
+const columns = [
+ {
+ title: 'col1',
+ dataIndex: 'col1',
+ key: 'col1',
+ },
+ {
+ title: 'col2',
+ dataIndex: 'col2',
+ key: 'col2',
+ },
+ {
+ title: 'col3',
+ dataIndex: 'col3',
+ key: 'col3',
+ },
+ {
+ title: 'col4',
+ dataIndex: 'col4',
+ key: 'col4',
+ },
+ {
+ title: 'col5',
+ dataIndex: 'col5',
+ key: 'col5',
+ },
+ {
+ title: 'col6',
+ dataIndex: 'col6',
+ key: 'col6',
+ },
+ {
+ title: 'col7',
+ dataIndex: 'col7',
+ key: 'col7',
+ },
+ {
+ title: 'col8',
+ dataIndex: 'col8',
+ key: 'col8',
+ },
+];
+
+const usageFormatter = (val: string): string => {
+ switch (val) {
+ case '10':
+ return '100%';
+ case '8':
+ return '80%';
+ case '6':
+ return '60%';
+ case '4':
+ return '40%';
+ case '2':
+ return '20%';
+ case '0':
+ return '0%';
+ default:
+ return '';
+ }
+};
+
+const CacheInfo: React.FC = () => {
+ const [baseInfoData, setBaseInfoData] = useState<any>([]);
+ const [memUsage, setMemUsage] = useState<Number>(0);
+ const [memUsageTitle, setMemUsageTitle] = useState<any>([]);
+ const [cmdInfoData, setCmdInfoData] = useState<DataItem[]>([]);
+ useEffect(() => {
+ getCacheInfo().then((res) => {
+ if (res.code === 200) {
+ const baseinfo = [];
+ baseinfo.push({
+ col1: 'Redis版本',
+ col2: res.data.info.redis_version,
+ col3: '运行模式',
+ col4: res.data.info.redis_mode === 'standalone' ? '单机' : '集群',
+ col5: '端口',
+ col6: res.data.info.tcp_port,
+ col7: '客户端数',
+ col8: res.data.info.connected_clients,
+ });
+ baseinfo.push({
+ col1: '运行时间(天)',
+ col2: res.data.info.uptime_in_days,
+ col3: '使用内存',
+ col4: res.data.info.used_memory_human,
+ col5: '使用CPU',
+ col6: `${res.data.info.used_cpu_user_children}%`,
+ col7: '内存配置',
+ col8: res.data.info.maxmemory_human,
+ });
+ baseinfo.push({
+ col1: 'AOF是否开启',
+ col2: res.data.info.aof_enabled === '0' ? '否' : '是',
+ col3: 'RDB是否成功',
+ col4: res.data.info.rdb_last_bgsave_status,
+ col5: 'Key数量',
+ col6: res.data.dbSize,
+ col7: '网络入口/出口',
+ col8: `${res.data.info.instantaneous_input_kbps}/${res.data.info.instantaneous_output_kbps}kps`,
+ });
+ setBaseInfoData(baseinfo);
+
+ const data: VisitDataType[] = res.data.commandStats.map((item) => {
+ return {
+ x: item.name,
+ y: Number(item.value),
+ };
+ });
+
+ setCmdInfoData(data);
+ setMemUsageTitle(res.data.info.used_memory_human);
+ setMemUsage(res.data.info.used_memory / res.data.info.total_system_memory);
+ }
+ });
+ }, []);
+
+ return (
+ <div>
+ <Row gutter={[24, 24]}>
+ <Col span={24}>
+ <Card title="基本信息" className={styles.card}>
+ <Table
+ rowKey="col1"
+ pagination={false}
+ showHeader={false}
+ dataSource={baseInfoData}
+ columns={columns}
+ />
+ </Card>
+ </Col>
+ </Row>
+ <Row gutter={[24, 24]}>
+ <Col span={12}>
+ <Card title="命令统计" className={styles.card}>
+ <Pie
+ height={320}
+ radius={0.8}
+ innerRadius={0.5}
+ angleField="y"
+ colorField="x"
+ data={cmdInfoData as any}
+ legend={false}
+ label={{
+ position: 'spider',
+ text: (item: { x: number; y: number }) => {
+ return `${item.x}: ${item.y}`;
+ },
+ }}
+ />
+ </Card>
+ </Col>
+ <Col span={12}>
+ <Card title="内存信息" className={styles.card}>
+ <Gauge
+ title={memUsageTitle}
+ height={320}
+ percent={memUsage}
+ formatter={usageFormatter}
+ padding={-16}
+ style={{
+ textContent: () => { return memUsage.toFixed(2).toString() + '%'},
+ }}
+ data={
+ {
+ target: memUsage,
+ total: 100,
+ name: 'score',
+ thresholds: [20, 40, 60, 80, 100],
+ } as any
+ }
+ meta={{
+ color: {
+ range: ['#C3F71F', '#B5E61D', '#FFC90E', '#FF7F27', '#FF2518'],
+ },
+ }}
+ />
+ </Card>
+ </Col>
+ </Row>
+ </div>
+ );
+};
+
+export default CacheInfo;
diff --git a/src/pages/Monitor/Druid/index.tsx b/src/pages/Monitor/Druid/index.tsx
new file mode 100644
index 0000000..55692d2
--- /dev/null
+++ b/src/pages/Monitor/Druid/index.tsx
@@ -0,0 +1,31 @@
+import React, { useEffect } from 'react';
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2023/02/07
+ *
+ * */
+
+const DruidInfo: React.FC = () => {
+ useEffect(() => {
+ const frame = document.getElementById('bdIframe');
+ if (frame) {
+ const deviceWidth = document.documentElement.clientWidth;
+ const deviceHeight = document.documentElement.clientHeight;
+ frame.style.width = `${Number(deviceWidth) - 220}px`;
+ frame.style.height = `${Number(deviceHeight) - 120}px`;
+ }
+ });
+
+ return (
+ <iframe
+ style={{ width: '100%', border: '0px', height: '100%' }}
+ src={`/api/druid/login.html`}
+ id="bdIframe"
+ />
+ // </WrapContent>
+ );
+};
+
+export default DruidInfo;
diff --git a/src/pages/Monitor/Job/detail.tsx b/src/pages/Monitor/Job/detail.tsx
new file mode 100644
index 0000000..e27bfb7
--- /dev/null
+++ b/src/pages/Monitor/Job/detail.tsx
@@ -0,0 +1,131 @@
+import React, { useEffect } from 'react';
+import { Modal, Descriptions, Button } from 'antd';
+import { FormattedMessage, useIntl } from '@umijs/max';
+import { getValueEnumLabel } from '@/utils/options';
+import { DictValueEnumObj } from '@/components/DictTag';
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2023/02/07
+ *
+ * */
+
+export type OperlogFormValueType = Record<string, unknown> & Partial<API.Monitor.Job>;
+
+export type OperlogFormProps = {
+ onCancel: (flag?: boolean, formVals?: OperlogFormValueType) => void;
+ open: boolean;
+ values: Partial<API.Monitor.Job>;
+ statusOptions: DictValueEnumObj;
+};
+
+const OperlogForm: React.FC<OperlogFormProps> = (props) => {
+ const { values, statusOptions } = props;
+
+ useEffect(() => {}, [props]);
+
+ const intl = useIntl();
+
+ const misfirePolicy: any = {
+ '0': '默认策略',
+ '1': '立即执行',
+ '2': '执行一次',
+ '3': '放弃执行',
+ };
+
+ const handleCancel = () => {
+ props.onCancel();
+ };
+
+ return (
+ <Modal
+ width={800}
+ title={intl.formatMessage({
+ id: 'monitor.job.detail',
+ defaultMessage: '操作日志详细信息',
+ })}
+ open={props.open}
+ destroyOnClose
+ onCancel={handleCancel}
+ footer={[
+ <Button key="back" onClick={handleCancel}>
+ 关闭
+ </Button>,
+ ]}
+ >
+ <Descriptions column={24}>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.job.job_id" defaultMessage="任务编号" />}
+ >
+ {values.jobId}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.job.job_name" defaultMessage="任务名称" />}
+ >
+ {values.jobName}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.job.job_group" defaultMessage="任务组名" />}
+ >
+ {values.jobGroup}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.job.concurrent" defaultMessage="是否并发执行" />}
+ >
+ {values.concurrent === '1' ? '禁止' : '允许'}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={
+ <FormattedMessage id="monitor.job.misfire_policy" defaultMessage="计划执行错误策略" />
+ }
+ >
+ {misfirePolicy[values.misfirePolicy ? values.misfirePolicy : '0']}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.job.create_time" defaultMessage="创建时间" />}
+ >
+ {values.createTime?.toString()}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.job.status" defaultMessage="状态" />}
+ >
+ {getValueEnumLabel(statusOptions, values.status, '未知')}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={
+ <FormattedMessage id="monitor.job.next_valid_time" defaultMessage="下次执行时间" />
+ }
+ >
+ {values.nextValidTime}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={
+ <FormattedMessage id="monitor.job.cron_expression" defaultMessage="cron执行表达式" />
+ }
+ >
+ {values.cronExpression}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={
+ <FormattedMessage id="monitor.job.invoke_target" defaultMessage="调用目标字符串" />
+ }
+ >
+ {values.invokeTarget}
+ </Descriptions.Item>
+ </Descriptions>
+ </Modal>
+ );
+};
+
+export default OperlogForm;
diff --git a/src/pages/Monitor/Job/edit.tsx b/src/pages/Monitor/Job/edit.tsx
new file mode 100644
index 0000000..e4bfc74
--- /dev/null
+++ b/src/pages/Monitor/Job/edit.tsx
@@ -0,0 +1,232 @@
+import React, { useEffect } from 'react';
+import {
+ ProForm,
+ ProFormDigit,
+ ProFormText,
+ ProFormTextArea,
+ ProFormRadio,
+ ProFormSelect,
+ ProFormCaptcha,
+} from '@ant-design/pro-components';
+import { Form, Modal } from 'antd';
+import { useIntl, FormattedMessage } from '@umijs/max';
+import { DictOptionType, DictValueEnumObj } from '@/components/DictTag';
+
+/**
+ * 定时任务调度 Edit Form
+ *
+ * @author whiteshader
+ * @date 2023-02-07
+ */
+
+export type JobFormData = Record<string, unknown> & Partial<API.Monitor.Job>;
+
+export type JobFormProps = {
+ onCancel: (flag?: boolean, formVals?: JobFormData) => void;
+ onSubmit: (values: JobFormData) => Promise<void>;
+ open: boolean;
+ values: Partial<API.Monitor.Job>;
+ jobGroupOptions: DictOptionType[];
+ statusOptions: DictValueEnumObj;
+};
+
+const JobForm: React.FC<JobFormProps> = (props) => {
+ const [form] = Form.useForm();
+ const { jobGroupOptions, statusOptions } = props;
+
+ useEffect(() => {
+ form.resetFields();
+ form.setFieldsValue({
+ jobId: props.values.jobId,
+ jobName: props.values.jobName,
+ jobGroup: props.values.jobGroup,
+ invokeTarget: props.values.invokeTarget,
+ cronExpression: props.values.cronExpression,
+ misfirePolicy: props.values.misfirePolicy,
+ concurrent: props.values.concurrent,
+ status: props.values.status,
+ createBy: props.values.createBy,
+ createTime: props.values.createTime,
+ updateBy: props.values.updateBy,
+ updateTime: props.values.updateTime,
+ remark: props.values.remark,
+ });
+ }, [form, props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ form.resetFields();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit(values as JobFormData);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'monitor.job.title',
+ defaultMessage: '编辑定时任务调度',
+ })}
+ open={props.open}
+ forceRender
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ form={form}
+ grid={true}
+ submitter={false}
+ layout="horizontal"
+ onFinish={handleFinish}>
+ <ProFormDigit
+ name="jobId"
+ label={intl.formatMessage({
+ id: 'monitor.job.job_id',
+ defaultMessage: '任务编号',
+ })}
+ colProps={{ md: 24 }}
+ placeholder="请输入任务编号"
+ disabled
+ hidden={true}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入任务编号!" defaultMessage="请输入任务编号!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="jobName"
+ label={intl.formatMessage({
+ id: 'monitor.job.job_name',
+ defaultMessage: '任务名称',
+ })}
+ colProps={{ md: 24 }}
+ placeholder="请输入任务名称"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入任务名称!" defaultMessage="请输入任务名称!" />,
+ },
+ ]}
+ />
+ <ProFormSelect
+ name="jobGroup"
+ options={jobGroupOptions}
+ label={intl.formatMessage({
+ id: 'monitor.job.job_group',
+ defaultMessage: '任务组名',
+ })}
+ colProps={{ md: 24 }}
+ placeholder="请输入任务组名"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入任务组名!" defaultMessage="请输入任务组名!" />,
+ },
+ ]}
+ />
+ <ProFormTextArea
+ name="invokeTarget"
+ label={intl.formatMessage({
+ id: 'monitor.job.invoke_target',
+ defaultMessage: '调用目标字符串',
+ })}
+ colProps={{ md: 24 }}
+ placeholder="请输入调用目标字符串"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入调用目标字符串!" defaultMessage="请输入调用目标字符串!" />,
+ },
+ ]}
+ />
+ <ProFormCaptcha
+ name="cronExpression"
+ label={intl.formatMessage({
+ id: 'monitor.job.cron_expression',
+ defaultMessage: 'cron执行表达式',
+ })}
+ captchaTextRender={() => "生成表达式"}
+ onGetCaptcha={() => {
+ // form.setFieldValue('cronExpression', '0/20 * * * * ?');
+ return new Promise((resolve, reject) => {
+ reject();
+ });
+ }}
+ />
+ <ProFormRadio.Group
+ name="misfirePolicy"
+ label={intl.formatMessage({
+ id: 'monitor.job.misfire_policy',
+ defaultMessage: '计划执行错误策略',
+ })}
+ colProps={{ md: 24 }}
+ placeholder="请输入计划执行错误策略"
+ valueEnum={{
+ 0: '立即执行',
+ 1: '执行一次',
+ 3: '放弃执行'
+ }}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入计划执行错误策略!" defaultMessage="请输入计划执行错误策略!" />,
+ },
+ ]}
+ fieldProps={{
+ optionType: "button",
+ buttonStyle: "solid"
+ }}
+ />
+ <ProFormRadio.Group
+ name="concurrent"
+ label={intl.formatMessage({
+ id: 'monitor.job.concurrent',
+ defaultMessage: '是否并发执行',
+ })}
+ colProps={{ md: 24 }}
+ placeholder="请输入是否并发执行"
+ valueEnum={{
+ 0: '允许',
+ 1: '禁止',
+ }}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入是否并发执行!" defaultMessage="请输入是否并发执行!" />,
+ },
+ ]}
+ fieldProps={{
+ optionType: "button",
+ buttonStyle: "solid"
+ }}
+ />
+ <ProFormRadio.Group
+ valueEnum={statusOptions}
+ name="status"
+ label={intl.formatMessage({
+ id: 'monitor.job.status',
+ defaultMessage: '状态',
+ })}
+ colProps={{ md: 24 }}
+ placeholder="请输入状态"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入状态!" defaultMessage="请输入状态!" />,
+ },
+ ]}
+ />
+ </ProForm>
+ </Modal>
+ );
+};
+
+export default JobForm;
diff --git a/src/pages/Monitor/Job/index.tsx b/src/pages/Monitor/Job/index.tsx
new file mode 100644
index 0000000..43030d9
--- /dev/null
+++ b/src/pages/Monitor/Job/index.tsx
@@ -0,0 +1,460 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess, history } from '@umijs/max';
+import { Dropdown, FormInstance, Space } from 'antd';
+import { Button, message, Modal } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined, DownOutlined, EditOutlined } from '@ant-design/icons';
+import { getJobList, removeJob, addJob, updateJob, exportJob, runJob } from '@/services/monitor/job';
+import { getDictSelectOption, getDictValueEnum } from '@/services/system/dict';
+import UpdateForm from './edit';
+import DetailForm from './detail';
+import DictTag from '@/components/DictTag';
+
+/**
+ * 定时任务调度 List Page
+ *
+ * @author whiteshader
+ * @date 2023-02-07
+ */
+
+/**
+ * 添加节点
+ *
+ * @param fields
+ */
+const handleAdd = async (fields: API.Monitor.Job) => {
+ const hide = message.loading('正在添加');
+ try {
+ const resp = await addJob({ ...fields });
+ hide();
+ if (resp.code === 200) {
+ message.success('添加成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('添加失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 更新节点
+ *
+ * @param fields
+ */
+const handleUpdate = async (fields: API.Monitor.Job) => {
+ const hide = message.loading('正在更新');
+ try {
+ const resp = await updateJob(fields);
+ hide();
+ if (resp.code === 200) {
+ message.success('更新成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.Monitor.Job[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ const resp = await removeJob(selectedRows.map((row) => row.jobId).join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleRemoveOne = async (selectedRow: API.Monitor.Job) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRow) return true;
+ try {
+ const params = [selectedRow.jobId];
+ const resp = await removeJob(params.join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+/**
+ * 导出数据
+ *
+ */
+const handleExport = async () => {
+ const hide = message.loading('正在导出');
+ try {
+ await exportJob();
+ hide();
+ message.success('导出成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('导出失败,请重试');
+ return false;
+ }
+};
+
+
+const JobTableList: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+ const [detailModalVisible, setDetailModalVisible] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<API.Monitor.Job>();
+ const [selectedRows, setSelectedRows] = useState<API.Monitor.Job[]>([]);
+
+ const [jobGroupOptions, setJobGroupOptions] = useState<any>([]);
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+ getDictSelectOption('sys_job_group').then((data) => {
+ setJobGroupOptions(data);
+ });
+ getDictValueEnum('sys_normal_disable').then((data) => {
+ setStatusOptions(data);
+ });
+ }, []);
+
+ const columns: ProColumns<API.Monitor.Job>[] = [
+ {
+ title: <FormattedMessage id="monitor.job.job_id" defaultMessage="任务编号" />,
+ dataIndex: 'jobId',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.job.job_name" defaultMessage="任务名称" />,
+ dataIndex: 'jobName',
+ valueType: 'text',
+ render: (dom, record) => {
+ return (
+ <a
+ onClick={() => {
+ setDetailModalVisible(true);
+ setCurrentRow(record);
+ }}
+ >
+ {dom}
+ </a>
+ );
+ },
+ },
+ {
+ title: <FormattedMessage id="monitor.job.job_group" defaultMessage="任务组名" />,
+ dataIndex: 'jobGroup',
+ valueType: 'text',
+ valueEnum: jobGroupOptions,
+ render: (_, record) => {
+ return (<DictTag options={jobGroupOptions} value={record.jobGroup} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="monitor.job.invoke_target" defaultMessage="调用目标字符串" />,
+ dataIndex: 'invokeTarget',
+ valueType: 'textarea',
+ },
+ {
+ title: <FormattedMessage id="monitor.job.cron_expression" defaultMessage="cron执行表达式" />,
+ dataIndex: 'cronExpression',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.job.status" defaultMessage="状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (<DictTag enums={statusOptions} value={record.status} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '220px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ icon = <EditOutlined />
+ hidden={!access.hasPerms('monitor:job:edit')}
+ onClick={() => {
+ setModalVisible(true);
+ setCurrentRow(record);
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ danger
+ key="batchRemove"
+ icon = <DeleteOutlined />
+ hidden={!access.hasPerms('monitor:job:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemoveOne(record);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ <Dropdown
+ key="more"
+ menu={{
+ items: [
+ {
+ label: '执行一次',
+ key: 'runOnce',
+ },
+ {
+ label: '详细',
+ key: 'detail',
+ },
+ {
+ label: '历史',
+ key: 'log',
+ },
+ ],
+ onClick: ({ key }) => {
+ if (key === 'runOnce') {
+ Modal.confirm({
+ title: '警告',
+ content: '确认要立即执行一次?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await runJob(record.jobId, record.jobGroup);
+ if (success) {
+ message.success('执行成功');
+ }
+ },
+ });
+ }
+ else if (key === 'detail') {
+ setDetailModalVisible(true);
+ setCurrentRow(record);
+ }
+ else if( key === 'log') {
+ history.push(`/monitor/job-log/index/${record.jobId}`);
+ }
+ }
+ }}
+ >
+ <a className="ant-dropdown-link" onClick={(e) => e.preventDefault()}>
+ <Space>
+ <DownOutlined />
+ 更多
+ </Space>
+ </a>
+ </Dropdown>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.Monitor.Job>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="jobId"
+ key="jobList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('monitor:job:add')}
+ onClick={async () => {
+ setCurrentRow(undefined);
+ setModalVisible(true);
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('monitor:job:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ <Button
+ type="primary"
+ key="export"
+ hidden={!access.hasPerms('monitor:job:export')}
+ onClick={async () => {
+ handleExport();
+ }}
+ >
+ <PlusOutlined />
+ <FormattedMessage id="pages.searchTable.export" defaultMessage="导出" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getJobList({ ...params } as API.Monitor.JobListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('monitor:job:del')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <UpdateForm
+ onSubmit={async (values) => {
+ let success = false;
+ if (values.jobId) {
+ success = await handleUpdate({ ...values } as API.Monitor.Job);
+ } else {
+ success = await handleAdd({ ...values } as API.Monitor.Job);
+ }
+ if (success) {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ }}
+ open={modalVisible}
+ values={currentRow || {}}
+ jobGroupOptions={jobGroupOptions||{}}
+ statusOptions={statusOptions}
+ />
+ <DetailForm
+ onCancel={() => {
+ setDetailModalVisible(false);
+ setCurrentRow(undefined);
+ }}
+ open={detailModalVisible}
+ values={currentRow || {}}
+ statusOptions={statusOptions}
+ />
+ </PageContainer>
+ );
+};
+
+export default JobTableList;
diff --git a/src/pages/Monitor/JobLog/detail.tsx b/src/pages/Monitor/JobLog/detail.tsx
new file mode 100644
index 0000000..560889c
--- /dev/null
+++ b/src/pages/Monitor/JobLog/detail.tsx
@@ -0,0 +1,105 @@
+import { getValueEnumLabel } from '@/utils/options';
+import { FormattedMessage, useIntl } from '@umijs/max';
+import { Descriptions, Modal } from 'antd';
+import React, { useEffect } from 'react';
+import { DictValueEnumObj } from '@/components/DictTag';
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2021/09/16
+ *
+ * */
+
+export type JobLogFormValueType = Record<string, unknown> & Partial<API.Monitor.JobLog>;
+
+export type JobLogFormProps = {
+ onCancel: (flag?: boolean, formVals?: JobLogFormValueType) => void;
+ open: boolean;
+ values: Partial<API.Monitor.JobLog>;
+ statusOptions: DictValueEnumObj;
+ jobGroupOptions: DictValueEnumObj;
+};
+
+const JobLogDetailForm: React.FC<JobLogFormProps> = (props) => {
+
+ const { values, statusOptions, jobGroupOptions } = props;
+
+ useEffect(() => {
+ }, []);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'monitor.job.log.title',
+ defaultMessage: '定时任务调度日志',
+ })}
+ open={props.open}
+ forceRender
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <Descriptions column={24}>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.job.job_id" defaultMessage="任务编号" />}
+ >
+ {values.jobLogId}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.job.create_time" defaultMessage="执行时间" />}
+ >
+ {values.createTime?.toString()}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.job.job_name" defaultMessage="任务名称" />}
+ >
+ {values.jobName}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.job.job_group" defaultMessage="任务组名" />}
+ >
+ {getValueEnumLabel(jobGroupOptions, values.jobGroup, '无')}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={<FormattedMessage id="monitor.job.invoke_target" defaultMessage="调用目标" />}
+ >
+ {values.invokeTarget}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={<FormattedMessage id="monitor.job.log.job_message" defaultMessage="日志信息" />}
+ >
+ {values.jobMessage}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={<FormattedMessage id="monitor.job.log.exception_info" defaultMessage="异常信息" />}
+ >
+ {values.exceptionInfo}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.job.status" defaultMessage="执行状态" />}
+ >
+ {getValueEnumLabel(statusOptions, values.status, '未知')}
+ </Descriptions.Item>
+ </Descriptions>
+ </Modal>
+ );
+};
+
+export default JobLogDetailForm;
diff --git a/src/pages/Monitor/JobLog/index.tsx b/src/pages/Monitor/JobLog/index.tsx
new file mode 100644
index 0000000..46e3313
--- /dev/null
+++ b/src/pages/Monitor/JobLog/index.tsx
@@ -0,0 +1,343 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess, useParams, history } from '@umijs/max';
+import type { FormInstance } from 'antd';
+import { Button, message, Modal } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
+import { getJobLogList, removeJobLog, exportJobLog } from '@/services/monitor/jobLog';
+import DetailForm from './detail';
+import { getDictValueEnum } from '@/services/system/dict';
+import { getJob } from '@/services/monitor/job';
+import DictTag from '@/components/DictTag';
+
+/**
+ * 定时任务调度日志 List Page
+ *
+ * @author whiteshader
+ * @date 2023-02-07
+ */
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.Monitor.JobLog[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ const resp = await removeJobLog(selectedRows.map((row) => row.jobLogId).join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleRemoveOne = async (selectedRow: API.Monitor.JobLog) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRow) return true;
+ try {
+ const params = [selectedRow.jobLogId];
+ const resp = await removeJobLog(params.join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+/**
+ * 清空日志数据
+ *
+ */
+const handleExport = async () => {
+ const hide = message.loading('正在导出');
+ try {
+ await exportJobLog();
+ hide();
+ message.success('导出成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('导出失败,请重试');
+ return false;
+ }
+};
+
+
+const JobLogTableList: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+
+ const [modalOpen, setModalOpen] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<API.Monitor.JobLog>();
+ const [selectedRows, setSelectedRows] = useState<API.Monitor.JobLog[]>([]);
+
+ const [jobGroupOptions, setJobGroupOptions] = useState<any>([]);
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const [queryParams, setQueryParams] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ const params = useParams();
+ if (params.id === undefined) {
+ history.push('/monitor/job');
+ }
+ const jobId = params.id || 0;
+ useEffect(() => {
+ if (jobId !== undefined && jobId !== 0) {
+ getJob(Number(jobId)).then(response => {
+ setQueryParams({
+ jobName: response.data.jobName,
+ jobGroup: response.data.jobGroup
+ });
+ });
+ }
+ getDictValueEnum('sys_job_status').then((data) => {
+ setStatusOptions(data);
+ });
+ getDictValueEnum('sys_job_group').then((data) => {
+ setJobGroupOptions(data);
+ });
+ }, []);
+
+ const columns: ProColumns<API.Monitor.JobLog>[] = [
+ {
+ title: <FormattedMessage id="monitor.job.log.job_log_id" defaultMessage="任务日志编号" />,
+ dataIndex: 'jobLogId',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.job.log.job_name" defaultMessage="任务名称" />,
+ dataIndex: 'jobName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.job.log.job_group" defaultMessage="任务组名" />,
+ dataIndex: 'jobGroup',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.job.log.invoke_target" defaultMessage="调用目标字符串" />,
+ dataIndex: 'invokeTarget',
+ valueType: 'textarea',
+ },
+ {
+ title: <FormattedMessage id="monitor.job.log.job_message" defaultMessage="日志信息" />,
+ dataIndex: 'jobMessage',
+ valueType: 'textarea',
+ },
+ {
+ title: <FormattedMessage id="monitor.job.log.status" defaultMessage="执行状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (<DictTag enums={statusOptions} value={record.status} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="monitor.job.log.create_time" defaultMessage="异常信息" />,
+ dataIndex: 'createTime',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '120px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ hidden={!access.hasPerms('monitor:job-log:edit')}
+ onClick={() => {
+ setModalOpen(true);
+ setCurrentRow(record);
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ danger
+ key="batchRemove"
+ hidden={!access.hasPerms('monitor:job-log:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemoveOne(record);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.Monitor.JobLog>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="jobLogId"
+ key="job-logList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('monitor:job-log:add')}
+ onClick={async () => {
+ setCurrentRow(undefined);
+ setModalOpen(true);
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('monitor:job-log:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ <Button
+ type="primary"
+ key="export"
+ hidden={!access.hasPerms('monitor:job-log:export')}
+ onClick={async () => {
+ handleExport();
+ }}
+ >
+ <PlusOutlined />
+ <FormattedMessage id="pages.searchTable.export" defaultMessage="导出" />
+ </Button>,
+ ]}
+ params={queryParams}
+ request={(params) =>
+ getJobLogList({ ...params } as API.Monitor.JobLogListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('monitor:job-log:del')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <DetailForm
+ onCancel={() => {
+ setModalOpen(false);
+ setCurrentRow(undefined);
+ }}
+ open={modalOpen}
+ values={currentRow || {}}
+ statusOptions={statusOptions}
+ jobGroupOptions={jobGroupOptions}
+ />
+ </PageContainer>
+ );
+};
+
+export default JobLogTableList;
diff --git a/src/pages/Monitor/Logininfor/edit.tsx b/src/pages/Monitor/Logininfor/edit.tsx
new file mode 100644
index 0000000..5243191
--- /dev/null
+++ b/src/pages/Monitor/Logininfor/edit.tsx
@@ -0,0 +1,216 @@
+import React, { useEffect } from 'react';
+import {
+ ProForm,
+ ProFormDigit,
+ ProFormText,
+ ProFormRadio,
+ ProFormTimePicker,
+ } from '@ant-design/pro-components';
+import { Form, Modal} from 'antd';
+import { useIntl, FormattedMessage } from '@umijs/max';
+import { DictValueEnumObj } from '@/components/DictTag';
+
+export type LogininforFormData = Record<string, unknown> & Partial<API.Monitor.Logininfor>;
+
+export type LogininforFormProps = {
+ onCancel: (flag?: boolean, formVals?: LogininforFormData) => void;
+ onSubmit: (values: LogininforFormData) => Promise<void>;
+ open: boolean;
+ values: Partial<API.Monitor.Logininfor>;
+ statusOptions: DictValueEnumObj;
+};
+
+const LogininforForm: React.FC<LogininforFormProps> = (props) => {
+ const [form] = Form.useForm();
+
+ const { statusOptions, } = props;
+
+ useEffect(() => {
+ form.resetFields();
+ form.setFieldsValue({
+ infoId: props.values.infoId,
+ userName: props.values.userName,
+ ipaddr: props.values.ipaddr,
+ loginLocation: props.values.loginLocation,
+ browser: props.values.browser,
+ os: props.values.os,
+ status: props.values.status,
+ msg: props.values.msg,
+ loginTime: props.values.loginTime,
+ });
+ }, [form, props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ form.resetFields();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit(values as LogininforFormData);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'system.logininfor.title',
+ defaultMessage: '编辑系统访问记录',
+ })}
+ open={props.open}
+ destroyOnClose
+ forceRender
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ form={form}
+ grid={true}
+ layout="horizontal"
+ onFinish={handleFinish}>
+ <ProFormDigit
+ name="infoId"
+ label={intl.formatMessage({
+ id: 'system.logininfor.info_id',
+ defaultMessage: '访问编号',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入访问编号"
+ disabled
+ hidden={true}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入访问编号!" defaultMessage="请输入访问编号!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="userName"
+ label={intl.formatMessage({
+ id: 'system.logininfor.user_name',
+ defaultMessage: '用户账号',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入用户账号"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入用户账号!" defaultMessage="请输入用户账号!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="ipaddr"
+ label={intl.formatMessage({
+ id: 'system.logininfor.ipaddr',
+ defaultMessage: '登录IP地址',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入登录IP地址"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入登录IP地址!" defaultMessage="请输入登录IP地址!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="loginLocation"
+ label={intl.formatMessage({
+ id: 'system.logininfor.login_location',
+ defaultMessage: '登录地点',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入登录地点"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入登录地点!" defaultMessage="请输入登录地点!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="browser"
+ label={intl.formatMessage({
+ id: 'system.logininfor.browser',
+ defaultMessage: '浏览器类型',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入浏览器类型"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入浏览器类型!" defaultMessage="请输入浏览器类型!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="os"
+ label={intl.formatMessage({
+ id: 'system.logininfor.os',
+ defaultMessage: '操作系统',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入操作系统"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入操作系统!" defaultMessage="请输入操作系统!" />,
+ },
+ ]}
+ />
+ <ProFormRadio.Group
+ valueEnum={statusOptions}
+ name="status"
+ label={intl.formatMessage({
+ id: 'system.logininfor.status',
+ defaultMessage: '登录状态',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入登录状态"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入登录状态!" defaultMessage="请输入登录状态!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="msg"
+ label={intl.formatMessage({
+ id: 'system.logininfor.msg',
+ defaultMessage: '提示消息',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入提示消息"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入提示消息!" defaultMessage="请输入提示消息!" />,
+ },
+ ]}
+ />
+ <ProFormTimePicker
+ name="loginTime"
+ label={intl.formatMessage({
+ id: 'system.logininfor.login_time',
+ defaultMessage: '访问时间',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入访问时间"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入访问时间!" defaultMessage="请输入访问时间!" />,
+ },
+ ]}
+ />
+ </ProForm>
+ </Modal>
+ );
+};
+
+export default LogininforForm;
diff --git a/src/pages/Monitor/Logininfor/index.tsx b/src/pages/Monitor/Logininfor/index.tsx
new file mode 100644
index 0000000..5a39281
--- /dev/null
+++ b/src/pages/Monitor/Logininfor/index.tsx
@@ -0,0 +1,335 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess } from '@umijs/max';
+import type { FormInstance } from 'antd';
+import { Button, message, Modal } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined, UnlockOutlined } from '@ant-design/icons';
+import { getLogininforList, removeLogininfor, exportLogininfor, unlockLogininfor, cleanLogininfor } from '@/services/monitor/logininfor';
+import DictTag from '@/components/DictTag';
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.Monitor.Logininfor[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ const resp = await removeLogininfor(selectedRows.map((row) => row.infoId).join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleClean = async () => {
+ const hide = message.loading('请稍候');
+ try {
+ const resp = await cleanLogininfor();
+ hide();
+ if (resp.code === 200) {
+ message.success('清空成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('请求失败,请重试');
+ return false;
+ }
+};
+
+const handleUnlock = async (userName: string) => {
+ const hide = message.loading('正在解锁');
+ try {
+ const resp = await unlockLogininfor(userName);
+ hide();
+ if (resp.code === 200) {
+ message.success('解锁成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('解锁失败,请重试');
+ return false;
+ }
+};
+
+/**
+ * 导出数据
+ *
+ * @param id
+ */
+const handleExport = async () => {
+ const hide = message.loading('正在导出');
+ try {
+ await exportLogininfor();
+ hide();
+ message.success('导出成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('导出失败,请重试');
+ return false;
+ }
+};
+
+
+const LogininforTableList: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+
+ const actionRef = useRef<ActionType>();
+ const [selectedRows, setSelectedRows] = useState<API.Monitor.Logininfor[]>([]);
+
+ const access = useAccess();
+
+ const statusOptions = {
+ 0: {
+ label: '成功',
+ key: '0',
+ value: '0',
+ text: '成功',
+ status: 'success',
+ listClass: 'success'
+ },
+ 1: {
+ label: '失败',
+ key: '1',
+ value: '1',
+ text: '失败',
+ status: 'error',
+ listClass: 'danger'
+ },
+ };
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+
+ }, []);
+
+ const columns: ProColumns<API.Monitor.Logininfor>[] = [
+ {
+ title: <FormattedMessage id="monitor.logininfor.info_id" defaultMessage="访问编号" />,
+ dataIndex: 'infoId',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.user_name" defaultMessage="用户账号" />,
+ dataIndex: 'userName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.ipaddr" defaultMessage="登录IP地址" />,
+ dataIndex: 'ipaddr',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.login_location" defaultMessage="登录地点" />,
+ dataIndex: 'loginLocation',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.browser" defaultMessage="浏览器类型" />,
+ dataIndex: 'browser',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.os" defaultMessage="操作系统" />,
+ dataIndex: 'os',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.status" defaultMessage="登录状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ render: (_, record) => {
+ return (<DictTag enums={statusOptions} value={record.status} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.msg" defaultMessage="提示消息" />,
+ dataIndex: 'msg',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.login_time" defaultMessage="访问时间" />,
+ dataIndex: 'loginTime',
+ valueType: 'dateTime',
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.Monitor.Logininfor>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="infoId"
+ key="logininforList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('monitor:logininfor:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ <Button
+ type="primary"
+ key="clean"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('monitor:logininfor:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认清空所有数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleClean();
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.cleanAll" defaultMessage="清空" />
+ </Button>,
+ <Button
+ type="primary"
+ key="unlock"
+ hidden={selectedRows?.length === 0 || !access.hasPerms('monitor:logininfor:unlock')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认解锁该用户的数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleUnlock(selectedRows[0].userName);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <UnlockOutlined />
+ <FormattedMessage id="monitor.logininfor.unlock" defaultMessage="解锁" />
+ </Button>,
+ <Button
+ type="primary"
+ key="export"
+ hidden={!access.hasPerms('monitor:logininfor:export')}
+ onClick={async () => {
+ handleExport();
+ }}
+ >
+ <PlusOutlined />
+ <FormattedMessage id="pages.searchTable.export" defaultMessage="导出" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getLogininforList({ ...params } as API.Monitor.LogininforListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('monitor:logininfor:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ </PageContainer>
+ );
+};
+
+export default LogininforTableList;
diff --git a/src/pages/Monitor/Online/index.tsx b/src/pages/Monitor/Online/index.tsx
new file mode 100644
index 0000000..2f81e3b
--- /dev/null
+++ b/src/pages/Monitor/Online/index.tsx
@@ -0,0 +1,162 @@
+import type { FormInstance } from 'antd';
+import { Button, message, Modal } from 'antd';
+import React, { useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess } from '@umijs/max';
+import { getOnlineUserList, forceLogout } from '@/services/monitor/online';
+import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
+import { DeleteOutlined } from '@ant-design/icons';
+
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2023/02/07
+ *
+ * */
+
+
+const handleForceLogout = async (selectedRow: API.Monitor.OnlineUserType) => {
+ const hide = message.loading('正在强制下线');
+ try {
+ await forceLogout(selectedRow.tokenId);
+ hide();
+ message.success('强制下线成功,即将刷新');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('强制下线失败,请重试');
+ return false;
+ }
+};
+
+const OnlineUserTableList: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+ const actionRef = useRef<ActionType>();
+ const access = useAccess();
+ const intl = useIntl();
+
+ useEffect(() => {}, []);
+
+ const columns: ProColumns<API.Monitor.OnlineUserType>[] = [
+ {
+ title: <FormattedMessage id="monitor.online.user.token_id" defaultMessage="会话编号" />,
+ dataIndex: 'tokenId',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.online.user.user_name" defaultMessage="用户账号" />,
+ dataIndex: 'userName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.online.user.dept_name" defaultMessage="部门名称" />,
+ dataIndex: 'deptName',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.online.user.ipaddr" defaultMessage="登录IP地址" />,
+ dataIndex: 'ipaddr',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.online.user.login_location" defaultMessage="登录地点" />,
+ dataIndex: 'loginLocation',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.online.user.browser" defaultMessage="浏览器类型" />,
+ dataIndex: 'browser',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.online.user.os" defaultMessage="操作系统" />,
+ dataIndex: 'os',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.online.user.login_time" defaultMessage="登录时间" />,
+ dataIndex: 'loginTime',
+ valueType: 'dateRange',
+ render: (_, record) => <span>{record.loginTime}</span>,
+ hideInSearch: true,
+ search: {
+ transform: (value) => {
+ return {
+ 'params[beginTime]': value[0],
+ 'params[endTime]': value[1],
+ };
+ },
+ },
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '60px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ danger
+ key="batchRemove"
+ icon=<DeleteOutlined />
+ hidden={!access.hasPerms('monitor:online:forceLogout')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '强踢',
+ content: '确定强制将该用户踢下线吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleForceLogout(record);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 强退
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.Monitor.OnlineUserType>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="tokenId"
+ key="logininforList"
+ search={{
+ labelWidth: 120,
+ }}
+ request={(params) =>
+ getOnlineUserList({ ...params } as API.Monitor.OnlineUserListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ />
+ </div>
+ );
+};
+
+export default OnlineUserTableList;
diff --git a/src/pages/Monitor/Operlog/detail.tsx b/src/pages/Monitor/Operlog/detail.tsx
new file mode 100644
index 0000000..f247522
--- /dev/null
+++ b/src/pages/Monitor/Operlog/detail.tsx
@@ -0,0 +1,113 @@
+import React from 'react';
+import { Descriptions, Modal } from 'antd';
+import { useIntl, FormattedMessage } from '@umijs/max';
+import { DictValueEnumObj } from '@/components/DictTag';
+import { getValueEnumLabel } from '@/utils/options';
+
+export type OperlogFormData = Record<string, unknown> & Partial<API.Monitor.Operlog>;
+
+export type OperlogFormProps = {
+ onCancel: (flag?: boolean, formVals?: OperlogFormData) => void;
+ onSubmit: (values: OperlogFormData) => Promise<void>;
+ open: boolean;
+ values: Partial<API.Monitor.Operlog>;
+ businessTypeOptions: DictValueEnumObj;
+ operatorTypeOptions: DictValueEnumObj;
+ statusOptions: DictValueEnumObj;
+};
+
+const OperlogDetailForm: React.FC<OperlogFormProps> = (props) => {
+
+ const { values, businessTypeOptions, operatorTypeOptions, statusOptions, } = props;
+
+ const intl = useIntl();
+ const handleOk = () => {};
+ const handleCancel = () => {
+ props.onCancel();
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'monitor.operlog.title',
+ defaultMessage: '编辑操作日志记录',
+ })}
+ open={props.open}
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <Descriptions column={24}>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.operlog.module" defaultMessage="操作模块" />}
+ >
+ {`${values.title}/${getValueEnumLabel(businessTypeOptions, values.businessType)}`}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.operlog.request_method" defaultMessage="请求方式" />}
+ >
+ {values.requestMethod}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.operlog.oper_name" defaultMessage="操作人员" />}
+ >
+ {`${values.operName}/${values.operIp}`}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.operlog.operator_type" defaultMessage="操作类别" />}
+ >
+ {getValueEnumLabel(operatorTypeOptions, values.operatorType)}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={<FormattedMessage id="monitor.operlog.method" defaultMessage="方法名称" />}
+ >
+ {values.method}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={<FormattedMessage id="monitor.operlog.oper_url" defaultMessage="请求URL" />}
+ >
+ {values.operUrl}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={<FormattedMessage id="monitor.operlog.oper_param" defaultMessage="请求参数" />}
+ >
+ {values.operParam}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={<FormattedMessage id="monitor.operlog.json_result" defaultMessage="返回参数" />}
+ >
+ {values.jsonResult}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={<FormattedMessage id="monitor.operlog.error_msg" defaultMessage="错误消息" />}
+ >
+ {values.errorMsg}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.operlog.status" defaultMessage="操作状态" />}
+ >
+ {getValueEnumLabel(statusOptions, values.status)}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.operlog.oper_time" defaultMessage="操作时间" />}
+ >
+ {values.operTime?.toString()}
+ </Descriptions.Item>
+ </Descriptions>
+ </Modal>
+ );
+};
+
+export default OperlogDetailForm;
diff --git a/src/pages/Monitor/Operlog/index.tsx b/src/pages/Monitor/Operlog/index.tsx
new file mode 100644
index 0000000..9440bc8
--- /dev/null
+++ b/src/pages/Monitor/Operlog/index.tsx
@@ -0,0 +1,365 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess } from '@umijs/max';
+import type { FormInstance } from 'antd';
+import { Button, message, Modal } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
+import { getOperlogList, removeOperlog, addOperlog, updateOperlog, exportOperlog } from '@/services/monitor/operlog';
+import UpdateForm from './detail';
+import { getDictValueEnum } from '@/services/system/dict';
+import DictTag from '@/components/DictTag';
+
+/**
+ * 添加节点
+ *
+ * @param fields
+ */
+const handleAdd = async (fields: API.Monitor.Operlog) => {
+ const hide = message.loading('正在添加');
+ try {
+ const resp = await addOperlog({ ...fields });
+ hide();
+ if (resp.code === 200) {
+ message.success('添加成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('添加失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 更新节点
+ *
+ * @param fields
+ */
+const handleUpdate = async (fields: API.Monitor.Operlog) => {
+ const hide = message.loading('正在更新');
+ try {
+ const resp = await updateOperlog(fields);
+ hide();
+ if (resp.code === 200) {
+ message.success('更新成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.Monitor.Operlog[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ const resp = await removeOperlog(selectedRows.map((row) => row.operId).join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+
+/**
+ * 导出数据
+ *
+ *
+ */
+const handleExport = async () => {
+ const hide = message.loading('正在导出');
+ try {
+ await exportOperlog();
+ hide();
+ message.success('导出成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('导出失败,请重试');
+ return false;
+ }
+};
+
+
+const OperlogTableList: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<API.Monitor.Operlog>();
+ const [selectedRows, setSelectedRows] = useState<API.Monitor.Operlog[]>([]);
+
+ const [businessTypeOptions, setBusinessTypeOptions] = useState<any>([]);
+ const [operatorTypeOptions, setOperatorTypeOptions] = useState<any>([]);
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+ getDictValueEnum('sys_oper_type', true).then((data) => {
+ setBusinessTypeOptions(data);
+ });
+ getDictValueEnum('sys_oper_type', true).then((data) => {
+ setOperatorTypeOptions(data);
+ });
+ getDictValueEnum('sys_common_status', true).then((data) => {
+ setStatusOptions(data);
+ });
+ }, []);
+
+ const columns: ProColumns<API.Monitor.Operlog>[] = [
+ {
+ title: <FormattedMessage id="monitor.operlog.oper_id" defaultMessage="日志主键" />,
+ dataIndex: 'operId',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.title" defaultMessage="操作模块" />,
+ dataIndex: 'title',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.business_type" defaultMessage="业务类型" />,
+ dataIndex: 'businessType',
+ valueType: 'select',
+ valueEnum: businessTypeOptions,
+ render: (_, record) => {
+ return (<DictTag enums={businessTypeOptions} value={record.businessType} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.request_method" defaultMessage="请求方式" />,
+ dataIndex: 'requestMethod',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.operator_type" defaultMessage="操作类别" />,
+ dataIndex: 'operatorType',
+ valueType: 'select',
+ valueEnum: operatorTypeOptions,
+ render: (_, record) => {
+ return (<DictTag enums={operatorTypeOptions} value={record.operatorType} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.oper_name" defaultMessage="操作人员" />,
+ dataIndex: 'operName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.oper_ip" defaultMessage="主机地址" />,
+ dataIndex: 'operIp',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.oper_location" defaultMessage="操作地点" />,
+ dataIndex: 'operLocation',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.status" defaultMessage="操作状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (<DictTag key="status" enums={statusOptions} value={record.status} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.oper_time" defaultMessage="操作时间" />,
+ dataIndex: 'operTime',
+ valueType: 'dateTime',
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '120px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ hidden={!access.hasPerms('system:operlog:edit')}
+ onClick={() => {
+ setModalVisible(true);
+ setCurrentRow(record);
+ }}
+ >
+ 详细
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.Monitor.Operlog>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="operId"
+ key="operlogList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('system:operlog:add')}
+ onClick={async () => {
+ setCurrentRow(undefined);
+ setModalVisible(true);
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('system:operlog:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ <Button
+ type="primary"
+ key="export"
+ hidden={!access.hasPerms('system:operlog:export')}
+ onClick={async () => {
+ handleExport();
+ }}
+ >
+ <PlusOutlined />
+ <FormattedMessage id="pages.searchTable.export" defaultMessage="导出" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getOperlogList({ ...params } as API.Monitor.OperlogListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('system:operlog:del')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <UpdateForm
+ onSubmit={async (values) => {
+ let success = false;
+ if (values.operId) {
+ success = await handleUpdate({ ...values } as API.Monitor.Operlog);
+ } else {
+ success = await handleAdd({ ...values } as API.Monitor.Operlog);
+ }
+ if (success) {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ }}
+ open={modalVisible}
+ values={currentRow || {}}
+ businessTypeOptions={businessTypeOptions}
+ operatorTypeOptions={operatorTypeOptions}
+ statusOptions={statusOptions}
+ />
+ </PageContainer>
+ );
+};
+
+export default OperlogTableList;
diff --git a/src/pages/Monitor/Server/index.tsx b/src/pages/Monitor/Server/index.tsx
new file mode 100644
index 0000000..9ea3969
--- /dev/null
+++ b/src/pages/Monitor/Server/index.tsx
@@ -0,0 +1,267 @@
+import React, { useEffect, useState } from 'react';
+import { getServerInfo } from '@/services/monitor/server';
+import { Card, Col, Row, Table } from 'antd';
+import { FormattedMessage } from '@umijs/max';
+import styles from './style.less';
+
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2023/02/07
+ *
+ * */
+
+
+const columns = [
+ {
+ title: '属性',
+ dataIndex: 'name',
+ key: 'name',
+ },
+ {
+ title: '值',
+ dataIndex: 'value',
+ key: 'value',
+ },
+];
+
+const memColumns = [
+ {
+ title: '属性',
+ dataIndex: 'name',
+ key: 'name',
+ },
+ {
+ title: '内存',
+ dataIndex: 'mem',
+ key: 'mem',
+ },
+ {
+ title: 'JVM',
+ dataIndex: 'jvm',
+ key: 'jvm',
+ },
+];
+
+const hostColumns = [
+ {
+ title: 'col1',
+ dataIndex: 'col1',
+ key: 'col1',
+ },
+ {
+ title: 'col2',
+ dataIndex: 'col2',
+ key: 'col2',
+ },
+ {
+ title: 'col3',
+ dataIndex: 'col3',
+ key: 'col3',
+ },
+ {
+ title: 'col4',
+ dataIndex: 'col4',
+ key: 'col4',
+ },
+];
+
+const diskColumns = [
+ {
+ title: <FormattedMessage id="monitor.server.disk.dirName" defaultMessage="盘符路径" />,
+ dataIndex: 'dirName',
+ key: 'dirName',
+ },
+ {
+ title: <FormattedMessage id="monitor.server.disk.sysTypeName" defaultMessage="文件系统" />,
+ dataIndex: 'sysTypeName',
+ key: 'sysTypeName',
+ },
+ {
+ title: <FormattedMessage id="monitor.server.disk.typeName" defaultMessage="盘符类型" />,
+ dataIndex: 'typeName',
+ key: 'typeName',
+ },
+ {
+ title: <FormattedMessage id="monitor.server.disk.total" defaultMessage="总大小" />,
+ dataIndex: 'total',
+ key: 'total',
+ },
+ {
+ title: <FormattedMessage id="monitor.server.disk.free" defaultMessage="可用大小" />,
+ dataIndex: 'free',
+ key: 'free',
+ },
+ {
+ title: <FormattedMessage id="monitor.server.disk.used" defaultMessage="已用大小" />,
+ dataIndex: 'used',
+ key: 'used',
+ },
+ {
+ title: <FormattedMessage id="monitor.server.disk.usage" defaultMessage="已用百分比" />,
+ dataIndex: 'usage',
+ key: 'usage',
+ },
+];
+
+const ServerInfo: React.FC = () => {
+ const [cpuData, setCpuData] = useState<API.Monitor.CpuRowType[]>([]);
+ const [memData, setMemData] = useState<API.Monitor.MemRowType[]>([]);
+ const [hostData, setHostData] = useState<any>([]);
+ const [jvmData, setJvmData] = useState<any>([]);
+ const [diskData, setDiskData] = useState<any>([]);
+
+ useEffect(() => {
+ getServerInfo().then((res: API.Monitor.ServerInfoResponseType) => {
+ if (res.code === 200) {
+ // const cpuinfo: CpuRowType[] = [];
+ // Object.keys(res.data.cpu).forEach((item: any) => {
+ // cpuinfo.push({
+ // name: item,
+ // value: res.data.cpu[item],
+ // });
+ // });
+ // setCpuData(cpuinfo);
+
+ const cpuinfo: API.Monitor.CpuRowType[] = [];
+ cpuinfo.push({ name: '核心数', value: res.data.cpu.cpuNum });
+ cpuinfo.push({ name: '用户使用率', value: `${res.data.cpu.used}%` });
+ cpuinfo.push({ name: '系统使用率', value: `${res.data.cpu.sys}%` });
+ cpuinfo.push({ name: '当前空闲率', value: `${res.data.cpu.free}%` });
+
+ setCpuData(cpuinfo);
+
+ const memDatas: API.Monitor.MemRowType[] = [];
+ memDatas.push({
+ name: '总内存',
+ mem: `${res.data.mem.total}G`,
+ jvm: `${res.data.jvm.total}M`,
+ });
+ memDatas.push({
+ name: '已用内存',
+ mem: `${res.data.mem.used}G`,
+ jvm: `${res.data.jvm.used}M`,
+ });
+ memDatas.push({
+ name: '剩余内存',
+ mem: `${res.data.mem.free}G`,
+ jvm: `${res.data.jvm.free}M`,
+ });
+ memDatas.push({
+ name: '使用率',
+ mem: `${res.data.mem.usage}%`,
+ jvm: `${res.data.jvm.usage}%`,
+ });
+ setMemData(memDatas);
+
+ const hostinfo = [];
+ hostinfo.push({
+ col1: '服务器名称',
+ col2: res.data.sys.computerName,
+ col3: '操作系统',
+ col4: res.data.sys.osName,
+ });
+ hostinfo.push({
+ col1: '服务器IP',
+ col2: res.data.sys.computerIp,
+ col3: '系统架构',
+ col4: res.data.sys.osArch,
+ });
+ setHostData(hostinfo);
+
+ const jvminfo = [];
+ jvminfo.push({
+ col1: 'Java名称',
+ col2: res.data.jvm.name,
+ col3: 'Java版本',
+ col4: res.data.jvm.version,
+ });
+ jvminfo.push({
+ col1: '启动时间',
+ col2: res.data.jvm.startTime,
+ col3: '运行时长',
+ col4: res.data.jvm.runTime,
+ });
+ jvminfo.push({
+ col1: '安装路径',
+ col2: res.data.jvm.home,
+ col3: '项目路径',
+ col4: res.data.sys.userDir,
+ });
+ setJvmData(jvminfo);
+
+ const diskinfo = res.data.sysFiles.map((item: API.Monitor.DiskInfoType) => {
+ return {
+ dirName: item.dirName,
+ sysTypeName: item.sysTypeName,
+ typeName: item.typeName,
+ total: item.total,
+ free: item.free,
+ used: item.used,
+ usage: `${item.usage}%`,
+ };
+ });
+ setDiskData(diskinfo);
+ }
+ });
+ }, []);
+
+ return (
+ <div>
+ <Row gutter={[24, 24]}>
+ <Col span={12}>
+ <Card title="CPU" className={styles.card}>
+ <Table rowKey="name" pagination={false} showHeader={false} dataSource={cpuData} columns={columns} />
+ </Card>
+ </Col>
+ <Col span={12}>
+ <Card title="内存" className={styles.card}>
+ <Table
+ rowKey="name"
+ pagination={false}
+ showHeader={false}
+ dataSource={memData}
+ columns={memColumns}
+ />
+ </Card>
+ </Col>
+ </Row>
+ <Row gutter={[16, 16]}>
+ <Col span={24}>
+ <Card title="服务器信息" className={styles.card}>
+ <Table
+ rowKey="col1"
+ pagination={false}
+ showHeader={false}
+ dataSource={hostData}
+ columns={hostColumns}
+ />
+ </Card>
+ </Col>
+ </Row>
+ <Row gutter={[16, 16]}>
+ <Col span={24}>
+ <Card title="Java虚拟机信息" className={styles.card}>
+ <Table
+ rowKey="col1"
+ pagination={false}
+ showHeader={false}
+ dataSource={jvmData}
+ columns={hostColumns}
+ />
+ </Card>
+ </Col>
+ </Row>
+ <Row gutter={[16, 16]}>
+ <Col span={24}>
+ <Card title="磁盘状态" className={styles.card}>
+ <Table rowKey="dirName" pagination={false} dataSource={diskData} columns={diskColumns} />
+ </Card>
+ </Col>
+ </Row>
+ </div>
+ );
+};
+
+export default ServerInfo;
diff --git a/src/pages/Monitor/Server/style.less b/src/pages/Monitor/Server/style.less
new file mode 100644
index 0000000..d244a53
--- /dev/null
+++ b/src/pages/Monitor/Server/style.less
@@ -0,0 +1,11 @@
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2021/09/16
+ *
+ * */
+
+.card {
+ margin-bottom: 12px;
+}
diff --git a/src/pages/System/Config/edit.tsx b/src/pages/System/Config/edit.tsx
new file mode 100644
index 0000000..6cb3942
--- /dev/null
+++ b/src/pages/System/Config/edit.tsx
@@ -0,0 +1,172 @@
+import React, { useEffect } from 'react';
+import {
+ ProForm,
+ ProFormDigit,
+ ProFormText,
+ ProFormTextArea,
+ ProFormRadio,
+ } from '@ant-design/pro-components';
+import { Form, Modal} from 'antd';
+import { useIntl, FormattedMessage } from '@umijs/max';
+import { DictValueEnumObj } from '@/components/DictTag';
+
+export type ConfigFormData = Record<string, unknown> & Partial<API.System.Config>;
+
+export type ConfigFormProps = {
+ onCancel: (flag?: boolean, formVals?: ConfigFormData) => void;
+ onSubmit: (values: ConfigFormData) => Promise<void>;
+ open: boolean;
+ values: Partial<API.System.Config>;
+ configTypeOptions: DictValueEnumObj;
+};
+
+const ConfigForm: React.FC<ConfigFormProps> = (props) => {
+ const [form] = Form.useForm();
+
+ const { configTypeOptions } = props;
+
+ useEffect(() => {
+ form.resetFields();
+ form.setFieldsValue({
+ configId: props.values.configId,
+ configName: props.values.configName,
+ configKey: props.values.configKey,
+ configValue: props.values.configValue,
+ configType: props.values.configType,
+ createBy: props.values.createBy,
+ createTime: props.values.createTime,
+ updateBy: props.values.updateBy,
+ updateTime: props.values.updateTime,
+ remark: props.values.remark,
+ });
+ }, [form, props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit(values as ConfigFormData);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'system.config.title',
+ defaultMessage: '编辑参数配置',
+ })}
+ open={props.open}
+ forceRender
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ form={form}
+ grid={true}
+ submitter={false}
+ layout="horizontal"
+ onFinish={handleFinish}>
+ <ProFormDigit
+ name="configId"
+ label={intl.formatMessage({
+ id: 'system.config.config_id',
+ defaultMessage: '参数主键',
+ })}
+ colProps={{ md: 24 }}
+ placeholder="请输入参数主键"
+ disabled
+ hidden={true}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入参数主键!" defaultMessage="请输入参数主键!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="configName"
+ label={intl.formatMessage({
+ id: 'system.config.config_name',
+ defaultMessage: '参数名称',
+ })}
+ colProps={{ md: 24 }}
+ placeholder="请输入参数名称"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入参数名称!" defaultMessage="请输入参数名称!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="configKey"
+ label={intl.formatMessage({
+ id: 'system.config.config_key',
+ defaultMessage: '参数键名',
+ })}
+ colProps={{ md: 24 }}
+ placeholder="请输入参数键名"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入参数键名!" defaultMessage="请输入参数键名!" />,
+ },
+ ]}
+ />
+ <ProFormTextArea
+ name="configValue"
+ label={intl.formatMessage({
+ id: 'system.config.config_value',
+ defaultMessage: '参数键值',
+ })}
+ colProps={{ md: 24 }}
+ placeholder="请输入参数键值"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入参数键值!" defaultMessage="请输入参数键值!" />,
+ },
+ ]}
+ />
+ <ProFormRadio.Group
+ valueEnum={configTypeOptions}
+ name="configType"
+ label={intl.formatMessage({
+ id: 'system.config.config_type',
+ defaultMessage: '系统内置',
+ })}
+ colProps={{ md: 24 }}
+ placeholder="请输入系统内置"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入系统内置!" defaultMessage="请输入系统内置!" />,
+ },
+ ]}
+ />
+ <ProFormTextArea
+ name="remark"
+ label={intl.formatMessage({
+ id: 'system.config.remark',
+ defaultMessage: '备注',
+ })}
+ colProps={{ md: 24 }}
+ placeholder="请输入备注"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入备注!" defaultMessage="请输入备注!" />,
+ },
+ ]}
+ />
+ </ProForm>
+ </Modal>
+ );
+};
+
+export default ConfigForm;
diff --git a/src/pages/System/Config/index.tsx b/src/pages/System/Config/index.tsx
new file mode 100644
index 0000000..1428891
--- /dev/null
+++ b/src/pages/System/Config/index.tsx
@@ -0,0 +1,397 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess } from '@umijs/max';
+import type { FormInstance } from 'antd';
+import { Button, message, Modal } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined, ReloadOutlined, DownloadOutlined } from '@ant-design/icons';
+import { getConfigList, removeConfig, addConfig, updateConfig, exportConfig, refreshConfigCache } from '@/services/system/config';
+import UpdateForm from './edit';
+import { getDictValueEnum } from '@/services/system/dict';
+import DictTag from '@/components/DictTag';
+
+/**
+ * 添加节点
+ *
+ * @param fields
+ */
+const handleAdd = async (fields: API.System.Config) => {
+ const hide = message.loading('正在添加');
+ try {
+ const resp = await addConfig({ ...fields });
+ hide();
+ if (resp.code === 200) {
+ message.success('添加成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('添加失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 更新节点
+ *
+ * @param fields
+ */
+const handleUpdate = async (fields: API.System.Config) => {
+ const hide = message.loading('正在更新');
+ try {
+ const resp = await updateConfig(fields);
+ hide();
+ if (resp.code === 200) {
+ message.success('更新成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.System.Config[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ const resp = await removeConfig(selectedRows.map((row) => row.configId).join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleRemoveOne = async (selectedRow: API.System.Config) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRow) return true;
+ try {
+ const params = [selectedRow.configId];
+ const resp = await removeConfig(params.join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+/**
+ * 导出数据
+ *
+ *
+ */
+const handleExport = async () => {
+ const hide = message.loading('正在导出');
+ try {
+ await exportConfig();
+ hide();
+ message.success('导出成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('导出失败,请重试');
+ return false;
+ }
+};
+
+const handleRefreshCache = async () => {
+ const hide = message.loading('正在刷新');
+ try {
+ await refreshConfigCache();
+ hide();
+ message.success('刷新成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('刷新失败,请重试');
+ return false;
+ }
+};
+
+const ConfigTableList: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<API.System.Config>();
+ const [selectedRows, setSelectedRows] = useState<API.System.Config[]>([]);
+
+ const [configTypeOptions, setConfigTypeOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+ getDictValueEnum('sys_yes_no').then((data) => {
+ setConfigTypeOptions(data);
+ });
+ }, []);
+
+ const columns: ProColumns<API.System.Config>[] = [
+ {
+ title: <FormattedMessage id="system.config.config_id" defaultMessage="参数主键" />,
+ dataIndex: 'configId',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.config.config_name" defaultMessage="参数名称" />,
+ dataIndex: 'configName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.config.config_key" defaultMessage="参数键名" />,
+ dataIndex: 'configKey',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.config.config_value" defaultMessage="参数键值" />,
+ dataIndex: 'configValue',
+ valueType: 'textarea',
+ },
+ {
+ title: <FormattedMessage id="system.config.config_type" defaultMessage="系统内置" />,
+ dataIndex: 'configType',
+ valueType: 'select',
+ valueEnum: configTypeOptions,
+ render: (_, record) => {
+ return (<DictTag enums={configTypeOptions} value={record.configType} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="system.config.remark" defaultMessage="备注" />,
+ dataIndex: 'remark',
+ valueType: 'textarea',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '120px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ hidden={!access.hasPerms('system:config:edit')}
+ onClick={() => {
+ setModalVisible(true);
+ setCurrentRow(record);
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ danger
+ key="batchRemove"
+ hidden={!access.hasPerms('system:config:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemoveOne(record);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.System.Config>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="configId"
+ key="configList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('system:config:add')}
+ onClick={async () => {
+ setCurrentRow(undefined);
+ setModalVisible(true);
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('system:config:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ <Button
+ type="primary"
+ key="export"
+ hidden={!access.hasPerms('system:config:export')}
+ onClick={async () => {
+ handleExport();
+ }}
+ >
+ <DownloadOutlined />
+ <FormattedMessage id="pages.searchTable.export" defaultMessage="导出" />
+ </Button>,
+ <Button
+ type="primary"
+ key="refresh"
+ danger
+ hidden={!access.hasPerms('system:config:remove')}
+ onClick={async () => {
+ handleRefreshCache();
+ }}
+ >
+ <ReloadOutlined />
+ <FormattedMessage id="system.config.refreshCache" defaultMessage="刷新缓存" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getConfigList({ ...params } as API.System.ConfigListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('system:config:del')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <UpdateForm
+ onSubmit={async (values) => {
+ let success = false;
+ if (values.configId) {
+ success = await handleUpdate({ ...values } as API.System.Config);
+ } else {
+ success = await handleAdd({ ...values } as API.System.Config);
+ }
+ if (success) {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ }}
+ open={modalVisible}
+ values={currentRow || {}}
+ configTypeOptions={configTypeOptions}
+ />
+ </PageContainer>
+ );
+};
+
+export default ConfigTableList;
diff --git a/src/pages/System/Dept/edit.tsx b/src/pages/System/Dept/edit.tsx
new file mode 100644
index 0000000..cb64813
--- /dev/null
+++ b/src/pages/System/Dept/edit.tsx
@@ -0,0 +1,212 @@
+import React, { useEffect } from 'react';
+import {
+ ProForm,
+ ProFormDigit,
+ ProFormText,
+ ProFormRadio,
+ ProFormTreeSelect,
+} from '@ant-design/pro-components';
+import { Form, Modal} from 'antd';
+import { useIntl, FormattedMessage } from '@umijs/max';
+import { DataNode } from 'antd/es/tree';
+import { DictValueEnumObj } from '@/components/DictTag';
+
+export type DeptFormData = Record<string, unknown> & Partial<API.System.Dept>;
+
+export type DeptFormProps = {
+ onCancel: (flag?: boolean, formVals?: DeptFormData) => void;
+ onSubmit: (values: DeptFormData) => Promise<void>;
+ open: boolean;
+ values: Partial<API.System.Dept>;
+ deptTree: DataNode[];
+ statusOptions: DictValueEnumObj;
+};
+
+const DeptForm: React.FC<DeptFormProps> = (props) => {
+ const [form] = Form.useForm();
+
+ const { statusOptions, deptTree } = props;
+
+ useEffect(() => {
+ form.resetFields();
+ form.setFieldsValue({
+ deptId: props.values.deptId,
+ parentId: props.values.parentId,
+ ancestors: props.values.ancestors,
+ deptName: props.values.deptName,
+ orderNum: props.values.orderNum,
+ leader: props.values.leader,
+ phone: props.values.phone,
+ email: props.values.email,
+ status: props.values.status,
+ delFlag: props.values.delFlag,
+ createBy: props.values.createBy,
+ createTime: props.values.createTime,
+ updateBy: props.values.updateBy,
+ updateTime: props.values.updateTime,
+ });
+ }, [form, props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit(values as DeptFormData);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'system.dept.title',
+ defaultMessage: '编辑部门',
+ })}
+ open={props.open}
+ forceRender
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ form={form}
+ grid={true}
+ submitter={false}
+ layout="horizontal"
+ onFinish={handleFinish}>
+ <ProFormDigit
+ name="deptId"
+ label={intl.formatMessage({
+ id: 'system.dept.dept_id',
+ defaultMessage: '部门id',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入部门id"
+ disabled
+ hidden={true}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入部门id!" defaultMessage="请输入部门id!" />,
+ },
+ ]}
+ />
+ <ProFormTreeSelect
+ name="parentId"
+ label={intl.formatMessage({
+ id: 'system.dept.parent_dept',
+ defaultMessage: '上级部门:',
+ })}
+ request={async () => {
+ return deptTree;
+ }}
+ placeholder="请选择上级部门"
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage id="请输入用户昵称!" defaultMessage="请选择上级部门!" />
+ ),
+ },
+ ]}
+ />
+ <ProFormText
+ name="deptName"
+ label={intl.formatMessage({
+ id: 'system.dept.dept_name',
+ defaultMessage: '部门名称',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入部门名称"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入部门名称!" defaultMessage="请输入部门名称!" />,
+ },
+ ]}
+ />
+ <ProFormDigit
+ name="orderNum"
+ label={intl.formatMessage({
+ id: 'system.dept.order_num',
+ defaultMessage: '显示顺序',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入显示顺序"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入显示顺序!" defaultMessage="请输入显示顺序!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="leader"
+ label={intl.formatMessage({
+ id: 'system.dept.leader',
+ defaultMessage: '负责人',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入负责人"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入负责人!" defaultMessage="请输入负责人!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="phone"
+ label={intl.formatMessage({
+ id: 'system.dept.phone',
+ defaultMessage: '联系电话',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入联系电话"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入联系电话!" defaultMessage="请输入联系电话!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="email"
+ label={intl.formatMessage({
+ id: 'system.dept.email',
+ defaultMessage: '邮箱',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入邮箱"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入邮箱!" defaultMessage="请输入邮箱!" />,
+ },
+ ]}
+ />
+ <ProFormRadio.Group
+ valueEnum={statusOptions}
+ name="status"
+ label={intl.formatMessage({
+ id: 'system.dept.status',
+ defaultMessage: '部门状态',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入部门状态"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入部门状态!" defaultMessage="请输入部门状态!" />,
+ },
+ ]}
+ />
+ </ProForm>
+ </Modal>
+ );
+};
+
+export default DeptForm;
diff --git a/src/pages/System/Dept/index.tsx b/src/pages/System/Dept/index.tsx
new file mode 100644
index 0000000..3a1c5de
--- /dev/null
+++ b/src/pages/System/Dept/index.tsx
@@ -0,0 +1,346 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess } from '@umijs/max';
+import type { FormInstance } from 'antd';
+import { Button, message, Modal } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
+import { getDeptList, removeDept, addDept, updateDept, getDeptListExcludeChild } from '@/services/system/dept';
+import UpdateForm from './edit';
+import { getDictValueEnum } from '@/services/system/dict';
+import { buildTreeData } from '@/utils/tree';
+import DictTag from '@/components/DictTag';
+
+/**
+ * 添加节点
+ *
+ * @param fields
+ */
+const handleAdd = async (fields: API.System.Dept) => {
+ const hide = message.loading('正在添加');
+ try {
+ const resp = await addDept({ ...fields });
+ hide();
+ if (resp.code === 200) {
+ message.success('添加成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('添加失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 更新节点
+ *
+ * @param fields
+ */
+const handleUpdate = async (fields: API.System.Dept) => {
+ const hide = message.loading('正在更新');
+ try {
+ const resp = await updateDept(fields);
+ hide();
+ if (resp.code === 200) {
+ message.success('更新成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.System.Dept[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ const resp = await removeDept(selectedRows.map((row) => row.deptId).join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleRemoveOne = async (selectedRow: API.System.Dept) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRow) return true;
+ try {
+ const params = [selectedRow.deptId];
+ const resp = await removeDept(params.join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+
+const DeptTableList: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<API.System.Dept>();
+ const [selectedRows, setSelectedRows] = useState<API.System.Dept[]>([]);
+
+ const [deptTree, setDeptTree] = useState<any>([]);
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+ getDictValueEnum('sys_normal_disable').then((data) => {
+ setStatusOptions(data);
+ });
+ }, []);
+
+ const columns: ProColumns<API.System.Dept>[] = [
+ {
+ title: <FormattedMessage id="system.dept.dept_name" defaultMessage="部门名称" />,
+ dataIndex: 'deptName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.dept.order_num" defaultMessage="显示顺序" />,
+ dataIndex: 'orderNum',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.dept.status" defaultMessage="部门状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (<DictTag enums={statusOptions} value={record.status} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '220px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ hidden={!access.hasPerms('system:dept:edit')}
+ onClick={() => {
+ getDeptListExcludeChild(record.deptId).then((res) => {
+ if (res.code === 200) {
+ let depts = buildTreeData(res.data, 'deptId', 'deptName', '', '', '');
+ if(depts.length === 0) {
+ depts = [{ id: 0, title: '无上级', children: undefined, key: 0, value: 0 }];
+ }
+ setDeptTree(depts);
+ setModalVisible(true);
+ setCurrentRow(record);
+ } else {
+ message.warning(res.msg);
+ }
+ });
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ danger
+ key="batchRemove"
+ hidden={!access.hasPerms('system:dept:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemoveOne(record);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.System.Dept>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="deptId"
+ key="deptList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('system:dept:add')}
+ onClick={async () => {
+ getDeptList().then((res) => {
+ if (res.code === 200) {
+ setDeptTree(buildTreeData(res.data, 'deptId', 'deptName', '', '', ''));
+ setCurrentRow(undefined);
+ setModalVisible(true);
+ } else {
+ message.warning(res.msg);
+ }
+ });
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('system:dept:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() {},
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getDeptList({ ...params } as API.System.DeptListParams).then((res) => {
+ const result = {
+ data: buildTreeData(res.data, 'deptId', '', '', '', ''),
+ total: res.data.length,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('system:dept:del')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <UpdateForm
+ onSubmit={async (values) => {
+ let success = false;
+ if (values.deptId) {
+ success = await handleUpdate({ ...values } as API.System.Dept);
+ } else {
+ success = await handleAdd({ ...values } as API.System.Dept);
+ }
+ if (success) {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ }}
+ open={modalVisible}
+ values={currentRow || {}}
+ deptTree={deptTree}
+ statusOptions={statusOptions}
+ />
+ </PageContainer>
+ );
+};
+
+export default DeptTableList;
diff --git a/src/pages/System/Dict/edit.tsx b/src/pages/System/Dict/edit.tsx
new file mode 100644
index 0000000..882c1cb
--- /dev/null
+++ b/src/pages/System/Dict/edit.tsx
@@ -0,0 +1,152 @@
+import React, { useEffect } from 'react';
+import {
+ ProForm,
+ ProFormDigit,
+ ProFormText,
+ ProFormRadio,
+ ProFormTextArea,
+ } from '@ant-design/pro-components';
+import { Form, Modal} from 'antd';
+import { useIntl, FormattedMessage } from '@umijs/max';
+import { DictValueEnumObj } from '@/components/DictTag';
+
+export type DictTypeFormData = Record<string, unknown> & Partial<API.System.DictType>;
+
+export type DictTypeFormProps = {
+ onCancel: (flag?: boolean, formVals?: DictTypeFormData) => void;
+ onSubmit: (values: DictTypeFormData) => Promise<void>;
+ open: boolean;
+ values: Partial<API.System.DictType>;
+ statusOptions: DictValueEnumObj;
+};
+
+const DictTypeForm: React.FC<DictTypeFormProps> = (props) => {
+ const [form] = Form.useForm();
+
+ const { statusOptions } = props;
+
+ useEffect(() => {
+ form.resetFields();
+ form.setFieldsValue({
+ dictId: props.values.dictId,
+ dictName: props.values.dictName,
+ dictType: props.values.dictType,
+ status: props.values.status,
+ createBy: props.values.createBy,
+ createTime: props.values.createTime,
+ updateBy: props.values.updateBy,
+ updateTime: props.values.updateTime,
+ remark: props.values.remark,
+ });
+ }, [form, props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit(values as DictTypeFormData);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'system.dict.title',
+ defaultMessage: '编辑字典类型',
+ })}
+ open={props.open}
+ forceRender
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ form={form}
+ grid={true}
+ submitter={false}
+ layout="horizontal"
+ onFinish={handleFinish}>
+ <ProFormDigit
+ name="dictId"
+ label={intl.formatMessage({
+ id: 'system.dict.dict_id',
+ defaultMessage: '字典主键',
+ })}
+ placeholder="请输入字典主键"
+ disabled
+ hidden={true}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入字典主键!" defaultMessage="请输入字典主键!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="dictName"
+ label={intl.formatMessage({
+ id: 'system.dict.dict_name',
+ defaultMessage: '字典名称',
+ })}
+ placeholder="请输入字典名称"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入字典名称!" defaultMessage="请输入字典名称!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="dictType"
+ label={intl.formatMessage({
+ id: 'system.dict.dict_type',
+ defaultMessage: '字典类型',
+ })}
+ placeholder="请输入字典类型"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入字典类型!" defaultMessage="请输入字典类型!" />,
+ },
+ ]}
+ />
+ <ProFormRadio.Group
+ valueEnum={statusOptions}
+ name="status"
+ label={intl.formatMessage({
+ id: 'system.dict.status',
+ defaultMessage: '状态',
+ })}
+ initialValue={'0'}
+ placeholder="请输入状态"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入状态!" defaultMessage="请输入状态!" />,
+ },
+ ]}
+ />
+ <ProFormTextArea
+ name="remark"
+ label={intl.formatMessage({
+ id: 'system.dict.remark',
+ defaultMessage: '备注',
+ })}
+ placeholder="请输入备注"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入备注!" defaultMessage="请输入备注!" />,
+ },
+ ]}
+ />
+ </ProForm>
+ </Modal>
+ );
+};
+
+export default DictTypeForm;
diff --git a/src/pages/System/Dict/index.tsx b/src/pages/System/Dict/index.tsx
new file mode 100644
index 0000000..ccdbc2a
--- /dev/null
+++ b/src/pages/System/Dict/index.tsx
@@ -0,0 +1,394 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess, history } from '@umijs/max';
+import type { FormInstance } from 'antd';
+import { Button, message, Modal } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
+import { getDictTypeList, removeDictType, addDictType, updateDictType, exportDictType } from '@/services/system/dict';
+import UpdateForm from './edit';
+import { getDictValueEnum } from '@/services/system/dict';
+import DictTag from '@/components/DictTag';
+
+/**
+ * 添加节点
+ *
+ * @param fields
+ */
+const handleAdd = async (fields: API.System.DictType) => {
+ const hide = message.loading('正在添加');
+ try {
+ const resp = await addDictType({ ...fields });
+ hide();
+ if (resp.code === 200) {
+ message.success('添加成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('添加失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 更新节点
+ *
+ * @param fields
+ */
+const handleUpdate = async (fields: API.System.DictType) => {
+ const hide = message.loading('正在更新');
+ try {
+ const resp = await updateDictType(fields);
+ hide();
+ if (resp.code === 200) {
+ message.success('更新成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.System.DictType[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ const resp = await removeDictType(selectedRows.map((row) => row.dictId).join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleRemoveOne = async (selectedRow: API.System.DictType) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRow) return true;
+ try {
+ const params = [selectedRow.dictId];
+ const resp = await removeDictType(params.join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+/**
+ * 导出数据
+ *
+ *
+ */
+const handleExport = async () => {
+ const hide = message.loading('正在导出');
+ try {
+ await exportDictType();
+ hide();
+ message.success('导出成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('导出失败,请重试');
+ return false;
+ }
+};
+
+
+const DictTableList: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<API.System.DictType>();
+ const [selectedRows, setSelectedRows] = useState<API.System.DictType[]>([]);
+
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+ getDictValueEnum('sys_normal_disable').then((data) => {
+ setStatusOptions(data);
+ });
+ }, []);
+
+ const columns: ProColumns<API.System.DictType>[] = [
+ {
+ title: <FormattedMessage id="system.dict.dict_id" defaultMessage="字典编号" />,
+ dataIndex: 'dictId',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.dict.dict_name" defaultMessage="字典名称" />,
+ dataIndex: 'dictName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.dict.dict_type" defaultMessage="字典类型" />,
+ dataIndex: 'dictType',
+ valueType: 'text',
+ render: (dom, record) => {
+ return (
+ <a
+ onClick={() => {
+ history.push(`/system/dict-data/index/${record.dictId}`);
+ }}
+ >
+ {dom}
+ </a>
+ );
+ },
+ },
+ {
+ title: <FormattedMessage id="system.dict.status" defaultMessage="状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (<DictTag enums={statusOptions} value={record.status} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="system.dict.remark" defaultMessage="备注" />,
+ dataIndex: 'remark',
+ valueType: 'textarea',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.role.create_time" defaultMessage="创建时间" />,
+ dataIndex: 'createTime',
+ valueType: 'dateRange',
+ render: (_, record) => {
+ return (<span>{record.createTime.toString()} </span>);
+ },
+ search: {
+ transform: (value) => {
+ return {
+ 'params[beginTime]': value[0],
+ 'params[endTime]': value[1],
+ };
+ },
+ },
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '220px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ hidden={!access.hasPerms('system:dictType:edit')}
+ onClick={() => {
+ setModalVisible(true);
+ setCurrentRow(record);
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ danger
+ key="batchRemove"
+ hidden={!access.hasPerms('system:dictType:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemoveOne(record);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.System.DictType>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="dictId"
+ key="dictTypeList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('system:dictType:add')}
+ onClick={async () => {
+ setCurrentRow(undefined);
+ setModalVisible(true);
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('system:dictType:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() {},
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ <Button
+ type="primary"
+ key="export"
+ hidden={!access.hasPerms('system:dictType:export')}
+ onClick={async () => {
+ handleExport();
+ }}
+ >
+ <PlusOutlined />
+ <FormattedMessage id="pages.searchTable.export" defaultMessage="导出" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getDictTypeList({ ...params } as API.System.DictTypeListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('system:dictType:del')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <UpdateForm
+ onSubmit={async (values) => {
+ let success = false;
+ if (values.dictId) {
+ success = await handleUpdate({ ...values } as API.System.DictType);
+ } else {
+ success = await handleAdd({ ...values } as API.System.DictType);
+ }
+ if (success) {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ }}
+ open={modalVisible}
+ values={currentRow || {}}
+ statusOptions={statusOptions}
+ />
+ </PageContainer>
+ );
+};
+
+export default DictTableList;
diff --git a/src/pages/System/DictData/edit.tsx b/src/pages/System/DictData/edit.tsx
new file mode 100644
index 0000000..188e01b
--- /dev/null
+++ b/src/pages/System/DictData/edit.tsx
@@ -0,0 +1,252 @@
+import React, { useEffect } from 'react';
+import {
+ ProForm,
+ ProFormDigit,
+ ProFormText,
+ ProFormSelect,
+ ProFormRadio,
+ ProFormTextArea,
+} from '@ant-design/pro-components';
+import { Form, Modal} from 'antd';
+import { useIntl, FormattedMessage } from '@umijs/max';
+import { DictValueEnumObj } from '@/components/DictTag';
+
+export type DataFormData = Record<string, unknown> & Partial<API.System.DictData>;
+
+export type DataFormProps = {
+ onCancel: (flag?: boolean, formVals?: DataFormData) => void;
+ onSubmit: (values: DataFormData) => Promise<void>;
+ open: boolean;
+ values: Partial<API.System.DictData>;
+ statusOptions: DictValueEnumObj;
+};
+
+const DictDataForm: React.FC<DataFormProps> = (props) => {
+ const [form] = Form.useForm();
+
+ const { statusOptions } = props;
+
+ useEffect(() => {
+ form.resetFields();
+ form.setFieldsValue({
+ dictCode: props.values.dictCode,
+ dictSort: props.values.dictSort,
+ dictLabel: props.values.dictLabel,
+ dictValue: props.values.dictValue,
+ dictType: props.values.dictType,
+ cssClass: props.values.cssClass,
+ listClass: props.values.listClass,
+ isDefault: props.values.isDefault,
+ status: props.values.status,
+ createBy: props.values.createBy,
+ createTime: props.values.createTime,
+ updateBy: props.values.updateBy,
+ updateTime: props.values.updateTime,
+ remark: props.values.remark,
+ });
+ }, [form, props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit(values as DataFormData);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'system.dict.data.title',
+ defaultMessage: '编辑字典数据',
+ })}
+ open={props.open}
+ forceRender
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ form={form}
+ grid={true}
+ submitter={false}
+ layout="horizontal"
+ onFinish={handleFinish}>
+ <ProFormDigit
+ name="dictCode"
+ label={intl.formatMessage({
+ id: 'system.dict.data.dict_code',
+ defaultMessage: '字典编码',
+ })}
+ colProps={{ md: 24, xl: 24 }}
+ placeholder="请输入字典编码"
+ disabled
+ hidden={true}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入字典编码!" defaultMessage="请输入字典编码!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="dictType"
+ label={intl.formatMessage({
+ id: 'system.dict.data.dict_type',
+ defaultMessage: '字典类型',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入字典类型"
+ disabled
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入字典类型!" defaultMessage="请输入字典类型!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="dictLabel"
+ label={intl.formatMessage({
+ id: 'system.dict.data.dict_label',
+ defaultMessage: '字典标签',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入字典标签"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入字典标签!" defaultMessage="请输入字典标签!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="dictValue"
+ label={intl.formatMessage({
+ id: 'system.dict.data.dict_value',
+ defaultMessage: '字典键值',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入字典键值"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入字典键值!" defaultMessage="请输入字典键值!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="cssClass"
+ label={intl.formatMessage({
+ id: 'system.dict.data.css_class',
+ defaultMessage: '样式属性',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入样式属性"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入样式属性!" defaultMessage="请输入样式属性!" />,
+ },
+ ]}
+ />
+ <ProFormSelect
+ name="listClass"
+ label={intl.formatMessage({
+ id: 'system.dict.data.list_class',
+ defaultMessage: '回显样式',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入回显样式"
+ valueEnum={{
+ 'default': '默认',
+ 'primary': '主要',
+ 'success': '成功',
+ 'info': '信息',
+ 'warning': '警告',
+ 'danger': '危险',
+ }}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入回显样式!" defaultMessage="请输入回显样式!" />,
+ },
+ ]}
+ />
+ <ProFormDigit
+ name="dictSort"
+ label={intl.formatMessage({
+ id: 'system.dict.data.dict_sort',
+ defaultMessage: '字典排序',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入字典排序"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入字典排序!" defaultMessage="请输入字典排序!" />,
+ },
+ ]}
+ />
+ <ProFormRadio.Group
+ name="isDefault"
+ label={intl.formatMessage({
+ id: 'system.dict.data.is_default',
+ defaultMessage: '是否默认',
+ })}
+ valueEnum={{
+ 'Y': '是',
+ 'N': '否',
+ }}
+ initialValue={'N'}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入是否默认"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入是否默认!" defaultMessage="请输入是否默认!" />,
+ },
+ ]}
+ />
+ <ProFormRadio.Group
+ valueEnum={statusOptions}
+ name="status"
+ label={intl.formatMessage({
+ id: 'system.dict.data.status',
+ defaultMessage: '状态',
+ })}
+ initialValue={'0'}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入状态"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入状态!" defaultMessage="请输入状态!" />,
+ },
+ ]}
+ />
+ <ProFormTextArea
+ name="remark"
+ label={intl.formatMessage({
+ id: 'system.dict.data.remark',
+ defaultMessage: '备注',
+ })}
+ colProps={{ md: 24, xl: 24 }}
+ placeholder="请输入备注"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入备注!" defaultMessage="请输入备注!" />,
+ },
+ ]}
+ />
+ </ProForm>
+ </Modal>
+ );
+};
+
+export default DictDataForm;
diff --git a/src/pages/System/DictData/index.tsx b/src/pages/System/DictData/index.tsx
new file mode 100644
index 0000000..2bf1069
--- /dev/null
+++ b/src/pages/System/DictData/index.tsx
@@ -0,0 +1,439 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess, history, useParams } from '@umijs/max';
+import type { FormInstance } from 'antd';
+import { Button, message, Modal } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
+import { getDictDataList, removeDictData, addDictData, updateDictData, exportDictData } from '@/services/system/dictdata';
+import UpdateForm from './edit';
+import { getDictValueEnum, getDictType, getDictTypeOptionSelect } from '@/services/system/dict';
+import DictTag from '@/components/DictTag';
+
+/**
+ * 添加节点
+ *
+ * @param fields
+ */
+const handleAdd = async (fields: API.System.DictData) => {
+ const hide = message.loading('正在添加');
+ try {
+ const resp = await addDictData({ ...fields });
+ hide();
+ if (resp.code === 200) {
+ message.success('添加成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('添加失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 更新节点
+ *
+ * @param fields
+ */
+const handleUpdate = async (fields: API.System.DictData) => {
+ const hide = message.loading('正在更新');
+ try {
+ const resp = await updateDictData(fields);
+ hide();
+ if (resp.code === 200) {
+ message.success('更新成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.System.DictData[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ const resp = await removeDictData(selectedRows.map((row) => row.dictCode).join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleRemoveOne = async (selectedRow: API.System.DictData) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRow) return true;
+ try {
+ const params = [selectedRow.dictCode];
+ const resp = await removeDictData(params.join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+/**
+ * 导出数据
+ *
+ *
+ */
+const handleExport = async () => {
+ const hide = message.loading('正在导出');
+ try {
+ await exportDictData();
+ hide();
+ message.success('导出成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('导出失败,请重试');
+ return false;
+ }
+};
+
+export type DictTypeArgs = {
+ id: string;
+};
+
+
+const DictDataTableList: React.FC = () => {
+
+ const formTableRef = useRef<FormInstance>();
+
+ const [dictId, setDictId] = useState<string>('');
+ const [dictType, setDictType] = useState<string>('');
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<API.System.DictData>();
+ const [selectedRows, setSelectedRows] = useState<API.System.DictData[]>([]);
+
+ const [dictTypeOptions, setDictTypeOptions] = useState<any>([]);
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ const params = useParams();
+ if (params.id === undefined) {
+ history.push('/system/dict');
+ }
+ const id = params.id || '0';
+
+ useEffect(() => {
+ if (dictId !== id) {
+ setDictId(id);
+ getDictTypeOptionSelect().then((res) => {
+ if (res.code === 200) {
+ const opts: any = {};
+ res.data.forEach((item: any) => {
+ opts[item.dictType] = item.dictName;
+ });
+ setDictTypeOptions(opts);
+ }
+ });
+ getDictValueEnum('sys_normal_disable').then((data) => {
+ setStatusOptions(data);
+ });
+ getDictType(id).then((res) => {
+ if (res.code === 200) {
+ setDictType(res.data.dictType);
+ formTableRef.current?.setFieldsValue({
+ dictType: res.data.dictType,
+ });
+ actionRef.current?.reloadAndRest?.();
+ } else {
+ message.error(res.msg);
+ }
+ });
+ }
+ }, [dictId, dictType, params]);
+
+ const columns: ProColumns<API.System.DictData>[] = [
+ {
+ title: <FormattedMessage id="system.dict.data.dict_code" defaultMessage="字典编码" />,
+ dataIndex: 'dictCode',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.dict.data.dict_label" defaultMessage="字典标签" />,
+ dataIndex: 'dictLabel',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.dict.data.dict_type" defaultMessage="字典类型" />,
+ dataIndex: 'dictType',
+ valueType: 'select',
+ hideInTable: true,
+ valueEnum: dictTypeOptions,
+ search: {
+ transform: (value) => {
+ setDictType(value);
+ return value;
+ },
+ },
+ },
+ {
+ title: <FormattedMessage id="system.dict.data.dict_value" defaultMessage="字典键值" />,
+ dataIndex: 'dictValue',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.dict.data.dict_sort" defaultMessage="字典排序" />,
+ dataIndex: 'dictSort',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.dict.data.status" defaultMessage="状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (<DictTag enums={statusOptions} value={record.status} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="system.dict.data.remark" defaultMessage="备注" />,
+ dataIndex: 'remark',
+ valueType: 'textarea',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.dict.data.create_time" defaultMessage="创建时间" />,
+ dataIndex: 'createTime',
+ valueType: 'dateRange',
+ render: (_, record) => {
+ return (<span>{record.createTime.toString()} </span>);
+ },
+ search: {
+ transform: (value) => {
+ return {
+ 'params[beginTime]': value[0],
+ 'params[endTime]': value[1],
+ };
+ },
+ },
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '120px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ hidden={!access.hasPerms('system:data:edit')}
+ onClick={() => {
+ setModalVisible(true);
+ setCurrentRow(record);
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ danger
+ key="batchRemove"
+ hidden={!access.hasPerms('system:data:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemoveOne(record);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.System.DictData>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="dictCode"
+ key="dataList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('system:data:add')}
+ onClick={async () => {
+ setCurrentRow({ dictType: dictType, isDefault: 'N', status: '0' } as API.System.DictData);
+ setModalVisible(true);
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('system:data:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ <Button
+ type="primary"
+ key="export"
+ hidden={!access.hasPerms('system:data:export')}
+ onClick={async () => {
+ handleExport();
+ }}
+ >
+ <PlusOutlined />
+ <FormattedMessage id="pages.searchTable.export" defaultMessage="导出" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getDictDataList({ ...params, dictType } as API.System.DictDataListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('system:data:del')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <UpdateForm
+ onSubmit={async (values) => {
+ let success = false;
+ if (values.dictCode) {
+ success = await handleUpdate({ ...values } as API.System.DictData);
+ } else {
+ success = await handleAdd({ ...values } as API.System.DictData);
+ }
+ if (success) {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ }}
+ open={modalVisible}
+ values={currentRow || {}}
+ statusOptions={statusOptions}
+ />
+ </PageContainer>
+ );
+};
+
+export default DictDataTableList;
diff --git a/src/pages/System/Logininfor/edit.tsx b/src/pages/System/Logininfor/edit.tsx
new file mode 100644
index 0000000..5243191
--- /dev/null
+++ b/src/pages/System/Logininfor/edit.tsx
@@ -0,0 +1,216 @@
+import React, { useEffect } from 'react';
+import {
+ ProForm,
+ ProFormDigit,
+ ProFormText,
+ ProFormRadio,
+ ProFormTimePicker,
+ } from '@ant-design/pro-components';
+import { Form, Modal} from 'antd';
+import { useIntl, FormattedMessage } from '@umijs/max';
+import { DictValueEnumObj } from '@/components/DictTag';
+
+export type LogininforFormData = Record<string, unknown> & Partial<API.Monitor.Logininfor>;
+
+export type LogininforFormProps = {
+ onCancel: (flag?: boolean, formVals?: LogininforFormData) => void;
+ onSubmit: (values: LogininforFormData) => Promise<void>;
+ open: boolean;
+ values: Partial<API.Monitor.Logininfor>;
+ statusOptions: DictValueEnumObj;
+};
+
+const LogininforForm: React.FC<LogininforFormProps> = (props) => {
+ const [form] = Form.useForm();
+
+ const { statusOptions, } = props;
+
+ useEffect(() => {
+ form.resetFields();
+ form.setFieldsValue({
+ infoId: props.values.infoId,
+ userName: props.values.userName,
+ ipaddr: props.values.ipaddr,
+ loginLocation: props.values.loginLocation,
+ browser: props.values.browser,
+ os: props.values.os,
+ status: props.values.status,
+ msg: props.values.msg,
+ loginTime: props.values.loginTime,
+ });
+ }, [form, props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ form.resetFields();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit(values as LogininforFormData);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'system.logininfor.title',
+ defaultMessage: '编辑系统访问记录',
+ })}
+ open={props.open}
+ destroyOnClose
+ forceRender
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ form={form}
+ grid={true}
+ layout="horizontal"
+ onFinish={handleFinish}>
+ <ProFormDigit
+ name="infoId"
+ label={intl.formatMessage({
+ id: 'system.logininfor.info_id',
+ defaultMessage: '访问编号',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入访问编号"
+ disabled
+ hidden={true}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入访问编号!" defaultMessage="请输入访问编号!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="userName"
+ label={intl.formatMessage({
+ id: 'system.logininfor.user_name',
+ defaultMessage: '用户账号',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入用户账号"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入用户账号!" defaultMessage="请输入用户账号!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="ipaddr"
+ label={intl.formatMessage({
+ id: 'system.logininfor.ipaddr',
+ defaultMessage: '登录IP地址',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入登录IP地址"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入登录IP地址!" defaultMessage="请输入登录IP地址!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="loginLocation"
+ label={intl.formatMessage({
+ id: 'system.logininfor.login_location',
+ defaultMessage: '登录地点',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入登录地点"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入登录地点!" defaultMessage="请输入登录地点!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="browser"
+ label={intl.formatMessage({
+ id: 'system.logininfor.browser',
+ defaultMessage: '浏览器类型',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入浏览器类型"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入浏览器类型!" defaultMessage="请输入浏览器类型!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="os"
+ label={intl.formatMessage({
+ id: 'system.logininfor.os',
+ defaultMessage: '操作系统',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入操作系统"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入操作系统!" defaultMessage="请输入操作系统!" />,
+ },
+ ]}
+ />
+ <ProFormRadio.Group
+ valueEnum={statusOptions}
+ name="status"
+ label={intl.formatMessage({
+ id: 'system.logininfor.status',
+ defaultMessage: '登录状态',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入登录状态"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入登录状态!" defaultMessage="请输入登录状态!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="msg"
+ label={intl.formatMessage({
+ id: 'system.logininfor.msg',
+ defaultMessage: '提示消息',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入提示消息"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入提示消息!" defaultMessage="请输入提示消息!" />,
+ },
+ ]}
+ />
+ <ProFormTimePicker
+ name="loginTime"
+ label={intl.formatMessage({
+ id: 'system.logininfor.login_time',
+ defaultMessage: '访问时间',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入访问时间"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入访问时间!" defaultMessage="请输入访问时间!" />,
+ },
+ ]}
+ />
+ </ProForm>
+ </Modal>
+ );
+};
+
+export default LogininforForm;
diff --git a/src/pages/System/Logininfor/index.tsx b/src/pages/System/Logininfor/index.tsx
new file mode 100644
index 0000000..ee5c1e5
--- /dev/null
+++ b/src/pages/System/Logininfor/index.tsx
@@ -0,0 +1,321 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess } from '@umijs/max';
+import type { FormInstance } from 'antd';
+import { Button, message, Modal } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined, UnlockOutlined } from '@ant-design/icons';
+import { getLogininforList, removeLogininfor, exportLogininfor, unlockLogininfor, cleanLogininfor } from '@/services/monitor/logininfor';
+import DictTag from '@/components/DictTag';
+import { getDictValueEnum } from '@/services/system/dict';
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.Monitor.Logininfor[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ const resp = await removeLogininfor(selectedRows.map((row) => row.infoId).join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleClean = async () => {
+ const hide = message.loading('请稍候');
+ try {
+ const resp = await cleanLogininfor();
+ hide();
+ if (resp.code === 200) {
+ message.success('清空成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('请求失败,请重试');
+ return false;
+ }
+};
+
+const handleUnlock = async (userName: string) => {
+ const hide = message.loading('正在解锁');
+ try {
+ const resp = await unlockLogininfor(userName);
+ hide();
+ if (resp.code === 200) {
+ message.success('解锁成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('解锁失败,请重试');
+ return false;
+ }
+};
+
+/**
+ * 导出数据
+ *
+ * @param id
+ */
+const handleExport = async () => {
+ const hide = message.loading('正在导出');
+ try {
+ await exportLogininfor();
+ hide();
+ message.success('导出成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('导出失败,请重试');
+ return false;
+ }
+};
+
+
+const LogininforTableList: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+
+ const actionRef = useRef<ActionType>();
+ const [selectedRows, setSelectedRows] = useState<API.Monitor.Logininfor[]>([]);
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+ getDictValueEnum('sys_common_status', true).then((data) => {
+ setStatusOptions(data);
+ });
+ }, []);
+
+ const columns: ProColumns<API.Monitor.Logininfor>[] = [
+ {
+ title: <FormattedMessage id="monitor.logininfor.info_id" defaultMessage="访问编号" />,
+ dataIndex: 'infoId',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.user_name" defaultMessage="用户账号" />,
+ dataIndex: 'userName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.ipaddr" defaultMessage="登录IP地址" />,
+ dataIndex: 'ipaddr',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.login_location" defaultMessage="登录地点" />,
+ dataIndex: 'loginLocation',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.browser" defaultMessage="浏览器类型" />,
+ dataIndex: 'browser',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.os" defaultMessage="操作系统" />,
+ dataIndex: 'os',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.status" defaultMessage="登录状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (<DictTag enums={statusOptions} value={record.status} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.msg" defaultMessage="提示消息" />,
+ dataIndex: 'msg',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.logininfor.login_time" defaultMessage="访问时间" />,
+ dataIndex: 'loginTime',
+ valueType: 'dateTime',
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.Monitor.Logininfor>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="infoId"
+ key="logininforList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('monitor:logininfor:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ <Button
+ type="primary"
+ key="clean"
+ danger
+ hidden={!access.hasPerms('monitor:logininfor:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认清空所有数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleClean();
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.cleanAll" defaultMessage="清空" />
+ </Button>,
+ <Button
+ type="primary"
+ key="unlock"
+ hidden={selectedRows?.length === 0 || !access.hasPerms('monitor:logininfor:unlock')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认解锁该用户的数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleUnlock(selectedRows[0].userName);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <UnlockOutlined />
+ <FormattedMessage id="monitor.logininfor.unlock" defaultMessage="解锁" />
+ </Button>,
+ <Button
+ type="primary"
+ key="export"
+ hidden={!access.hasPerms('monitor:logininfor:export')}
+ onClick={async () => {
+ handleExport();
+ }}
+ >
+ <PlusOutlined />
+ <FormattedMessage id="pages.searchTable.export" defaultMessage="导出" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getLogininforList({ ...params } as API.Monitor.LogininforListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('monitor:logininfor:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ </PageContainer>
+ );
+};
+
+export default LogininforTableList;
diff --git a/src/pages/System/Menu/edit.tsx b/src/pages/System/Menu/edit.tsx
new file mode 100644
index 0000000..e094255
--- /dev/null
+++ b/src/pages/System/Menu/edit.tsx
@@ -0,0 +1,386 @@
+import React, { useEffect, useState } from 'react';
+import {
+ ProForm,
+ ProFormDigit,
+ ProFormText,
+ ProFormRadio,
+ ProFormTreeSelect,
+ ProFormSelect,
+} from '@ant-design/pro-components';
+import { Form, Modal} from 'antd';
+import { useIntl, FormattedMessage } from '@umijs/max';
+import { DataNode } from 'antd/es/tree';
+import { createIcon } from '@/utils/IconUtil';
+import { DictValueEnumObj } from '@/components/DictTag';
+import IconSelector from '@/components/IconSelector';
+
+export type MenuFormData = Record<string, unknown> & Partial<API.System.Menu>;
+
+export type MenuFormProps = {
+ onCancel: (flag?: boolean, formVals?: MenuFormData) => void;
+ onSubmit: (values: MenuFormData) => Promise<void>;
+ open: boolean;
+ values: Partial<API.System.Menu>;
+ visibleOptions: DictValueEnumObj;
+ statusOptions: DictValueEnumObj;
+ menuTree: DataNode[];
+};
+
+const MenuForm: React.FC<MenuFormProps> = (props) => {
+
+ const [form] = Form.useForm();
+
+ const [menuTypeId, setMenuTypeId] = useState<any>('M');
+ const [menuIconName, setMenuIconName] = useState<any>();
+ const [iconSelectorOpen, setIconSelectorOpen] = useState<boolean>(false);
+
+ const { menuTree, visibleOptions, statusOptions } = props;
+
+ useEffect(() => {
+ form.resetFields();
+ setMenuIconName(props.values.icon);
+ form.setFieldsValue({
+ menuId: props.values.menuId,
+ menuName: props.values.menuName,
+ parentId: props.values.parentId,
+ orderNum: props.values.orderNum,
+ path: props.values.path,
+ component: props.values.component,
+ query: props.values.query,
+ isFrame: props.values.isFrame,
+ isCache: props.values.isCache,
+ menuType: props.values.menuType,
+ visible: props.values.visible,
+ status: props.values.status,
+ perms: props.values.perms,
+ icon: props.values.icon,
+ createBy: props.values.createBy,
+ createTime: props.values.createTime,
+ updateBy: props.values.updateBy,
+ updateTime: props.values.updateTime,
+ remark: props.values.remark,
+ });
+ }, [form, props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit(values as MenuFormData);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'system.menu.title',
+ defaultMessage: '编辑菜单权限',
+ })}
+ open={props.open}
+ forceRender
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ form={form}
+ grid={true}
+ submitter={false}
+ layout="horizontal"
+ onFinish={handleFinish}>
+ <ProFormDigit
+ name="menuId"
+ label={intl.formatMessage({
+ id: 'system.menu.menu_id',
+ defaultMessage: '菜单编号',
+ })}
+ placeholder="请输入菜单编号"
+ disabled
+ hidden={true}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入菜单编号!" defaultMessage="请输入菜单编号!" />,
+ },
+ ]}
+ />
+ <ProFormTreeSelect
+ name="parentId"
+ label={intl.formatMessage({
+ id: 'system.menu.parent_id',
+ defaultMessage: '上级菜单',
+ })}
+ params={{menuTree}}
+ request={async () => {
+ return menuTree;
+ }}
+ placeholder="请输入父菜单编号"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入父菜单编号!" defaultMessage="请输入父菜单编号!" />,
+ },
+ ]}
+ fieldProps = {{
+ defaultValue: 0
+ }}
+ />
+ <ProFormRadio.Group
+ name="menuType"
+ valueEnum={{
+ M: '目录',
+ C: '菜单',
+ F: '按钮',
+ }}
+ label={intl.formatMessage({
+ id: 'system.menu.menu_type',
+ defaultMessage: '菜单类型',
+ })}
+ placeholder="请输入菜单类型"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入菜单类型!" defaultMessage="请输入菜单类型!" />,
+ },
+ ]}
+ fieldProps={{
+ defaultValue: 'M',
+ onChange: (e) => {
+ setMenuTypeId(e.target.value);
+ },
+ }}
+ />
+ <ProFormSelect
+ name="icon"
+ label={intl.formatMessage({
+ id: 'system.menu.icon',
+ defaultMessage: '菜单图标',
+ })}
+ valueEnum={{}}
+ hidden={menuTypeId === 'F'}
+ addonBefore={createIcon(menuIconName)}
+ fieldProps={{
+ onClick: () => {
+ setIconSelectorOpen(true);
+ },
+ }}
+ placeholder="请输入菜单图标"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入菜单图标!" defaultMessage="请输入菜单图标!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="menuName"
+ label={intl.formatMessage({
+ id: 'system.menu.menu_name',
+ defaultMessage: '菜单名称',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入菜单名称"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入菜单名称!" defaultMessage="请输入菜单名称!" />,
+ },
+ ]}
+ />
+ <ProFormDigit
+ name="orderNum"
+ label={intl.formatMessage({
+ id: 'system.menu.order_num',
+ defaultMessage: '显示顺序',
+ })}
+ width="lg"
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入显示顺序"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入显示顺序!" defaultMessage="请输入显示顺序!" />,
+ },
+ ]}
+ fieldProps = {{
+ defaultValue: 1
+ }}
+ />
+ <ProFormRadio.Group
+ name="isFrame"
+ valueEnum={{
+ 0: '是',
+ 1: '否',
+ }}
+ initialValue="1"
+ label={intl.formatMessage({
+ id: 'system.menu.is_frame',
+ defaultMessage: '是否为外链',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入是否为外链"
+ hidden={menuTypeId === 'F'}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入是否为外链!" defaultMessage="请输入是否为外链!" />,
+ },
+ ]}
+ fieldProps = {{
+ defaultValue: '1'
+ }}
+ />
+ <ProFormText
+ name="path"
+ label={intl.formatMessage({
+ id: 'system.menu.path',
+ defaultMessage: '路由地址',
+ })}
+ width="lg"
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入路由地址"
+ hidden={menuTypeId === 'F'}
+ rules={[
+ {
+ required: menuTypeId !== 'F',
+ message: <FormattedMessage id="请输入路由地址!" defaultMessage="请输入路由地址!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="component"
+ label={intl.formatMessage({
+ id: 'system.menu.component',
+ defaultMessage: '组件路径',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入组件路径"
+ hidden={menuTypeId !== 'C'}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入组件路径!" defaultMessage="请输入组件路径!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="query"
+ label={intl.formatMessage({
+ id: 'system.menu.query',
+ defaultMessage: '路由参数',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入路由参数"
+ hidden={menuTypeId !== 'C'}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入路由参数!" defaultMessage="请输入路由参数!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="perms"
+ label={intl.formatMessage({
+ id: 'system.menu.perms',
+ defaultMessage: '权限标识',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入权限标识"
+ hidden={menuTypeId === 'M'}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入权限标识!" defaultMessage="请输入权限标识!" />,
+ },
+ ]}
+ />
+ <ProFormRadio.Group
+ name="isCache"
+ valueEnum={{
+ 0: '缓存',
+ 1: '不缓存',
+ }}
+ label={intl.formatMessage({
+ id: 'system.menu.is_cache',
+ defaultMessage: '是否缓存',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入是否缓存"
+ hidden={menuTypeId !== 'C'}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入是否缓存!" defaultMessage="请输入是否缓存!" />,
+ },
+ ]}
+ fieldProps = {{
+ defaultValue: 0
+ }}
+ />
+ <ProFormRadio.Group
+ name="visible"
+ valueEnum={visibleOptions}
+ label={intl.formatMessage({
+ id: 'system.menu.visible',
+ defaultMessage: '显示状态',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入显示状态"
+ hidden={menuTypeId === 'F'}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入显示状态!" defaultMessage="请输入显示状态!" />,
+ },
+ ]}
+ fieldProps = {{
+ defaultValue: '0'
+ }}
+ />
+ <ProFormRadio.Group
+ valueEnum={statusOptions}
+ name="status"
+ label={intl.formatMessage({
+ id: 'system.menu.status',
+ defaultMessage: '菜单状态',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入菜单状态"
+ hidden={menuTypeId === 'F'}
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入菜单状态!" defaultMessage="请输入菜单状态!" />,
+ },
+ ]}
+ fieldProps = {{
+ defaultValue: '0'
+ }}
+ />
+ </ProForm>
+ <Modal
+ width={800}
+ open={iconSelectorOpen}
+ onCancel={() => {
+ setIconSelectorOpen(false);
+ }}
+ footer={null}
+ >
+ <IconSelector
+ onSelect={(name: string) => {
+ form.setFieldsValue({ icon: name });
+ setMenuIconName(name);
+ setIconSelectorOpen(false);
+ }}
+ />
+ </Modal>
+ </Modal>
+ );
+};
+
+export default MenuForm;
diff --git a/src/pages/System/Menu/index.tsx b/src/pages/System/Menu/index.tsx
new file mode 100644
index 0000000..95ca2de
--- /dev/null
+++ b/src/pages/System/Menu/index.tsx
@@ -0,0 +1,339 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess } from '@umijs/max';
+import { Button, message, Modal } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
+import { getMenuList, removeMenu, addMenu, updateMenu } from '@/services/system/menu';
+import UpdateForm from './edit';
+import { getDictValueEnum } from '@/services/system/dict';
+import { buildTreeData } from '@/utils/tree';
+import { DataNode } from 'antd/es/tree';
+import DictTag from '@/components/DictTag';
+
+/**
+ * 添加节点
+ *
+ * @param fields
+ */
+const handleAdd = async (fields: API.System.Menu) => {
+ const hide = message.loading('正在添加');
+ try {
+ await addMenu({ ...fields });
+ hide();
+ message.success('添加成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('添加失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 更新节点
+ *
+ * @param fields
+ */
+const handleUpdate = async (fields: API.System.Menu) => {
+ const hide = message.loading('正在配置');
+ try {
+ await updateMenu(fields);
+ hide();
+ message.success('配置成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.System.Menu[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ await removeMenu(selectedRows.map((row) => row.menuId).join(','));
+ hide();
+ message.success('删除成功,即将刷新');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleRemoveOne = async (selectedRow: API.System.Menu) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRow) return true;
+ try {
+ const params = [selectedRow.menuId];
+ await removeMenu(params.join(','));
+ hide();
+ message.success('删除成功,即将刷新');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+
+const MenuTableList: React.FC = () => {
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<API.System.Menu>();
+ const [selectedRows, setSelectedRows] = useState<API.System.Menu[]>([]);
+
+ const [menuTree, setMenuTree] = useState<DataNode[]>([]);
+ const [visibleOptions, setVisibleOptions] = useState<any>([]);
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+ getDictValueEnum('sys_show_hide').then((data) => {
+ setVisibleOptions(data);
+ });
+ getDictValueEnum('sys_normal_disable').then((data) => {
+ setStatusOptions(data);
+ });
+ }, []);
+
+ const columns: ProColumns<API.System.Menu>[] = [
+ {
+ title: <FormattedMessage id="system.menu.menu_name" defaultMessage="菜单名称" />,
+ dataIndex: 'menuName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.menu.icon" defaultMessage="菜单图标" />,
+ dataIndex: 'icon',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.menu.order_num" defaultMessage="显示顺序" />,
+ dataIndex: 'orderNum',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.menu.component" defaultMessage="组件路径" />,
+ dataIndex: 'component',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.menu.perms" defaultMessage="权限标识" />,
+ dataIndex: 'perms',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.menu.status" defaultMessage="菜单状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (<DictTag enums={statusOptions} value={record.status} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '220px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ hidden={!access.hasPerms('system:menu:edit')}
+ onClick={() => {
+ setModalVisible(true);
+ setCurrentRow(record);
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ danger
+ key="batchRemove"
+ hidden={!access.hasPerms('system:menu:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemoveOne(record);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.System.Menu>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ rowKey="menuId"
+ key="menuList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('system:menu:add')}
+ onClick={async () => {
+ setCurrentRow(undefined);
+ setModalVisible(true);
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('system:menu:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() {},
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getMenuList({ ...params } as API.System.MenuListParams).then((res) => {
+ const rootMenu = { id: 0, label: '主类目', children: [] as DataNode[], value: 0 };
+ const memuData = buildTreeData(res.data, 'menuId', 'menuName', '', '', '');
+ rootMenu.children = memuData;
+ const treeData: any = [];
+ treeData.push(rootMenu);
+ setMenuTree(treeData);
+ return {
+ data: memuData,
+ total: res.data.length,
+ success: true,
+ };
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('system:menu:del')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <UpdateForm
+ onSubmit={async (values) => {
+ let success = false;
+ if (values.menuId) {
+ success = await handleUpdate({ ...values } as API.System.Menu);
+ } else {
+ success = await handleAdd({ ...values } as API.System.Menu);
+ }
+ if (success) {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ }}
+ open={modalVisible}
+ values={currentRow || {}}
+ visibleOptions={visibleOptions}
+ statusOptions={statusOptions}
+ menuTree={menuTree}
+ />
+ </PageContainer>
+ );
+};
+
+export default MenuTableList;
diff --git a/src/pages/System/Notice/edit.tsx b/src/pages/System/Notice/edit.tsx
new file mode 100644
index 0000000..0b111ce
--- /dev/null
+++ b/src/pages/System/Notice/edit.tsx
@@ -0,0 +1,174 @@
+import React, { useEffect } from 'react';
+import {
+ ProForm,
+ ProFormDigit,
+ ProFormText,
+ ProFormSelect,
+ ProFormTextArea,
+ ProFormRadio,
+ } from '@ant-design/pro-components';
+import { Form, Modal} from 'antd';
+import { useIntl, FormattedMessage } from '@umijs/max';
+import { DictValueEnumObj } from '@/components/DictTag';
+
+export type NoticeFormData = Record<string, unknown> & Partial<API.System.Notice>;
+
+export type NoticeFormProps = {
+ onCancel: (flag?: boolean, formVals?: NoticeFormData) => void;
+ onSubmit: (values: NoticeFormData) => Promise<void>;
+ open: boolean;
+ values: Partial<API.System.Notice>;
+ noticeTypeOptions: DictValueEnumObj;
+ statusOptions: DictValueEnumObj;
+};
+
+const NoticeForm: React.FC<NoticeFormProps> = (props) => {
+ const [form] = Form.useForm();
+
+ const { noticeTypeOptions,statusOptions, } = props;
+
+ useEffect(() => {
+ form.resetFields();
+ form.setFieldsValue({
+ noticeId: props.values.noticeId,
+ noticeTitle: props.values.noticeTitle,
+ noticeType: props.values.noticeType,
+ noticeContent: props.values.noticeContent,
+ status: props.values.status,
+ createBy: props.values.createBy,
+ createTime: props.values.createTime,
+ updateBy: props.values.updateBy,
+ updateTime: props.values.updateTime,
+ remark: props.values.remark,
+ });
+ }, [form, props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit(values as NoticeFormData);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'system.notice.title',
+ defaultMessage: '编辑通知公告',
+ })}
+ forceRender
+ open={props.open}
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ form={form}
+ grid={true}
+ submitter={false}
+ layout="horizontal"
+ onFinish={handleFinish}>
+ <ProFormDigit
+ name="noticeId"
+ label={intl.formatMessage({
+ id: 'system.notice.notice_id',
+ defaultMessage: '公告编号',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入公告编号"
+ disabled
+ hidden={true}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入公告编号!" defaultMessage="请输入公告编号!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="noticeTitle"
+ label={intl.formatMessage({
+ id: 'system.notice.notice_title',
+ defaultMessage: '公告标题',
+ })}
+ placeholder="请输入公告标题"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入公告标题!" defaultMessage="请输入公告标题!" />,
+ },
+ ]}
+ />
+ <ProFormSelect
+ valueEnum={noticeTypeOptions}
+ name="noticeType"
+ label={intl.formatMessage({
+ id: 'system.notice.notice_type',
+ defaultMessage: '公告类型',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入公告类型"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入公告类型!" defaultMessage="请输入公告类型!" />,
+ },
+ ]}
+ />
+ <ProFormRadio.Group
+ valueEnum={statusOptions}
+ name="status"
+ label={intl.formatMessage({
+ id: 'system.notice.status',
+ defaultMessage: '公告状态',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入公告状态"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入公告状态!" defaultMessage="请输入公告状态!" />,
+ },
+ ]}
+ />
+ <ProFormTextArea
+ name="noticeContent"
+ label={intl.formatMessage({
+ id: 'system.notice.notice_content',
+ defaultMessage: '公告内容',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入公告内容"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入公告内容!" defaultMessage="请输入公告内容!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="remark"
+ label={intl.formatMessage({
+ id: 'system.notice.remark',
+ defaultMessage: '备注',
+ })}
+ colProps={{ md: 12, xl: 24 }}
+ placeholder="请输入备注"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入备注!" defaultMessage="请输入备注!" />,
+ },
+ ]}
+ />
+ </ProForm>
+ </Modal>
+ );
+};
+
+export default NoticeForm;
diff --git a/src/pages/System/Notice/index.tsx b/src/pages/System/Notice/index.tsx
new file mode 100644
index 0000000..ea5b063
--- /dev/null
+++ b/src/pages/System/Notice/index.tsx
@@ -0,0 +1,366 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess } from '@umijs/max';
+import type { FormInstance } from 'antd';
+import { Button, message, Modal } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
+import { getNoticeList, removeNotice, addNotice, updateNotice } from '@/services/system/notice';
+import UpdateForm from './edit';
+import { getDictValueEnum } from '@/services/system/dict';
+import DictTag from '@/components/DictTag';
+
+/**
+ * 添加节点
+ *
+ * @param fields
+ */
+const handleAdd = async (fields: API.System.Notice) => {
+ const hide = message.loading('正在添加');
+ try {
+ const resp = await addNotice({ ...fields });
+ hide();
+ if (resp.code === 200) {
+ message.success('添加成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('添加失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 更新节点
+ *
+ * @param fields
+ */
+const handleUpdate = async (fields: API.System.Notice) => {
+ const hide = message.loading('正在更新');
+ try {
+ const resp = await updateNotice(fields);
+ hide();
+ if (resp.code === 200) {
+ message.success('更新成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.System.Notice[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ const resp = await removeNotice(selectedRows.map((row) => row.noticeId).join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleRemoveOne = async (selectedRow: API.System.Notice) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRow) return true;
+ try {
+ const params = [selectedRow.noticeId];
+ const resp = await removeNotice(params.join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+
+
+const NoticeTableList: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<API.System.Notice>();
+ const [selectedRows, setSelectedRows] = useState<API.System.Notice[]>([]);
+
+ const [noticeTypeOptions, setNoticeTypeOptions] = useState<any>([]);
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+ getDictValueEnum('sys_notice_type').then((data) => {
+ setNoticeTypeOptions(data);
+ });
+ getDictValueEnum('sys_notice_status').then((data) => {
+ setStatusOptions(data);
+ });
+ }, []);
+
+ const columns: ProColumns<API.System.Notice>[] = [
+ {
+ title: <FormattedMessage id="system.notice.notice_id" defaultMessage="公告编号" />,
+ dataIndex: 'noticeId',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.notice.notice_title" defaultMessage="公告标题" />,
+ dataIndex: 'noticeTitle',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.notice.notice_type" defaultMessage="公告类型" />,
+ dataIndex: 'noticeType',
+ valueType: 'select',
+ valueEnum: noticeTypeOptions,
+ },
+ {
+ title: <FormattedMessage id="system.notice.notice_content" defaultMessage="公告内容" />,
+ dataIndex: 'noticeContent',
+ valueType: 'text',
+ hideInTable: true,
+ },
+ {
+ title: <FormattedMessage id="system.notice.status" defaultMessage="公告状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (<DictTag enums={statusOptions} value={record.status} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="system.notice.remark" defaultMessage="备注" />,
+ dataIndex: 'remark',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.notice.create_time" defaultMessage="创建时间" />,
+ dataIndex: 'createTime',
+ valueType: 'dateRange',
+ render: (_, record) => {
+ return (<span>{record.createTime.toString()} </span>);
+ },
+ search: {
+ transform: (value) => {
+ return {
+ 'params[beginTime]': value[0],
+ 'params[endTime]': value[1],
+ };
+ },
+ },
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '120px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ hidden={!access.hasPerms('system:notice:edit')}
+ onClick={() => {
+ setModalVisible(true);
+ setCurrentRow(record);
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ danger
+ key="batchRemove"
+ hidden={!access.hasPerms('system:notice:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemoveOne(record);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.System.Notice>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="noticeId"
+ key="noticeList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('system:notice:add')}
+ onClick={async () => {
+ setCurrentRow(undefined);
+ setModalVisible(true);
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('system:notice:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() {},
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getNoticeList({ ...params } as API.System.NoticeListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('system:notice:del')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <UpdateForm
+ onSubmit={async (values) => {
+ let success = false;
+ if (values.noticeId) {
+ success = await handleUpdate({ ...values } as API.System.Notice);
+ } else {
+ success = await handleAdd({ ...values } as API.System.Notice);
+ }
+ if (success) {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ }}
+ open={modalVisible}
+ values={currentRow || {}}
+ noticeTypeOptions={noticeTypeOptions}
+ statusOptions={statusOptions}
+ />
+ </PageContainer>
+ );
+};
+
+export default NoticeTableList;
diff --git a/src/pages/System/Operlog/detail.tsx b/src/pages/System/Operlog/detail.tsx
new file mode 100644
index 0000000..f5b4f65
--- /dev/null
+++ b/src/pages/System/Operlog/detail.tsx
@@ -0,0 +1,115 @@
+import React from 'react';
+import { Descriptions, Modal } from 'antd';
+import { useIntl, FormattedMessage } from '@umijs/max';
+import { DictValueEnumObj } from '@/components/DictTag';
+import { getValueEnumLabel } from '@/utils/options';
+
+export type OperlogFormData = Record<string, unknown> & Partial<API.Monitor.Operlog>;
+
+export type OperlogFormProps = {
+ onCancel: (flag?: boolean, formVals?: OperlogFormData) => void;
+ onSubmit: (values: OperlogFormData) => Promise<void>;
+ open: boolean;
+ values: Partial<API.Monitor.Operlog>;
+ businessTypeOptions: DictValueEnumObj;
+ operatorTypeOptions: DictValueEnumObj;
+ statusOptions: DictValueEnumObj;
+};
+
+const OperlogDetailForm: React.FC<OperlogFormProps> = (props) => {
+
+ const { values, businessTypeOptions, operatorTypeOptions, statusOptions, } = props;
+
+ const intl = useIntl();
+ const handleOk = () => {
+ console.log("handle ok");
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'monitor.operlog.title',
+ defaultMessage: '编辑操作日志记录',
+ })}
+ open={props.open}
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <Descriptions column={24}>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.operlog.module" defaultMessage="操作模块" />}
+ >
+ {`${values.title}/${getValueEnumLabel(businessTypeOptions, values.businessType)}`}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.operlog.request_method" defaultMessage="请求方式" />}
+ >
+ {values.requestMethod}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.operlog.oper_name" defaultMessage="操作人员" />}
+ >
+ {`${values.operName}/${values.operIp}`}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.operlog.operator_type" defaultMessage="操作类别" />}
+ >
+ {getValueEnumLabel(operatorTypeOptions, values.operatorType)}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={<FormattedMessage id="monitor.operlog.method" defaultMessage="方法名称" />}
+ >
+ {values.method}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={<FormattedMessage id="monitor.operlog.oper_url" defaultMessage="请求URL" />}
+ >
+ {values.operUrl}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={<FormattedMessage id="monitor.operlog.oper_param" defaultMessage="请求参数" />}
+ >
+ {values.operParam}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={<FormattedMessage id="monitor.operlog.json_result" defaultMessage="返回参数" />}
+ >
+ {values.jsonResult}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={24}
+ label={<FormattedMessage id="monitor.operlog.error_msg" defaultMessage="错误消息" />}
+ >
+ {values.errorMsg}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.operlog.status" defaultMessage="操作状态" />}
+ >
+ {getValueEnumLabel(statusOptions, values.status)}
+ </Descriptions.Item>
+ <Descriptions.Item
+ span={12}
+ label={<FormattedMessage id="monitor.operlog.oper_time" defaultMessage="操作时间" />}
+ >
+ {values.operTime?.toString()}
+ </Descriptions.Item>
+ </Descriptions>
+ </Modal>
+ );
+};
+
+export default OperlogDetailForm;
diff --git a/src/pages/System/Operlog/index.tsx b/src/pages/System/Operlog/index.tsx
new file mode 100644
index 0000000..be46709
--- /dev/null
+++ b/src/pages/System/Operlog/index.tsx
@@ -0,0 +1,411 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess } from '@umijs/max';
+import type { FormInstance } from 'antd';
+import { Button, message, Modal } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
+import { getOperlogList, removeOperlog, addOperlog, updateOperlog, cleanAllOperlog, exportOperlog } from '@/services/monitor/operlog';
+import UpdateForm from './detail';
+import { getDictValueEnum } from '@/services/system/dict';
+import DictTag from '@/components/DictTag';
+
+/**
+ * 添加节点
+ *
+ * @param fields
+ */
+const handleAdd = async (fields: API.Monitor.Operlog) => {
+ const hide = message.loading('正在添加');
+ try {
+ const resp = await addOperlog({ ...fields });
+ hide();
+ if (resp.code === 200) {
+ message.success('添加成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('添加失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 更新节点
+ *
+ * @param fields
+ */
+const handleUpdate = async (fields: API.Monitor.Operlog) => {
+ const hide = message.loading('正在更新');
+ try {
+ const resp = await updateOperlog(fields);
+ hide();
+ if (resp.code === 200) {
+ message.success('更新成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.Monitor.Operlog[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ const resp = await removeOperlog(selectedRows.map((row) => row.operId).join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+/**
+ * 清空所有记录
+ *
+ */
+const handleCleanAll = async () => {
+ const hide = message.loading('正在清空');
+ try {
+ const resp = await cleanAllOperlog();
+ hide();
+ if (resp.code === 200) {
+ message.success('清空成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('清空失败,请重试');
+ return false;
+ }
+};
+
+
+/**
+ * 导出数据
+ *
+ *
+ */
+const handleExport = async () => {
+ const hide = message.loading('正在导出');
+ try {
+ await exportOperlog();
+ hide();
+ message.success('导出成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('导出失败,请重试');
+ return false;
+ }
+};
+
+
+const OperlogTableList: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<API.Monitor.Operlog>();
+ const [selectedRows, setSelectedRows] = useState<API.Monitor.Operlog[]>([]);
+
+ const [businessTypeOptions, setBusinessTypeOptions] = useState<any>([]);
+ const [operatorTypeOptions, setOperatorTypeOptions] = useState<any>([]);
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+ getDictValueEnum('sys_oper_type', true).then((data) => {
+ setBusinessTypeOptions(data);
+ });
+ getDictValueEnum('sys_oper_type', true).then((data) => {
+ setOperatorTypeOptions(data);
+ });
+ getDictValueEnum('sys_common_status', true).then((data) => {
+ setStatusOptions(data);
+ });
+ }, []);
+
+ const columns: ProColumns<API.Monitor.Operlog>[] = [
+ {
+ title: <FormattedMessage id="monitor.operlog.oper_id" defaultMessage="日志主键" />,
+ dataIndex: 'operId',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.title" defaultMessage="操作模块" />,
+ dataIndex: 'title',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.business_type" defaultMessage="业务类型" />,
+ dataIndex: 'businessType',
+ valueType: 'select',
+ valueEnum: businessTypeOptions,
+ render: (_, record) => {
+ return (<DictTag enums={businessTypeOptions} value={record.businessType} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.request_method" defaultMessage="请求方式" />,
+ dataIndex: 'requestMethod',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.operator_type" defaultMessage="操作类别" />,
+ dataIndex: 'operatorType',
+ valueType: 'select',
+ valueEnum: operatorTypeOptions,
+ render: (_, record) => {
+ return (<DictTag enums={operatorTypeOptions} value={record.operatorType} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.oper_name" defaultMessage="操作人员" />,
+ dataIndex: 'operName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.oper_ip" defaultMessage="主机地址" />,
+ dataIndex: 'operIp',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.oper_location" defaultMessage="操作地点" />,
+ dataIndex: 'operLocation',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.status" defaultMessage="操作状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (<DictTag key="status" enums={statusOptions} value={record.status} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="monitor.operlog.oper_time" defaultMessage="操作时间" />,
+ dataIndex: 'operTime',
+ valueType: 'dateTime',
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '120px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ hidden={!access.hasPerms('system:operlog:edit')}
+ onClick={() => {
+ setModalVisible(true);
+ setCurrentRow(record);
+ }}
+ >
+ 详细
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.Monitor.Operlog>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="operId"
+ key="operlogList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('system:operlog:add')}
+ onClick={async () => {
+ setCurrentRow(undefined);
+ setModalVisible(true);
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('system:operlog:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ <Button
+ type="primary"
+ key="clean"
+ danger
+ hidden={!access.hasPerms('system:operlog:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认清空所有数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleCleanAll();
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.cleanAll" defaultMessage="清空" />
+ </Button>,
+ <Button
+ type="primary"
+ key="export"
+ hidden={!access.hasPerms('system:operlog:export')}
+ onClick={async () => {
+ handleExport();
+ }}
+ >
+ <PlusOutlined />
+ <FormattedMessage id="pages.searchTable.export" defaultMessage="导出" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getOperlogList({ ...params } as API.Monitor.OperlogListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('system:operlog:del')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <UpdateForm
+ onSubmit={async (values) => {
+ let success = false;
+ if (values.operId) {
+ success = await handleUpdate({ ...values } as API.Monitor.Operlog);
+ } else {
+ success = await handleAdd({ ...values } as API.Monitor.Operlog);
+ }
+ if (success) {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ }}
+ open={modalVisible}
+ values={currentRow || {}}
+ businessTypeOptions={businessTypeOptions}
+ operatorTypeOptions={operatorTypeOptions}
+ statusOptions={statusOptions}
+ />
+ </PageContainer>
+ );
+};
+
+export default OperlogTableList;
diff --git a/src/pages/System/Post/edit.tsx b/src/pages/System/Post/edit.tsx
new file mode 100644
index 0000000..555fcde
--- /dev/null
+++ b/src/pages/System/Post/edit.tsx
@@ -0,0 +1,166 @@
+import React, { useEffect } from 'react';
+import {
+ ProForm,
+ ProFormDigit,
+ ProFormText,
+ ProFormRadio,
+ ProFormTextArea,
+ } from '@ant-design/pro-components';
+import { Form, Modal} from 'antd';
+import { useIntl, FormattedMessage } from '@umijs/max';
+import { DictValueEnumObj } from '@/components/DictTag';
+
+export type PostFormData = Record<string, unknown> & Partial<API.System.Post>;
+
+export type PostFormProps = {
+ onCancel: (flag?: boolean, formVals?: PostFormData) => void;
+ onSubmit: (values: PostFormData) => Promise<void>;
+ open: boolean;
+ values: Partial<API.System.Post>;
+ statusOptions: DictValueEnumObj;
+};
+
+const PostForm: React.FC<PostFormProps> = (props) => {
+ const [form] = Form.useForm();
+
+ const { statusOptions, } = props;
+
+ useEffect(() => {
+ form.resetFields();
+ form.setFieldsValue({
+ postId: props.values.postId,
+ postCode: props.values.postCode,
+ postName: props.values.postName,
+ postSort: props.values.postSort,
+ status: props.values.status,
+ createBy: props.values.createBy,
+ createTime: props.values.createTime,
+ updateBy: props.values.updateBy,
+ updateTime: props.values.updateTime,
+ remark: props.values.remark,
+ });
+ }, [form, props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit(values as PostFormData);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'system.post.title',
+ defaultMessage: '编辑岗位信息',
+ })}
+ open={props.open}
+ forceRender
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ form={form}
+ grid={true}
+ submitter={false}
+ layout="horizontal"
+ onFinish={handleFinish}>
+ <ProFormDigit
+ name="postId"
+ label={intl.formatMessage({
+ id: 'system.post.post_id',
+ defaultMessage: '岗位编号',
+ })}
+ placeholder="请输入岗位编号"
+ disabled
+ hidden={true}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入岗位编号!" defaultMessage="请输入岗位编号!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="postName"
+ label={intl.formatMessage({
+ id: 'system.post.post_name',
+ defaultMessage: '岗位名称',
+ })}
+ placeholder="请输入岗位名称"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入岗位名称!" defaultMessage="请输入岗位名称!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="postCode"
+ label={intl.formatMessage({
+ id: 'system.post.post_code',
+ defaultMessage: '岗位编码',
+ })}
+ placeholder="请输入岗位编码"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入岗位编码!" defaultMessage="请输入岗位编码!" />,
+ },
+ ]}
+ />
+ <ProFormDigit
+ name="postSort"
+ label={intl.formatMessage({
+ id: 'system.post.post_sort',
+ defaultMessage: '显示顺序',
+ })}
+ placeholder="请输入显示顺序"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入显示顺序!" defaultMessage="请输入显示顺序!" />,
+ },
+ ]}
+ />
+ <ProFormRadio.Group
+ valueEnum={statusOptions}
+ name="status"
+ label={intl.formatMessage({
+ id: 'system.post.status',
+ defaultMessage: '状态',
+ })}
+ placeholder="请输入状态"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入状态!" defaultMessage="请输入状态!" />,
+ },
+ ]}
+ />
+ <ProFormTextArea
+ name="remark"
+ label={intl.formatMessage({
+ id: 'system.post.remark',
+ defaultMessage: '备注',
+ })}
+ placeholder="请输入备注"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入备注!" defaultMessage="请输入备注!" />,
+ },
+ ]}
+ />
+ </ProForm>
+ </Modal>
+ );
+};
+
+export default PostForm;
diff --git a/src/pages/System/Post/index.tsx b/src/pages/System/Post/index.tsx
new file mode 100644
index 0000000..ad98aae
--- /dev/null
+++ b/src/pages/System/Post/index.tsx
@@ -0,0 +1,366 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess } from '@umijs/max';
+import type { FormInstance } from 'antd';
+import { Button, message, Modal } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons';
+import { getPostList, removePost, addPost, updatePost, exportPost } from '@/services/system/post';
+import UpdateForm from './edit';
+import { getDictValueEnum } from '@/services/system/dict';
+import DictTag from '@/components/DictTag';
+
+/**
+ * 添加节点
+ *
+ * @param fields
+ */
+const handleAdd = async (fields: API.System.Post) => {
+ const hide = message.loading('正在添加');
+ try {
+ const resp = await addPost({ ...fields });
+ hide();
+ if (resp.code === 200) {
+ message.success('添加成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('添加失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 更新节点
+ *
+ * @param fields
+ */
+const handleUpdate = async (fields: API.System.Post) => {
+ const hide = message.loading('正在更新');
+ try {
+ const resp = await updatePost(fields);
+ hide();
+ if (resp.code === 200) {
+ message.success('更新成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.System.Post[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ const resp = await removePost(selectedRows.map((row) => row.postId).join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleRemoveOne = async (selectedRow: API.System.Post) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRow) return true;
+ try {
+ const params = [selectedRow.postId];
+ const resp = await removePost(params.join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+/**
+ * 导出数据
+ *
+ *
+ */
+const handleExport = async () => {
+ const hide = message.loading('正在导出');
+ try {
+ await exportPost();
+ hide();
+ message.success('导出成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('导出失败,请重试');
+ return false;
+ }
+};
+
+
+const PostTableList: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<API.System.Post>();
+ const [selectedRows, setSelectedRows] = useState<API.System.Post[]>([]);
+
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+ getDictValueEnum('sys_normal_disable').then((data) => {
+ setStatusOptions(data);
+ });
+ }, []);
+
+ const columns: ProColumns<API.System.Post>[] = [
+ {
+ title: <FormattedMessage id="system.post.post_id" defaultMessage="岗位编号" />,
+ dataIndex: 'postId',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.post.post_code" defaultMessage="岗位编码" />,
+ dataIndex: 'postCode',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.post.post_name" defaultMessage="岗位名称" />,
+ dataIndex: 'postName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.post.post_sort" defaultMessage="显示顺序" />,
+ dataIndex: 'postSort',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.post.status" defaultMessage="状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (<DictTag enums={statusOptions} value={record.status} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '220px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ hidden={!access.hasPerms('system:post:edit')}
+ onClick={() => {
+ setModalVisible(true);
+ setCurrentRow(record);
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ danger
+ key="batchRemove"
+ hidden={!access.hasPerms('system:post:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemoveOne(record);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.System.Post>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="postId"
+ key="postList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('system:post:add')}
+ onClick={async () => {
+ setCurrentRow(undefined);
+ setModalVisible(true);
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('system:post:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() {},
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ <Button
+ type="primary"
+ key="export"
+ hidden={!access.hasPerms('system:post:export')}
+ onClick={async () => {
+ handleExport();
+ }}
+ >
+ <PlusOutlined />
+ <FormattedMessage id="pages.searchTable.export" defaultMessage="导出" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getPostList({ ...params } as API.System.PostListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('system:post:del')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <UpdateForm
+ onSubmit={async (values) => {
+ let success = false;
+ if (values.postId) {
+ success = await handleUpdate({ ...values } as API.System.Post);
+ } else {
+ success = await handleAdd({ ...values } as API.System.Post);
+ }
+ if (success) {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ }}
+ open={modalVisible}
+ values={currentRow || {}}
+ statusOptions={statusOptions}
+ />
+ </PageContainer>
+ );
+};
+
+export default PostTableList;
diff --git a/src/pages/System/Role/authUser.tsx b/src/pages/System/Role/authUser.tsx
new file mode 100644
index 0000000..c553d99
--- /dev/null
+++ b/src/pages/System/Role/authUser.tsx
@@ -0,0 +1,274 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess, history, useParams } from '@umijs/max';
+import { Button, Modal, message } from 'antd';
+import { ActionType, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined, RollbackOutlined } from '@ant-design/icons';
+import { authUserSelectAll, authUserCancel, authUserCancelAll, allocatedUserList, unallocatedUserList } from '@/services/system/role';
+import { getDictValueEnum } from '@/services/system/dict';
+import DictTag from '@/components/DictTag';
+import UserSelectorModal from './components/UserSelectorModal';
+import { HttpResult } from '@/enums/httpEnum';
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const cancelAuthUserAll = async (roleId: string, selectedRows: API.System.User[]) => {
+ const hide = message.loading('正在取消授权');
+ if (!selectedRows) return true;
+ try {
+ const userIds = selectedRows.map((row) => row.userId).join(',');
+ const resp = await authUserCancelAll({roleId, userIds});
+ hide();
+ if (resp.code === 200) {
+ message.success('取消授权成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('取消授权失败,请重试');
+ return false;
+ }
+};
+
+const cancelAuthUser = async (roleId: string, userId: number) => {
+ const hide = message.loading('正在取消授权');
+ try {
+ const resp = await authUserCancel({ userId, roleId });
+ hide();
+ if (resp.code === 200) {
+ message.success('取消授权成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('取消授权失败,请重试');
+ return false;
+ }
+};
+
+
+const AuthUserTableList: React.FC = () => {
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [selectedRows, setSelectedRows] = useState<API.System.User[]>([]);
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ const params = useParams();
+ if (params.id === undefined) {
+ history.back();
+ }
+ const roleId = params.id || '0';
+
+ useEffect(() => {
+ getDictValueEnum('sys_normal_disable').then((data) => {
+ setStatusOptions(data);
+ });
+ }, []);
+
+ const columns: ProColumns<API.System.User>[] = [
+ {
+ title: <FormattedMessage id="system.user.user_id" defaultMessage="用户编号" />,
+ dataIndex: 'deptId',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.user.user_name" defaultMessage="用户账号" />,
+ dataIndex: 'userName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.user.nick_name" defaultMessage="用户昵称" />,
+ dataIndex: 'nickName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.user.phonenumber" defaultMessage="手机号码" />,
+ dataIndex: 'phonenumber',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.role.create_time" defaultMessage="创建时间" />,
+ dataIndex: 'createTime',
+ valueType: 'dateRange',
+ render: (_, record) => {
+ return (<span>{record.createTime.toString()} </span>);
+ },
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.user.status" defaultMessage="帐号状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (<DictTag enums={statusOptions} value={record.status} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '60px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ danger
+ icon={<DeleteOutlined />}
+ key="remove"
+ hidden={!access.hasPerms('system:role:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确认要取消该用户' + record.userName + '"角色授权吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await cancelAuthUser(roleId, record.userId);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 取消授权
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.System.User>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ rowKey="userId"
+ key="userList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('system:role:add')}
+ onClick={async () => {
+ setModalVisible(true);
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="system.role.auth.addUser" defaultMessage="添加用户" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('system:role:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await cancelAuthUserAll(roleId, selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="system.role.auth.cancelAll" defaultMessage="批量取消授权" />
+ </Button>,
+ <Button
+ type="primary"
+ key="back"
+ onClick={async () => {
+ history.back();
+ }}
+ >
+ <RollbackOutlined />
+ <FormattedMessage id="pages.goback" defaultMessage="返回" />
+ </Button>,
+ ]}
+ request={(params) =>
+ allocatedUserList({ ...params, roleId } as API.System.RoleListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ <UserSelectorModal
+ open={modalVisible}
+ onSubmit={(values: React.Key[]) => {
+ const userIds = values.join(",");
+ if (userIds === "") {
+ message.warning("请选择要分配的用户");
+ return;
+ }
+ authUserSelectAll({ roleId: roleId, userIds: userIds }).then(resp => {
+ if (resp.code === HttpResult.SUCCESS) {
+ message.success('更新成功!');
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ } else {
+ message.warning(resp.msg);
+ }
+ })
+ setModalVisible(false);
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ }}
+ params={{roleId}}
+ request={(params) =>
+ unallocatedUserList({ ...params } as API.System.RoleListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.rows.length,
+ success: true,
+ };
+ return result;
+ })
+ }
+ />
+ </PageContainer>
+ );
+};
+
+export default AuthUserTableList;
diff --git a/src/pages/System/Role/components/DataScope.tsx b/src/pages/System/Role/components/DataScope.tsx
new file mode 100644
index 0000000..a3dcce0
--- /dev/null
+++ b/src/pages/System/Role/components/DataScope.tsx
@@ -0,0 +1,233 @@
+import React, { useEffect, useState } from 'react';
+import { Checkbox, Col, Form, Modal, Row, Tree } from 'antd';
+import { FormattedMessage, useIntl } from '@umijs/max';
+import { Key, ProForm, ProFormDigit, ProFormSelect, ProFormText } from '@ant-design/pro-components';
+import { DataNode } from 'antd/es/tree';
+import { CheckboxValueType } from 'antd/es/checkbox/Group';
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2023/02/06
+ *
+ * */
+
+export type FormValueType = any & Partial<API.System.Dept>;
+
+export type DataScopeFormProps = {
+ onCancel: (flag?: boolean, formVals?: FormValueType) => void;
+ onSubmit: (values: FormValueType) => Promise<void>;
+ open: boolean;
+ values: Partial<API.System.Role>;
+ deptTree: DataNode[];
+ deptCheckedKeys: string[];
+};
+
+const DataScopeForm: React.FC<DataScopeFormProps> = (props) => {
+ const [form] = Form.useForm();
+
+ const { deptTree, deptCheckedKeys } = props;
+ const [dataScopeType, setDataScopeType] = useState<string | undefined>('1');
+ const [deptIds, setDeptIds] = useState<string[] | {checked: string[], halfChecked: string[]}>([]);
+ const [deptTreeExpandKey, setDeptTreeExpandKey] = useState<Key[]>([]);
+ const [checkStrictly, setCheckStrictly] = useState<boolean>(true);
+
+
+ useEffect(() => {
+ setDeptIds(deptCheckedKeys);
+ form.resetFields();
+ form.setFieldsValue({
+ roleId: props.values.roleId,
+ roleName: props.values.roleName,
+ roleKey: props.values.roleKey,
+ dataScope: props.values.dataScope,
+ });
+ setDataScopeType(props.values.dataScope);
+ }, [props.values]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit({ ...values, deptIds } as FormValueType);
+ };
+
+ const getAllDeptNode = (node: DataNode[]) => {
+ let keys: any[] = [];
+ node.forEach(value => {
+ keys.push(value.key);
+ if(value.children) {
+ keys = keys.concat(getAllDeptNode(value.children));
+ }
+ });
+ return keys;
+ }
+
+ const deptAllNodes = getAllDeptNode(deptTree);
+
+
+ const onDeptOptionChange = (checkedValues: CheckboxValueType[]) => {
+ if(checkedValues.includes('deptExpand')) {
+ setDeptTreeExpandKey(deptAllNodes);
+ } else {
+ setDeptTreeExpandKey([]);
+ }
+ if(checkedValues.includes('deptNodeAll')) {
+ setDeptIds(deptAllNodes);
+ } else {
+ setDeptIds([]);
+ }
+
+ if(checkedValues.includes('deptCheckStrictly')) {
+ setCheckStrictly(false);
+ } else {
+ setCheckStrictly(true);
+ }
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'system.user.auth.role',
+ defaultMessage: '分配角色',
+ })}
+ open={props.open}
+ destroyOnClose
+ forceRender
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ form={form}
+ grid={true}
+ layout="horizontal"
+ onFinish={handleFinish}
+ initialValues={{
+ login_password: '',
+ confirm_password: '',
+ }}
+ >
+
+ <ProFormDigit
+ name="roleId"
+ label={intl.formatMessage({
+ id: 'system.role.role_id',
+ defaultMessage: '角色编号',
+ })}
+ colProps={{ md: 12, xl: 12 }}
+ placeholder="请输入角色编号"
+ disabled
+ hidden={true}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入角色编号!" defaultMessage="请输入角色编号!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="roleName"
+ label={intl.formatMessage({
+ id: 'system.role.role_name',
+ defaultMessage: '角色名称',
+ })}
+ disabled
+ placeholder="请输入角色名称"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入角色名称!" defaultMessage="请输入角色名称!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="roleKey"
+ label={intl.formatMessage({
+ id: 'system.role.role_key',
+ defaultMessage: '权限字符串',
+ })}
+ disabled
+ placeholder="请输入角色权限字符串"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入角色权限字符串!" defaultMessage="请输入角色权限字符串!" />,
+ },
+ ]}
+ />
+ <ProFormSelect
+ name="dataScope"
+ label='权限范围'
+ initialValue={'1'}
+ placeholder="请输入用户性别"
+ valueEnum={{
+ "1": "全部数据权限",
+ "2": "自定数据权限",
+ "3": "本部门数据权限",
+ "4": "本部门及以下数据权限",
+ "5": "仅本人数据权限"
+ }}
+ rules={[
+ {
+ required: true,
+ },
+ ]}
+ fieldProps={{
+ onChange: (value) => {
+ setDataScopeType(value);
+ },
+ }}
+ />
+ <ProForm.Item
+ name="deptIds"
+ label={intl.formatMessage({
+ id: 'system.role.auth',
+ defaultMessage: '菜单权限',
+ })}
+ required={dataScopeType === '1'}
+ hidden={dataScopeType !== '1'}
+ >
+ <Row gutter={[16, 16]}>
+ <Col md={24}>
+ <Checkbox.Group
+ options={[
+ { label: '展开/折叠', value: 'deptExpand' },
+ { label: '全选/全不选', value: 'deptNodeAll' },
+ // { label: '父子联动', value: 'deptCheckStrictly' },
+ ]}
+ onChange={onDeptOptionChange} />
+ </Col>
+ <Col md={24}>
+ <Tree
+ checkable={true}
+ checkStrictly={checkStrictly}
+ expandedKeys={deptTreeExpandKey}
+ treeData={deptTree}
+ checkedKeys={deptIds}
+ defaultCheckedKeys={deptCheckedKeys}
+ onCheck={(checkedKeys: any, checkInfo: any) => {
+ console.log(checkedKeys, checkInfo);
+ if(checkStrictly) {
+ return setDeptIds(checkedKeys.checked);
+ } else {
+ return setDeptIds({checked: checkedKeys, halfChecked: checkInfo.halfCheckedKeys});
+ }
+ }}
+ onExpand={(expandedKeys: Key[]) => {
+ setDeptTreeExpandKey(deptTreeExpandKey.concat(expandedKeys));
+ }}
+ />
+ </Col>
+ </Row>
+ </ProForm.Item>
+ </ProForm>
+ </Modal>
+ );
+};
+
+export default DataScopeForm;
diff --git a/src/pages/System/Role/components/UserSelectorModal.tsx b/src/pages/System/Role/components/UserSelectorModal.tsx
new file mode 100644
index 0000000..fdcfb85
--- /dev/null
+++ b/src/pages/System/Role/components/UserSelectorModal.tsx
@@ -0,0 +1,125 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { Modal } from 'antd';
+import { FormattedMessage, useIntl } from '@umijs/max';
+import { ActionType, ParamsType, ProColumns, ProTable, RequestData } from '@ant-design/pro-components';
+import { getDictValueEnum } from '@/services/system/dict';
+import DictTag from '@/components/DictTag';
+
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2023/02/10
+ *
+ * */
+
+export type DataScopeFormProps = {
+ onCancel: () => void;
+ onSubmit: (values: React.Key[]) => void;
+ open: boolean;
+ params: ParamsType;
+ request?: (params: Record<string, any>) => Promise<Partial<RequestData<API.System.User>>>;
+};
+
+const UserSelectorModal: React.FC<DataScopeFormProps> = (props) => {
+
+ const actionRef = useRef<ActionType>();
+ const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ useEffect(() => {
+ getDictValueEnum('sys_normal_disable').then((data) => {
+ setStatusOptions(data);
+ });
+ }, [props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ props.onSubmit(selectedRowKeys);
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+
+ const columns: ProColumns<API.System.User>[] = [
+ {
+ title: <FormattedMessage id="system.user.user_id" defaultMessage="用户编号" />,
+ dataIndex: 'userId',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.user.user_name" defaultMessage="用户账号" />,
+ dataIndex: 'userName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.user.nick_name" defaultMessage="用户昵称" />,
+ dataIndex: 'nickName',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.user.phonenumber" defaultMessage="手机号码" />,
+ dataIndex: 'phonenumber',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.user.status" defaultMessage="帐号状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ hideInSearch: true,
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (<DictTag enums={statusOptions} value={record.status} />);
+ },
+ },
+ {
+ title: <FormattedMessage id="system.user.create_time" defaultMessage="创建时间" />,
+ dataIndex: 'createTime',
+ valueType: 'dateRange',
+ hideInSearch: true,
+ render: (_, record) => {
+ return (<span>{record.createTime.toString()} </span>);
+ },
+ }
+ ];
+
+ return (
+ <Modal
+ width={800}
+ title={intl.formatMessage({
+ id: 'system.role.auth.user',
+ defaultMessage: '选择用户',
+ })}
+ open={props.open}
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProTable<API.System.User>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ rowKey="userId"
+ key="userList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolbar={{}}
+ params={props.params}
+ request={props.request}
+ columns={columns}
+ rowSelection={{
+ onChange: (selectedRowKeys: React.Key[]) => {
+ setSelectedRowKeys(selectedRowKeys);
+ },
+ }}
+ />
+ </Modal>
+ );
+};
+
+export default UserSelectorModal;
diff --git a/src/pages/System/Role/edit.tsx b/src/pages/System/Role/edit.tsx
new file mode 100644
index 0000000..580493a
--- /dev/null
+++ b/src/pages/System/Role/edit.tsx
@@ -0,0 +1,199 @@
+import React, { useEffect, useState } from 'react';
+import {
+ ProForm,
+ ProFormDigit,
+ ProFormText,
+ ProFormRadio,
+ ProFormTextArea,
+} from '@ant-design/pro-components';
+import { Form, Modal } from 'antd';
+import { useIntl, FormattedMessage } from '@umijs/max';
+import Tree, { DataNode } from 'antd/es/tree';
+import { DictValueEnumObj } from '@/components/DictTag';
+
+export type RoleFormData = Record<string, unknown> & Partial<API.System.Role>;
+
+export type RoleFormProps = {
+ onCancel: (flag?: boolean, formVals?: RoleFormData) => void;
+ onSubmit: (values: RoleFormData) => Promise<void>;
+ open: boolean;
+ values: Partial<API.System.Role>;
+ menuTree: DataNode[];
+ menuCheckedKeys: string[];
+ statusOptions: DictValueEnumObj;
+};
+
+const RoleForm: React.FC<RoleFormProps> = (props) => {
+ const [form] = Form.useForm();
+ const { menuTree, menuCheckedKeys } = props;
+ const [menuIds, setMenuIds] = useState<string[]>([]);
+ const { statusOptions } = props;
+
+ useEffect(() => {
+ form.resetFields();
+ form.setFieldsValue({
+ roleId: props.values.roleId,
+ roleName: props.values.roleName,
+ roleKey: props.values.roleKey,
+ roleSort: props.values.roleSort,
+ dataScope: props.values.dataScope,
+ menuCheckStrictly: props.values.menuCheckStrictly,
+ deptCheckStrictly: props.values.deptCheckStrictly,
+ status: props.values.status,
+ delFlag: props.values.delFlag,
+ createBy: props.values.createBy,
+ createTime: props.values.createTime,
+ updateBy: props.values.updateBy,
+ updateTime: props.values.updateTime,
+ remark: props.values.remark,
+ });
+ }, [form, props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit({ ...values, menuIds } as RoleFormData);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'system.role.title',
+ defaultMessage: '编辑角色信息',
+ })}
+ forceRender
+ open={props.open}
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ form={form}
+ grid={true}
+ layout="horizontal"
+ submitter={false}
+ onFinish={handleFinish}>
+ <ProFormDigit
+ name="roleId"
+ label={intl.formatMessage({
+ id: 'system.role.role_id',
+ defaultMessage: '角色编号',
+ })}
+ placeholder="请输入角色编号"
+ disabled
+ hidden={true}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入角色编号!" defaultMessage="请输入角色编号!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="roleName"
+ label={intl.formatMessage({
+ id: 'system.role.role_name',
+ defaultMessage: '角色名称',
+ })}
+ placeholder="请输入角色名称"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入角色名称!" defaultMessage="请输入角色名称!" />,
+ },
+ ]}
+ />
+ <ProFormText
+ name="roleKey"
+ label={intl.formatMessage({
+ id: 'system.role.role_key',
+ defaultMessage: '权限字符串',
+ })}
+ placeholder="请输入角色权限字符串"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入角色权限字符串!" defaultMessage="请输入角色权限字符串!" />,
+ },
+ ]}
+ />
+ <ProFormDigit
+ name="roleSort"
+ label={intl.formatMessage({
+ id: 'system.role.role_sort',
+ defaultMessage: '显示顺序',
+ })}
+ placeholder="请输入显示顺序"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入显示顺序!" defaultMessage="请输入显示顺序!" />,
+ },
+ ]}
+ fieldProps = {{
+ defaultValue: 1
+ }}
+ />
+ <ProFormRadio.Group
+ valueEnum={statusOptions}
+ name="status"
+ label={intl.formatMessage({
+ id: 'system.role.status',
+ defaultMessage: '角色状态',
+ })}
+ placeholder="请输入角色状态"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入角色状态!" defaultMessage="请输入角色状态!" />,
+ },
+ ]}
+ fieldProps = {{
+ defaultValue: "0"
+ }}
+ />
+ <ProForm.Item
+ name="menuIds"
+ label={intl.formatMessage({
+ id: 'system.role.auth',
+ defaultMessage: '菜单权限',
+ })}
+ >
+ <Tree
+ checkable={true}
+ multiple={true}
+ checkStrictly={true}
+ defaultExpandAll={false}
+ treeData={menuTree}
+ defaultCheckedKeys={menuCheckedKeys}
+ onCheck={(checkedKeys: any) => {
+ return setMenuIds(checkedKeys.checked);
+ }}
+ />
+ </ProForm.Item>
+ <ProFormTextArea
+ name="remark"
+ label={intl.formatMessage({
+ id: 'system.role.remark',
+ defaultMessage: '备注',
+ })}
+ placeholder="请输入备注"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入备注!" defaultMessage="请输入备注!" />,
+ },
+ ]}
+ />
+ </ProForm>
+ </Modal>
+ );
+};
+
+export default RoleForm;
diff --git a/src/pages/System/Role/index.tsx b/src/pages/System/Role/index.tsx
new file mode 100644
index 0000000..ea15b2e
--- /dev/null
+++ b/src/pages/System/Role/index.tsx
@@ -0,0 +1,513 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess, history } from '@umijs/max';
+import { DataNode } from 'antd/es/tree';
+import { Button, message, Modal, Dropdown, FormInstance, Space, Switch } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, EditOutlined, DeleteOutlined, ExclamationCircleOutlined, DownOutlined } from '@ant-design/icons';
+import { getRoleList, removeRole, addRole, updateRole, exportRole, getRoleMenuList, changeRoleStatus, updateRoleDataScope, getDeptTreeSelect, getRole } from '@/services/system/role';
+import UpdateForm from './edit';
+import { getDictValueEnum } from '@/services/system/dict';
+import { formatTreeData } from '@/utils/tree';
+import { getMenuTree } from '@/services/system/menu';
+import DataScopeForm from './components/DataScope';
+
+const { confirm } = Modal;
+
+/**
+ * 添加节点
+ *
+ * @param fields
+ */
+const handleAdd = async (fields: API.System.Role) => {
+ const hide = message.loading('正在添加');
+ try {
+ const resp = await addRole({ ...fields });
+ hide();
+ if (resp.code === 200) {
+ message.success('添加成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('添加失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 更新节点
+ *
+ * @param fields
+ */
+const handleUpdate = async (fields: API.System.Role) => {
+ const hide = message.loading('正在更新');
+ try {
+ const resp = await updateRole(fields);
+ hide();
+ if (resp.code === 200) {
+ message.success('更新成功');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.System.Role[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ const resp = await removeRole(selectedRows.map((row) => row.roleId).join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleRemoveOne = async (selectedRow: API.System.Role) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRow) return true;
+ try {
+ const params = [selectedRow.roleId];
+ const resp = await removeRole(params.join(','));
+ hide();
+ if (resp.code === 200) {
+ message.success('删除成功,即将刷新');
+ } else {
+ message.error(resp.msg);
+ }
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+/**
+ * 导出数据
+ *
+ *
+ */
+const handleExport = async () => {
+ const hide = message.loading('正在导出');
+ try {
+ await exportRole();
+ hide();
+ message.success('导出成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('导出失败,请重试');
+ return false;
+ }
+};
+
+
+const RoleTableList: React.FC = () => {
+
+ const [messageApi, contextHolder] = message.useMessage();
+ const formTableRef = useRef<FormInstance>();
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+ const [dataScopeModalOpen, setDataScopeModalOpen] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<API.System.Role>();
+ const [selectedRows, setSelectedRows] = useState<API.System.Role[]>([]);
+
+ const [menuTree, setMenuTree] = useState<DataNode[]>();
+ const [menuIds, setMenuIds] = useState<string[]>([]);
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+ getDictValueEnum('sys_normal_disable').then((data) => {
+ setStatusOptions(data);
+ });
+ }, []);
+
+ const showChangeStatusConfirm = (record: API.System.Role) => {
+ let text = record.status === "1" ? "启用" : "停用";
+ const newStatus = record.status === '0' ? '1' : '0';
+ confirm({
+ title: `确认要${text}${record.roleName}角色吗?`,
+ onOk() {
+ changeRoleStatus(record.roleId, newStatus).then(resp => {
+ if (resp.code === 200) {
+ messageApi.open({
+ type: 'success',
+ content: '更新成功!',
+ });
+ actionRef.current?.reload();
+ } else {
+ messageApi.open({
+ type: 'error',
+ content: '更新失败!',
+ });
+ }
+ });
+ },
+ });
+ };
+
+ const columns: ProColumns<API.System.Role>[] = [
+ {
+ title: <FormattedMessage id="system.role.role_id" defaultMessage="角色编号" />,
+ dataIndex: 'roleId',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.role.role_name" defaultMessage="角色名称" />,
+ dataIndex: 'roleName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.role.role_key" defaultMessage="角色权限字符串" />,
+ dataIndex: 'roleKey',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.role.role_sort" defaultMessage="显示顺序" />,
+ dataIndex: 'roleSort',
+ valueType: 'text',
+ hideInSearch: true,
+ },
+ {
+ title: <FormattedMessage id="system.role.status" defaultMessage="角色状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (
+ <Switch
+ checked={record.status === '0'}
+ checkedChildren="正常"
+ unCheckedChildren="停用"
+ defaultChecked
+ onClick={() => showChangeStatusConfirm(record)}
+ />)
+ },
+ },
+ {
+ title: <FormattedMessage id="system.role.create_time" defaultMessage="创建时间" />,
+ dataIndex: 'createTime',
+ valueType: 'dateRange',
+ render: (_, record) => {
+ return (<span>{record.createTime.toString()} </span>);
+ },
+ search: {
+ transform: (value) => {
+ return {
+ 'params[beginTime]': value[0],
+ 'params[endTime]': value[1],
+ };
+ },
+ },
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '220px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ icon=<EditOutlined />
+ hidden={!access.hasPerms('system:role:edit')}
+ onClick={() => {
+ getRoleMenuList(record.roleId).then((res) => {
+ if (res.code === 200) {
+ const treeData = formatTreeData(res.menus);
+ setMenuTree(treeData);
+ setMenuIds(res.checkedKeys.map(item => {
+ return `${item}`
+ }));
+ setModalVisible(true);
+ setCurrentRow(record);
+ } else {
+ message.warning(res.msg);
+ }
+ });
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ danger
+ key="batchRemove"
+ icon=<DeleteOutlined />
+ hidden={!access.hasPerms('system:role:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemoveOne(record);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ <Dropdown
+ key="more"
+ menu={{
+ items: [
+ {
+ label: '数据权限',
+ key: 'datascope',
+ disabled: !access.hasPerms('system:role:edit'),
+ },
+ {
+ label: '分配用户',
+ key: 'authUser',
+ disabled: !access.hasPerms('system:role:edit'),
+ },
+ ],
+ onClick: ({ key }: any) => {
+ if (key === 'datascope') {
+ getRole(record.roleId).then(resp => {
+ if(resp.code === 200) {
+ setCurrentRow(resp.data);
+ setDataScopeModalOpen(true);
+ }
+ })
+ getDeptTreeSelect(record.roleId).then(resp => {
+ if (resp.code === 200) {
+ setMenuTree(formatTreeData(resp.depts));
+ setMenuIds(resp.checkedKeys.map((item:number) => {
+ return `${item}`
+ }));
+ }
+ })
+ }
+ else if (key === 'authUser') {
+ history.push(`/system/role-auth/user/${record.roleId}`);
+ }
+ }
+ }}
+ >
+ <a onClick={(e) => e.preventDefault()}>
+ <Space>
+ <DownOutlined />
+ 更多
+ </Space>
+ </a>
+ </Dropdown>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ {contextHolder}
+ <div style={{ width: '100%', float: 'right' }}>
+ <ProTable<API.System.Role>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="roleId"
+ key="roleList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('system:role:add')}
+ onClick={async () => {
+ getMenuTree().then((res: any) => {
+ if (res.code === 200) {
+ const treeData = formatTreeData(res.data);
+ setMenuTree(treeData);
+ setMenuIds([]);
+ setModalVisible(true);
+ setCurrentRow(undefined);
+ } else {
+ message.warning(res.msg);
+ }
+ });
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('system:role:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ <Button
+ type="primary"
+ key="export"
+ hidden={!access.hasPerms('system:role:export')}
+ onClick={async () => {
+ handleExport();
+ }}
+ >
+ <PlusOutlined />
+ <FormattedMessage id="pages.searchTable.export" defaultMessage="导出" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getRoleList({ ...params } as API.System.RoleListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </div>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('system:role:del')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <UpdateForm
+ onSubmit={async (values) => {
+ let success = false;
+ if (values.roleId) {
+ success = await handleUpdate({ ...values } as API.System.Role);
+ } else {
+ success = await handleAdd({ ...values } as API.System.Role);
+ }
+ if (success) {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ }}
+ open={modalVisible}
+ values={currentRow || {}}
+ menuTree={menuTree || []}
+ menuCheckedKeys={menuIds || []}
+ statusOptions={statusOptions}
+ />
+ <DataScopeForm
+ onSubmit={async (values: any) => {
+ const success = await updateRoleDataScope(values);
+ if (success) {
+ setDataScopeModalOpen(false);
+ setSelectedRows([]);
+ setCurrentRow(undefined);
+ message.success('配置成功。');
+ }
+ }}
+ onCancel={() => {
+ setDataScopeModalOpen(false);
+ setSelectedRows([]);
+ setCurrentRow(undefined);
+ }}
+ open={dataScopeModalOpen}
+ values={currentRow || {}}
+ deptTree={menuTree || []}
+ deptCheckedKeys={menuIds || []}
+ />
+ </PageContainer>
+ );
+};
+
+export default RoleTableList;
diff --git a/src/pages/System/User/components/AuthRole.tsx b/src/pages/System/User/components/AuthRole.tsx
new file mode 100644
index 0000000..120d966
--- /dev/null
+++ b/src/pages/System/User/components/AuthRole.tsx
@@ -0,0 +1,81 @@
+import React, { useEffect } from 'react';
+import { Form, Modal } from 'antd';
+import { useIntl } from '@umijs/max';
+import { ProForm, ProFormSelect } from '@ant-design/pro-components';
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2023/02/06
+ *
+ * */
+
+export type FormValueType = any & Partial<API.System.Dept>;
+
+export type AuthRoleFormProps = {
+ onCancel: (flag?: boolean, formVals?: FormValueType) => void;
+ onSubmit: (values: FormValueType) => Promise<void>;
+ open: boolean;
+ roleIds: number[];
+ roles: string[];
+};
+
+const AuthRoleForm: React.FC<AuthRoleFormProps> = (props) => {
+ const [form] = Form.useForm();
+
+ useEffect(() => {
+ form.resetFields();
+ form.setFieldValue( 'roleIds', props.roleIds);
+ });
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit(values as FormValueType);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'system.user.auth.role',
+ defaultMessage: '分配角色',
+ })}
+ open={props.open}
+ destroyOnClose
+ forceRender
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ form={form}
+ grid={true}
+ layout="horizontal"
+ onFinish={handleFinish}
+ initialValues={{
+ login_password: '',
+ confirm_password: '',
+ }}
+ >
+ <ProFormSelect
+ name="roleIds"
+ mode="multiple"
+ label={intl.formatMessage({
+ id: 'system.user.role',
+ defaultMessage: '角色',
+ })}
+ options={props.roles}
+ placeholder="请选择角色"
+ rules={[{ required: true, message: '请选择角色!' }]}
+ />
+ </ProForm>
+ </Modal>
+ );
+};
+
+export default AuthRoleForm;
diff --git a/src/pages/System/User/components/DeptTree.tsx b/src/pages/System/User/components/DeptTree.tsx
new file mode 100644
index 0000000..d1a085c
--- /dev/null
+++ b/src/pages/System/User/components/DeptTree.tsx
@@ -0,0 +1,69 @@
+import React, { useState, useEffect } from 'react';
+import { Tree, message } from 'antd';
+import { getDeptTree } from '@/services/system/user';
+
+const { DirectoryTree } = Tree;
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2023/02/06
+ *
+ * */
+
+
+export type TreeProps = {
+ onSelect: (values: any) => Promise<void>;
+};
+
+const DeptTree: React.FC<TreeProps> = (props) => {
+ const [treeData, setTreeData] = useState<any>([]);
+ const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
+ const [autoExpandParent, setAutoExpandParent] = useState<boolean>(true);
+
+ const fetchDeptList = async () => {
+ const hide = message.loading('正在查询');
+ try {
+ await getDeptTree({}).then((res: any) => {
+ const exKeys = [];
+ exKeys.push('1');
+ setTreeData(res);
+ exKeys.push(res[0].children[0].id);
+ setExpandedKeys(exKeys);
+ props.onSelect(res[0].children[0]);
+ });
+ hide();
+ return true;
+ } catch (error) {
+ hide();
+ return false;
+ }
+ };
+
+ useEffect(() => {
+ fetchDeptList();
+ }, []);
+
+ const onSelect = (keys: React.Key[], info: any) => {
+ props.onSelect(info.node);
+ };
+
+ const onExpand = (expandedKeysValue: React.Key[]) => {
+ setExpandedKeys(expandedKeysValue);
+ setAutoExpandParent(false);
+ };
+
+ return (
+ <DirectoryTree
+ // multiple
+ defaultExpandAll
+ onExpand={onExpand}
+ expandedKeys={expandedKeys}
+ autoExpandParent={autoExpandParent}
+ onSelect={onSelect}
+ treeData={treeData}
+ />
+ );
+};
+
+export default DeptTree;
diff --git a/src/pages/System/User/components/ResetPwd.tsx b/src/pages/System/User/components/ResetPwd.tsx
new file mode 100644
index 0000000..5523cd6
--- /dev/null
+++ b/src/pages/System/User/components/ResetPwd.tsx
@@ -0,0 +1,95 @@
+import React from 'react';
+import { Form, Modal } from 'antd';
+import { useIntl } from '@umijs/max';
+import { ProForm, ProFormText } from '@ant-design/pro-components';
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2023/02/06
+ *
+ * */
+
+export type FormValueType = any & Partial<API.System.User>;
+
+export type UpdateFormProps = {
+ onCancel: (flag?: boolean, formVals?: FormValueType) => void;
+ onSubmit: (values: FormValueType) => Promise<void>;
+ open: boolean;
+ values: Partial<API.System.User>;
+};
+
+const UpdateForm: React.FC<UpdateFormProps> = (props) => {
+ const [form] = Form.useForm();
+ const loginPassword = Form.useWatch('password', form);
+ const userId = props.values.userId;
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit({ ...values, userId } as FormValueType);
+ };
+
+ const checkPassword = (rule: any, value: string) => {
+ if (value === loginPassword) {
+ // 校验条件自定义
+ return Promise.resolve();
+ }
+ return Promise.reject(new Error('两次密码输入不一致'));
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'system.user.reset.password',
+ defaultMessage: '密码重置',
+ })}
+ open={props.open}
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ grid={true}
+ form={form}
+ layout="horizontal"
+ onFinish={handleFinish}
+ initialValues={{
+ password: '',
+ confirm_password: '',
+ }}
+ >
+ <p>请输入用户{props.values.userName}的新密码!</p>
+ <ProFormText.Password
+ name="password"
+ label="登录密码"
+ rules={[
+ {
+ required: true,
+ message: '登录密码不可为空。',
+ },
+ ]}
+ />
+ <ProFormText.Password
+ name="confirm_password"
+ label="确认密码"
+ rules={[
+ {
+ required: true,
+ message: "确认密码",
+ },
+ { validator: checkPassword },
+ ]}
+ />
+ </ProForm>
+ </Modal>
+ );
+};
+
+export default UpdateForm;
diff --git a/src/pages/System/User/edit.tsx b/src/pages/System/User/edit.tsx
new file mode 100644
index 0000000..31d442b
--- /dev/null
+++ b/src/pages/System/User/edit.tsx
@@ -0,0 +1,279 @@
+import React, { useEffect } from 'react';
+import {
+ ProForm,
+ ProFormText,
+ ProFormSelect,
+ ProFormRadio,
+ ProFormTextArea,
+ ProFormTreeSelect,
+} from '@ant-design/pro-components';
+import { Form, Modal } from 'antd';
+import { useIntl, FormattedMessage } from '@umijs/max';
+import { DataNode } from 'antd/es/tree';
+import { DictValueEnumObj } from '@/components/DictTag';
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2023/02/06
+ *
+ * */
+
+
+export type UserFormData = Record<string, unknown> & Partial<API.System.User>;
+
+export type UserFormProps = {
+ onCancel: (flag?: boolean, formVals?: UserFormData) => void;
+ onSubmit: (values: UserFormData) => Promise<void>;
+ open: boolean;
+ values: Partial<API.System.User>;
+ sexOptions: DictValueEnumObj;
+ statusOptions: DictValueEnumObj;
+ postIds: number[];
+ posts: string[];
+ roleIds: number[];
+ roles: string[];
+ depts: DataNode[];
+};
+
+const UserForm: React.FC<UserFormProps> = (props) => {
+ const [form] = Form.useForm();
+ const userId = Form.useWatch('userId', form);
+ const { sexOptions, statusOptions, } = props;
+ const { roles, posts, depts } = props;
+
+ useEffect(() => {
+ form.resetFields();
+ form.setFieldsValue({
+ userId: props.values.userId,
+ deptId: props.values.deptId,
+ postIds: props.postIds,
+ roleIds: props.roleIds,
+ userName: props.values.userName,
+ nickName: props.values.nickName,
+ email: props.values.email,
+ phonenumber: props.values.phonenumber,
+ sex: props.values.sex,
+ avatar: props.values.avatar,
+ status: props.values.status,
+ delFlag: props.values.delFlag,
+ loginIp: props.values.loginIp,
+ loginDate: props.values.loginDate,
+ remark: props.values.remark,
+ });
+ }, [form, props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ form.submit();
+ };
+ const handleCancel = () => {
+ props.onCancel();
+ };
+ const handleFinish = async (values: Record<string, any>) => {
+ props.onSubmit(values as UserFormData);
+ };
+
+ return (
+ <Modal
+ width={640}
+ title={intl.formatMessage({
+ id: 'system.user.title',
+ defaultMessage: '编辑用户信息',
+ })}
+ open={props.open}
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <ProForm
+ grid={true}
+ form={form}
+ layout="horizontal"
+ submitter={false}
+ onFinish={handleFinish}>
+ <ProFormText
+ name="nickName"
+ label={intl.formatMessage({
+ id: 'system.user.nick_name',
+ defaultMessage: '用户昵称',
+ })}
+ placeholder="请输入用户昵称"
+ colProps={{ xs: 24, md: 12, xl: 12 }}
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage id="请输入用户昵称!" defaultMessage="请输入用户昵称!" />
+ ),
+ },
+ ]}
+ />
+ <ProFormTreeSelect
+ name="deptId"
+ label={intl.formatMessage({
+ id: 'system.user.dept_name',
+ defaultMessage: '部门',
+ })}
+ request={async () => {
+ return depts;
+ }}
+ placeholder="请输入用户部门"
+ colProps={{ md: 12, xl: 12 }}
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage id="请输入用户部门!" defaultMessage="请输入用户部门!" />
+ ),
+ },
+ ]}
+ />
+ <ProFormText
+ name="phonenumber"
+ label={intl.formatMessage({
+ id: 'system.user.phonenumber',
+ defaultMessage: '手机号码',
+ })}
+ placeholder="请输入手机号码"
+ colProps={{ md: 12, xl: 12 }}
+ rules={[
+ {
+ required: false,
+ message: (
+ <FormattedMessage id="请输入手机号码!" defaultMessage="请输入手机号码!" />
+ ),
+ },
+ ]}
+ />
+ <ProFormText
+ name="email"
+ label={intl.formatMessage({
+ id: 'system.user.email',
+ defaultMessage: '用户邮箱',
+ })}
+ placeholder="请输入用户邮箱"
+ colProps={{ md: 12, xl: 12 }}
+ rules={[
+ {
+ required: false,
+ message: (
+ <FormattedMessage id="请输入用户邮箱!" defaultMessage="请输入用户邮箱!" />
+ ),
+ },
+ ]}
+ />
+ <ProFormText
+ name="userName"
+ label={intl.formatMessage({
+ id: 'system.user.user_name',
+ defaultMessage: '用户账号',
+ })}
+ hidden={userId}
+ placeholder="请输入用户账号"
+ colProps={{ md: 12, xl: 12 }}
+ rules={[
+ {
+ required: true,
+ },
+ ]}
+ />
+ <ProFormText.Password
+ name="password"
+ label={intl.formatMessage({
+ id: 'system.user.password',
+ defaultMessage: '密码',
+ })}
+ hidden={userId}
+ placeholder="请输入密码"
+ colProps={{ md: 12, xl: 12 }}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入密码!" defaultMessage="请输入密码!" />,
+ },
+ ]}
+ />
+ <ProFormSelect
+ valueEnum={sexOptions}
+ name="sex"
+ label={intl.formatMessage({
+ id: 'system.user.sex',
+ defaultMessage: '用户性别',
+ })}
+ initialValue={'0'}
+ placeholder="请输入用户性别"
+ colProps={{ md: 12, xl: 12 }}
+ rules={[
+ {
+ required: false,
+ message: (
+ <FormattedMessage id="请输入用户性别!" defaultMessage="请输入用户性别!" />
+ ),
+ },
+ ]}
+ />
+ <ProFormRadio.Group
+ valueEnum={statusOptions}
+ name="status"
+ label={intl.formatMessage({
+ id: 'system.user.status',
+ defaultMessage: '帐号状态',
+ })}
+ initialValue={'0'}
+ placeholder="请输入帐号状态"
+ colProps={{ md: 12, xl: 12 }}
+ rules={[
+ {
+ required: false,
+ message: (
+ <FormattedMessage id="请输入帐号状态!" defaultMessage="请输入帐号状态!" />
+ ),
+ },
+ ]}
+ />
+ <ProFormSelect
+ name="postIds"
+ mode="multiple"
+ label={intl.formatMessage({
+ id: 'system.user.post',
+ defaultMessage: '岗位',
+ })}
+ options={posts}
+ placeholder="请选择岗位"
+ colProps={{ md: 12, xl: 12 }}
+ rules={[{ required: true, message: '请选择岗位!' }]}
+ />
+ <ProFormSelect
+ name="roleIds"
+ mode="multiple"
+ label={intl.formatMessage({
+ id: 'system.user.role',
+ defaultMessage: '角色',
+ })}
+ options={roles}
+ placeholder="请选择角色"
+ colProps={{ md: 12, xl: 12 }}
+ rules={[{ required: true, message: '请选择角色!' }]}
+ />
+ <ProFormTextArea
+ name="remark"
+ label={intl.formatMessage({
+ id: 'system.user.remark',
+ defaultMessage: '备注',
+ })}
+ placeholder="请输入备注"
+ colProps={{ md: 24, xl: 24 }}
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入备注!" defaultMessage="请输入备注!" />,
+ },
+ ]}
+ />
+ </ProForm>
+ </Modal>
+ );
+};
+
+export default UserForm;
diff --git a/src/pages/System/User/index.tsx b/src/pages/System/User/index.tsx
new file mode 100644
index 0000000..6435787
--- /dev/null
+++ b/src/pages/System/User/index.tsx
@@ -0,0 +1,561 @@
+
+import React, { useState, useRef, useEffect } from 'react';
+import { useIntl, FormattedMessage, useAccess } from '@umijs/max';
+import { Card, Col, Dropdown, FormInstance, Row, Space, Switch } from 'antd';
+import { Button, message, Modal } from 'antd';
+import { ActionType, FooterToolbar, PageContainer, ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, DeleteOutlined, ExclamationCircleOutlined, DownOutlined, EditOutlined } from '@ant-design/icons';
+import { getUserList, removeUser, addUser, updateUser, exportUser, getUser, changeUserStatus, updateAuthRole, resetUserPwd } from '@/services/system/user';
+import UpdateForm from './edit';
+import { getDictValueEnum } from '@/services/system/dict';
+import { DataNode } from 'antd/es/tree';
+import { getDeptTree } from '@/services/system/user';
+import DeptTree from './components/DeptTree';
+import ResetPwd from './components/ResetPwd';
+import { getPostList } from '@/services/system/post';
+import { getRoleList } from '@/services/system/role';
+import AuthRoleForm from './components/AuthRole';
+
+const { confirm } = Modal;
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2023/02/06
+ *
+ * */
+
+/**
+ * 添加节点
+ *
+ * @param fields
+ */
+const handleAdd = async (fields: API.System.User) => {
+ const hide = message.loading('正在添加');
+ try {
+ await addUser({ ...fields });
+ hide();
+ message.success('添加成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('添加失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 更新节点
+ *
+ * @param fields
+ */
+const handleUpdate = async (fields: API.System.User) => {
+ const hide = message.loading('正在配置');
+ try {
+ await updateUser(fields);
+ hide();
+ message.success('配置成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: API.System.User[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ await removeUser(selectedRows.map((row) => row.userId).join(','));
+ hide();
+ message.success('删除成功,即将刷新');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleRemoveOne = async (selectedRow: API.System.User) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRow) return true;
+ try {
+ const params = [selectedRow.userId];
+ await removeUser(params.join(','));
+ hide();
+ message.success('删除成功,即将刷新');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+/**
+ * 导出数据
+ *
+ *
+ */
+const handleExport = async () => {
+ const hide = message.loading('正在导出');
+ try {
+ await exportUser();
+ hide();
+ message.success('导出成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('导出失败,请重试');
+ return false;
+ }
+};
+
+const UserTableList: React.FC = () => {
+ const [messageApi, contextHolder] = message.useMessage();
+
+ const formTableRef = useRef<FormInstance>();
+
+ const [modalVisible, setModalVisible] = useState<boolean>(false);
+ const [resetPwdModalVisible, setResetPwdModalVisible] = useState<boolean>(false);
+ const [authRoleModalVisible, setAuthRoleModalVisible] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<API.System.User>();
+ const [selectedRows, setSelectedRows] = useState<API.System.User[]>([]);
+
+ const [selectDept, setSelectDept] = useState<any>({ id: 0 });
+ const [sexOptions, setSexOptions] = useState<any>([]);
+ const [statusOptions, setStatusOptions] = useState<any>([]);
+
+ const [postIds, setPostIds] = useState<number[]>();
+ const [postList, setPostList] = useState<any[]>();
+ const [roleIds, setRoleIds] = useState<number[]>();
+ const [roleList, setRoleList] = useState<any[]>();
+ const [deptTree, setDeptTree] = useState<DataNode[]>();
+
+ const access = useAccess();
+
+ /** 国际化配置 */
+ const intl = useIntl();
+
+ useEffect(() => {
+ getDictValueEnum('sys_user_sex').then((data) => {
+ setSexOptions(data);
+ });
+ getDictValueEnum('sys_normal_disable').then((data) => {
+ setStatusOptions(data);
+ });
+ }, []);
+
+ const showChangeStatusConfirm = (record: API.System.User) => {
+ let text = record.status === "1" ? "启用" : "停用";
+ const newStatus = record.status === '0' ? '1' : '0';
+ confirm({
+ title: `确认要${text}${record.userName}用户吗?`,
+ onOk() {
+ changeUserStatus(record.userId, newStatus).then(resp => {
+ if (resp.code === 200) {
+ messageApi.open({
+ type: 'success',
+ content: '更新成功!',
+ });
+ actionRef.current?.reload();
+ } else {
+ messageApi.open({
+ type: 'error',
+ content: '更新失败!',
+ });
+ }
+ });
+ },
+ });
+ };
+
+ const fetchUserInfo = async (userId: number) => {
+ const res = await getUser(userId);
+ setPostIds(res.postIds);
+ setPostList(
+ res.posts.map((item: any) => {
+ return {
+ value: item.postId,
+ label: item.postName,
+ };
+ }),
+ );
+ setRoleIds(res.roleIds);
+ setRoleList(
+ res.roles.map((item: any) => {
+ return {
+ value: item.roleId,
+ label: item.roleName,
+ };
+ }),
+ );
+ };
+
+ const columns: ProColumns<API.System.User>[] = [
+ {
+ title: <FormattedMessage id="system.user.user_id" defaultMessage="用户编号" />,
+ dataIndex: 'deptId',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.user.user_name" defaultMessage="用户账号" />,
+ dataIndex: 'userName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.user.nick_name" defaultMessage="用户昵称" />,
+ dataIndex: 'nickName',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.user.dept_name" defaultMessage="部门" />,
+ dataIndex: ['dept', 'deptName'],
+ valueType: 'text',
+ hideInSearch: true
+ },
+ {
+ title: <FormattedMessage id="system.user.phonenumber" defaultMessage="手机号码" />,
+ dataIndex: 'phonenumber',
+ valueType: 'text',
+ },
+ {
+ title: <FormattedMessage id="system.user.status" defaultMessage="帐号状态" />,
+ dataIndex: 'status',
+ valueType: 'select',
+ valueEnum: statusOptions,
+ render: (_, record) => {
+ return (
+ <Switch
+ checked={record.status === '0'}
+ checkedChildren="正常"
+ unCheckedChildren="停用"
+ defaultChecked
+ onClick={() => showChangeStatusConfirm(record)}
+ />)
+ },
+ },
+ {
+ title: <FormattedMessage id="pages.searchTable.titleOption" defaultMessage="操作" />,
+ dataIndex: 'option',
+ width: '220px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="edit"
+ icon=<EditOutlined />
+ hidden={!access.hasPerms('system:user:edit')}
+ onClick={async () => {
+ fetchUserInfo(record.userId);
+ const treeData = await getDeptTree({});
+ setDeptTree(treeData);
+ setModalVisible(true);
+ setCurrentRow(record);
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ danger
+ icon=<DeleteOutlined />
+ key="batchRemove"
+ hidden={!access.hasPerms('system:user:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemoveOne(record);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ <Dropdown
+ key="more"
+ menu={{
+ items: [
+ {
+ label: <FormattedMessage id="system.user.reset.password" defaultMessage="密码重置" />,
+ key: 'reset',
+ disabled: !access.hasPerms('system:user:edit'),
+ },
+ {
+ label: '分配角色',
+ key: 'authRole',
+ disabled: !access.hasPerms('system:user:edit'),
+ },
+ ],
+ onClick: ({ key }) => {
+ if (key === 'reset') {
+ setResetPwdModalVisible(true);
+ setCurrentRow(record);
+ }
+ else if (key === 'authRole') {
+ fetchUserInfo(record.userId);
+ setAuthRoleModalVisible(true);
+ setCurrentRow(record);
+ }
+ }
+ }}
+ >
+ <a onClick={(e) => e.preventDefault()}>
+ <Space>
+ <DownOutlined />
+ 更多
+ </Space>
+ </a>
+ </Dropdown>,
+ ],
+ },
+ ];
+
+ return (
+ <PageContainer>
+ {contextHolder}
+ <Row gutter={[16, 24]}>
+ <Col lg={6} md={24}>
+ <Card>
+ <DeptTree
+ onSelect={async (value: any) => {
+ setSelectDept(value);
+ if (actionRef.current) {
+ formTableRef?.current?.submit();
+ }
+ }}
+ />
+ </Card>
+ </Col>
+ <Col lg={18} md={24}>
+ <ProTable<API.System.User>
+ headerTitle={intl.formatMessage({
+ id: 'pages.searchTable.title',
+ defaultMessage: '信息',
+ })}
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="userId"
+ key="userList"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="add"
+ hidden={!access.hasPerms('system:user:add')}
+ onClick={async () => {
+ const treeData = await getDeptTree({});
+ setDeptTree(treeData);
+
+ const postResp = await getPostList()
+ if (postResp.code === 200) {
+ setPostList(
+ postResp.rows.map((item: any) => {
+ return {
+ value: item.postId,
+ label: item.postName,
+ };
+ }),
+ );
+ }
+
+ const roleResp = await getRoleList()
+ if (roleResp.code === 200) {
+ setRoleList(
+ roleResp.rows.map((item: any) => {
+ return {
+ value: item.roleId,
+ label: item.roleName,
+ };
+ }),
+ );
+ }
+ setCurrentRow(undefined);
+ setModalVisible(true);
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="pages.searchTable.new" defaultMessage="新建" />
+ </Button>,
+ <Button
+ type="primary"
+ key="remove"
+ danger
+ hidden={selectedRows?.length === 0 || !access.hasPerms('system:user:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '是否确认删除所选数据项?',
+ icon: <ExclamationCircleOutlined />,
+ content: '请谨慎操作',
+ async onOk() {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ onCancel() { },
+ });
+ }}
+ >
+ <DeleteOutlined />
+ <FormattedMessage id="pages.searchTable.delete" defaultMessage="删除" />
+ </Button>,
+ <Button
+ type="primary"
+ key="export"
+ hidden={!access.hasPerms('system:user:export')}
+ onClick={async () => {
+ handleExport();
+ }}
+ >
+ <PlusOutlined />
+ <FormattedMessage id="pages.searchTable.export" defaultMessage="导出" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getUserList({ ...params, deptId: selectDept.id } as API.System.UserListParams).then((res) => {
+ const result = {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ return result;
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ </Col>
+ </Row>
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="remove"
+ danger
+ hidden={!access.hasPerms('system:user:del')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除',
+ content: '确定删除该项吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <UpdateForm
+ onSubmit={async (values) => {
+ let success = false;
+ if (values.userId) {
+ success = await handleUpdate({ ...values } as API.System.User);
+ } else {
+ success = await handleAdd({ ...values } as API.System.User);
+ }
+ if (success) {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ }}
+ onCancel={() => {
+ setModalVisible(false);
+ setCurrentRow(undefined);
+ }}
+ open={modalVisible}
+ values={currentRow || {}}
+ sexOptions={sexOptions}
+ statusOptions={statusOptions}
+ posts={postList || []}
+ postIds={postIds || []}
+ roles={roleList || []}
+ roleIds={roleIds || []}
+ depts={deptTree || []}
+ />
+ <ResetPwd
+ onSubmit={async (values: any) => {
+ const success = await resetUserPwd(values.userId, values.password);
+ if (success) {
+ setResetPwdModalVisible(false);
+ setSelectedRows([]);
+ setCurrentRow(undefined);
+ message.success('密码重置成功。');
+ }
+ }}
+ onCancel={() => {
+ setResetPwdModalVisible(false);
+ setSelectedRows([]);
+ setCurrentRow(undefined);
+ }}
+ open={resetPwdModalVisible}
+ values={currentRow || {}}
+ />
+ <AuthRoleForm
+ onSubmit={async (values: any) => {
+ const success = await updateAuthRole(values);
+ if (success) {
+ setAuthRoleModalVisible(false);
+ setSelectedRows([]);
+ setCurrentRow(undefined);
+ message.success('配置成功。');
+ }
+ }}
+ onCancel={() => {
+ setAuthRoleModalVisible(false);
+ setSelectedRows([]);
+ setCurrentRow(undefined);
+ }}
+ open={authRoleModalVisible}
+ roles={roleList || []}
+ roleIds={roleIds || []}
+ />
+ </PageContainer>
+ );
+};
+
+export default UserTableList;
diff --git a/src/pages/Tool/Build/index.tsx b/src/pages/Tool/Build/index.tsx
new file mode 100644
index 0000000..647d729
--- /dev/null
+++ b/src/pages/Tool/Build/index.tsx
@@ -0,0 +1,8 @@
+import React from "react";
+
+const testIndex :React.FC = ()=>{
+ return (
+ <div>111</div>
+ );
+}
+export default testIndex;
diff --git a/src/pages/Tool/Gen/components/BaseInfo.tsx b/src/pages/Tool/Gen/components/BaseInfo.tsx
new file mode 100644
index 0000000..6a0466f
--- /dev/null
+++ b/src/pages/Tool/Gen/components/BaseInfo.tsx
@@ -0,0 +1,124 @@
+import { Button, Col, Form, message, Row } from 'antd';
+import React, { Fragment, useEffect } from 'react';
+import { history } from '@umijs/max';
+import styles from '../style.less';
+import { ProForm, ProFormText, ProFormTextArea } from '@ant-design/pro-components';
+
+export type BaseInfoProps = {
+ values?: any;
+ onStepSubmit?: any;
+};
+
+const BaseInfo: React.FC<BaseInfoProps> = (props) => {
+ const [form] = Form.useForm();
+ const { onStepSubmit } = props;
+
+ useEffect(() => {
+ form.resetFields();
+ form.setFieldsValue({
+ tableName: props.values.tableName,
+ });
+ });
+
+ const onValidateForm = async () => {
+ const values = await form.validateFields();
+ if (onStepSubmit) {
+ onStepSubmit('base', values);
+ }
+ };
+
+ return (
+ <Fragment>
+ <Row>
+ <Col span={24}>
+ <ProForm
+ form={form}
+ onFinish={async () => {
+ message.success('提交成功');
+ }}
+ initialValues={{
+ tableName: props.values?.tableName,
+ tableComment: props.values?.tableComment,
+ className: props.values?.className,
+ functionAuthor: props.values?.functionAuthor,
+ remark: props.values?.remark,
+ }}
+ submitter={{
+ resetButtonProps: {
+ style: { display: 'none' },
+ },
+ submitButtonProps: {
+ style: { display: 'none' },
+ },
+ }}
+ >
+ <Row>
+ <Col span={12} order={1}>
+ <ProFormText
+
+ name="tableName"
+ label="表名称"
+ rules={[
+ {
+ required: true,
+ message: '表名称不可为空。',
+ },
+ ]}
+ />
+ </Col>
+ <Col span={12} order={2}>
+ <ProFormText
+ name="tableComment"
+ label="表描述"
+ />
+ </Col>
+ </Row>
+ <Row>
+ <Col span={12} order={1}>
+ <ProFormText
+
+ name="className"
+ label="实体类名称"
+ rules={[
+ {
+ required: true,
+ message: '实体类名称不可为空。',
+ },
+ ]}
+ />
+ </Col>
+ <Col span={12} order={2}>
+ <ProFormText name="functionAuthor" label="作者" />
+ </Col>
+ </Row>
+ <Row>
+ <Col span={24}>
+ <ProFormTextArea name="remark" label="备注" />
+ </Col>
+ </Row>
+ </ProForm>
+ </Col>
+ </Row>
+ <Row justify="center">
+ <Col span={4}>
+ <Button
+ type="primary"
+ className={styles.step_buttons}
+ onClick={() => {
+ history.back();
+ }}
+ >
+ 返回
+ </Button>
+ </Col>
+ <Col span={4}>
+ <Button type="primary" onClick={onValidateForm}>
+ 下一步
+ </Button>
+ </Col>
+ </Row>
+ </Fragment>
+ );
+};
+
+export default BaseInfo;
diff --git a/src/pages/Tool/Gen/components/ColumnInfo.tsx b/src/pages/Tool/Gen/components/ColumnInfo.tsx
new file mode 100644
index 0000000..2db5de4
--- /dev/null
+++ b/src/pages/Tool/Gen/components/ColumnInfo.tsx
@@ -0,0 +1,304 @@
+import React, { Fragment, useEffect, useRef, useState } from 'react';
+import type { GenCodeType } from '../data';
+import { Button, Checkbox, Col, Row, Tag } from 'antd';
+import type { FormInstance } from 'antd';
+import { history } from '@umijs/max';
+import styles from '../style.less';
+import { EditableProTable, ProColumns } from '@ant-design/pro-components';
+
+export type ColumnInfoProps = {
+ parentType?: string;
+ data?: any[];
+ dictData?: any[];
+ onStepSubmit?: any;
+};
+
+const booleanEnum = [
+ {
+ label: 'true',
+ value: '1',
+ },
+ {
+ label: 'false',
+ value: '0',
+ },
+];
+
+const ColumnInfo: React.FC<ColumnInfoProps> = (props) => {
+ const formRef = useRef<FormInstance>();
+
+ const [dataSource, setDataSource] = useState<any[]>();
+
+ const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
+
+ const { data, dictData, onStepSubmit } = props;
+
+ const columns: ProColumns<GenCodeType>[] = [
+ {
+ title: '编号',
+ dataIndex: 'columnId',
+ editable: false,
+ width: 80,
+ },
+ {
+ title: '字段名',
+ dataIndex: 'columnName',
+ editable: false,
+ },
+ {
+ title: '字段描述',
+ dataIndex: 'columnComment',
+ hideInForm: true,
+ hideInSearch: true,
+ width: 200,
+ },
+ {
+ title: '字段类型',
+ dataIndex: 'columnType',
+ editable: false,
+ },
+ {
+ title: 'Java类型',
+ dataIndex: 'javaType',
+ valueType: 'select',
+ valueEnum: {
+ Long: {
+ text: 'Long',
+ },
+ String: {
+ text: 'String',
+ },
+ Integer: {
+ text: 'Integer',
+ },
+ Double: {
+ text: 'Double',
+ },
+ BigDecimal: {
+ text: 'BigDecimal',
+ },
+ Date: {
+ text: 'Date',
+ },
+ },
+ },
+ {
+ title: 'Java属性',
+ dataIndex: 'javaField',
+ },
+ {
+ title: '插入',
+ dataIndex: 'isInsert',
+ valueType: 'select',
+ fieldProps: {
+ options: booleanEnum,
+ },
+ render: (_, record) => {
+ return <Checkbox checked={record.isInsert === '1'} />;
+ },
+ },
+ {
+ title: '编辑',
+ dataIndex: 'isEdit',
+ valueType: 'select',
+ fieldProps: {
+ options: booleanEnum,
+ },
+ render: (_, record) => {
+ return <Checkbox checked={record.isEdit === '1'} />;
+ },
+ },
+ {
+ title: '列表',
+ dataIndex: 'isList',
+ valueType: 'select',
+ fieldProps: {
+ options: booleanEnum,
+ },
+ render: (_, record) => {
+ return <Checkbox checked={record.isList === '1'} />;
+ },
+ },
+ {
+ title: '查询',
+ dataIndex: 'isQuery',
+ valueType: 'select',
+ fieldProps: {
+ options: booleanEnum,
+ },
+ render: (_, record) => {
+ return <Checkbox checked={record.isQuery === '1'} />;
+ },
+ },
+ {
+ title: '查询方式',
+ dataIndex: 'queryType',
+ valueType: 'select',
+ valueEnum: {
+ EQ: {
+ text: '=',
+ },
+ NE: {
+ text: '!=',
+ },
+ GT: {
+ text: '>',
+ },
+ GTE: {
+ text: '>=',
+ },
+ LT: {
+ text: '<',
+ },
+ LTE: {
+ text: '<=',
+ },
+ LIKE: {
+ text: 'LIKE',
+ },
+ BETWEEN: {
+ text: 'BETWEEN',
+ },
+ },
+ },
+ {
+ title: '必填',
+ dataIndex: 'isRequired',
+ valueType: 'select',
+ fieldProps: {
+ options: booleanEnum,
+ },
+ render: (_, record) => {
+ return <Checkbox checked={record.isRequired === '1'} />;
+ },
+ },
+ {
+ title: '显示类型',
+ dataIndex: 'htmlType',
+ hideInSearch: true,
+ valueType: 'select',
+ valueEnum: {
+ input: {
+ text: '文本框',
+ },
+ textarea: {
+ text: '文本域',
+ },
+ select: {
+ text: '下拉框',
+ },
+ radio: {
+ text: '单选框',
+ },
+ checkbox: {
+ text: '复选框',
+ },
+ datetime: {
+ text: '日期控件',
+ },
+ imageUpload: {
+ text: '图片上传',
+ },
+ fileUpload: {
+ text: '文件上传',
+ },
+ editor: {
+ text: '富文本控件',
+ },
+ },
+ },
+ {
+ title: '字典类型',
+ dataIndex: 'dictType',
+ hideInSearch: true,
+ valueType: 'select',
+ fieldProps: {
+ options: dictData,
+ },
+ render: (text) => {
+ return <Tag color="#108ee9">{text}</Tag>;
+ },
+ },
+ ];
+
+ useEffect(() => {
+ setDataSource(data);
+ if (data) {
+ setEditableRowKeys(data.map((item) => item.columnId));
+ }
+ }, [data]);
+
+ const onSubmit = (direction: string) => {
+ if (onStepSubmit) {
+ onStepSubmit('column', dataSource, direction);
+ }
+ };
+
+ const onDataChange = (value: readonly GenCodeType[]) => {
+ setDataSource({ ...value } as []);
+ };
+
+ return (
+ <Fragment>
+ <Row>
+ <Col span={24}>
+ <EditableProTable<GenCodeType>
+ formRef={formRef}
+ rowKey="columnId"
+ search={false}
+ columns={columns}
+ value={dataSource}
+ editable={{
+ type: 'multiple',
+ editableKeys,
+ onChange: setEditableRowKeys,
+ actionRender: (row, config, defaultDoms) => {
+ return [defaultDoms.delete];
+ },
+ onValuesChange: (record, recordList) => {
+ setDataSource(recordList);
+ },
+ }}
+ onChange={onDataChange}
+ recordCreatorProps={false}
+ />
+ </Col>
+ </Row>
+ <Row justify="center">
+ <Col span={4}>
+ <Button
+ type="primary"
+ onClick={() => {
+ history.back();
+ }}
+ >
+ 返回
+ </Button>
+ </Col>
+ <Col span={4}>
+ <Button
+ type="primary"
+ className={styles.step_buttons}
+ onClick={() => {
+ onSubmit('prev');
+ }}
+ >
+ 上一步
+ </Button>
+ </Col>
+ <Col span={4}>
+ <Button
+ type="primary"
+ onClick={() => {
+ onSubmit('next');
+ }}
+ >
+ 下一步
+ </Button>
+ </Col>
+ </Row>
+ </Fragment>
+ );
+};
+
+export default ColumnInfo;
diff --git a/src/pages/Tool/Gen/components/GenInfo.tsx b/src/pages/Tool/Gen/components/GenInfo.tsx
new file mode 100644
index 0000000..97901ec
--- /dev/null
+++ b/src/pages/Tool/Gen/components/GenInfo.tsx
@@ -0,0 +1,330 @@
+import { Button, Col, Divider, Form, Row, TreeSelect } from 'antd';
+import React, { Fragment, useEffect, useState } from 'react';
+import { history } from '@umijs/max';
+import type { TableInfo } from '../data';
+import styles from '../style.less';
+import { DataNode } from 'antd/es/tree';
+import { ProForm, ProFormRadio, ProFormSelect, ProFormText } from '@ant-design/pro-components';
+
+export type GenInfoProps = {
+ values?: any;
+ menuData?: DataNode[];
+ tableInfo?: TableInfo[];
+ onStepSubmit?: any;
+};
+
+const GenInfo: React.FC<GenInfoProps> = (props) => {
+ const [form] = Form.useForm();
+
+ const [pathType, setPathType] = useState<string>('0');
+ const [tlpType, setTlpType] = useState<string>('curd');
+
+ const [subTablesColumnOptions, setSubTablesColumnOptions] = useState<any[]>();
+
+ const { menuData, tableInfo, onStepSubmit } = props;
+
+ const tablesOptions = tableInfo?.map((item: any) => {
+ return {
+ value: item.tableName,
+ label: `${item.tableName}:${item.tableComment}`,
+ };
+ });
+
+ if (tableInfo) {
+ for (let index = 0; index < tableInfo?.length; index += 1) {
+ const tbl = tableInfo[index];
+ if (tbl.tableName === props.values.subTableName) {
+ const opts = [];
+ tbl.columns.forEach((item) => {
+ opts.push({
+ value: item.columnName,
+ label: `${item.columnName}: ${item.columnComment}`,
+ });
+ });
+ break;
+ }
+ }
+ }
+
+ const treeColumns = props.values.columns.map((item: any) => {
+ return {
+ value: item.columnName,
+ label: `${item.columnName}: ${item.columnComment}`,
+ };
+ });
+
+ const onSubmit = async (direction: string) => {
+ const values = await form.validateFields();
+ onStepSubmit('gen', values, direction);
+ };
+
+ useEffect(() => {
+ setPathType(props.values.genType);
+ setTlpType(props.values.tplCategory);
+ }, [props.values.genType, props.values.tplCategory]);
+
+ return (
+ <Fragment>
+ <Row>
+ <Col span={24}>
+ <ProForm
+ form={form}
+ onFinish={async () => {
+ const values = await form.validateFields();
+ onStepSubmit('gen', values);
+ }}
+ initialValues={{
+ curd: props.values.curd,
+ tree: props.values.tree,
+ sub: props.values.sub,
+ tplCategory: props.values.tplCategory,
+ packageName: props.values.packageName,
+ moduleName: props.values.moduleName,
+ businessName: props.values.businessName,
+ functionName: props.values.functionName,
+ parentMenuId: props.values.parentMenuId,
+ genType: props.values.genType,
+ genPath: props.values.genPath,
+ treeCode: props.values.treeCode,
+ treeParentCode: props.values.treeParentCode,
+ treeName: props.values.treeName,
+ subTableName: props.values.subTableName,
+ subTableFkName: props.values.subTableFkName,
+ }}
+ submitter={{
+ resetButtonProps: {
+ style: { display: 'none' },
+ },
+ submitButtonProps: {
+ style: { display: 'none' },
+ },
+ }}
+ >
+ <Row gutter={[16, 16]}>
+ <Col span={12} order={1}>
+ <ProFormSelect
+ fieldProps={{
+ onChange: (val) => {
+ setTlpType(val);
+ },
+ }}
+ valueEnum={{
+ crud: '单表(增删改查)',
+ tree: '树表(增删改查)',
+ sub: '主子表(增删改查)',
+ }}
+ name="tplCategory"
+ label="生成模板"
+ rules={[
+ {
+ required: true,
+ message: '选择类型',
+ },
+ ]}
+ />
+ </Col>
+ <Col span={12} order={2}>
+ <ProFormText name="packageName" label="生成包路径" />
+ </Col>
+ </Row>
+ <Row gutter={[16, 16]}>
+ <Col span={12} order={1}>
+ <ProFormText name="moduleName" label="生成模块名" />
+ </Col>
+ <Col span={12} order={2}>
+ <ProFormText name="businessName" label="生成业务名" />
+ </Col>
+ </Row>
+ <Row gutter={[16, 16]}>
+ <Col span={12} order={1}>
+ <ProFormText name="functionName" label="生成功能名" />
+ </Col>
+ <Col span={12} order={2}>
+ <ProForm.Item
+ labelCol={{ span: 20 }}
+ name="parentMenuId"
+ label="父菜单"
+ >
+ <TreeSelect
+ style={{ width: '74%' }}
+ defaultValue={props.values.parentMenuId}
+ treeData={menuData}
+ placeholder="请选择父菜单"
+ />
+ </ProForm.Item>
+ </Col>
+ </Row>
+ <Row gutter={[16, 16]}>
+ <Col span={24}>
+ <ProFormRadio.Group
+ valueEnum={{
+ '0': 'zip压缩包',
+ '1': '自定义路径',
+ }}
+ name="genType"
+ label="生成代码方式"
+ rules={[
+ {
+ required: true,
+ message: '选择类型',
+ },
+ ]}
+ fieldProps={{
+ onChange: (e) => {
+ setPathType(e.target.value);
+ },
+ }}
+ />
+ </Col>
+ </Row>
+ <Row gutter={[16, 16]}>
+ <Col span={24} order={1}>
+ <ProFormText
+ hidden={pathType === '0'}
+ width="md"
+ name="genPath"
+ label="自定义路径"
+ />
+ </Col>
+ </Row>
+ <div hidden={tlpType !== 'tree'}>
+ <Divider orientation="left">其他信息</Divider>
+ <Row gutter={[16, 16]}>
+ <Col span={12} order={1}>
+ <ProFormSelect
+ name="treeCode"
+ label="树编码字段"
+ options={treeColumns}
+ rules={[
+ {
+ required: tlpType === 'tree',
+ message: '树编码字段',
+ },
+ ]}
+ />
+ </Col>
+ <Col span={12} order={2}>
+ <ProFormSelect
+ name="treeParentCode"
+ label="树父编码字段"
+ options={treeColumns}
+ rules={[
+ {
+ required: tlpType === 'tree',
+ message: '树父编码字段',
+ },
+ ]}
+ />
+ </Col>
+ </Row>
+ <Row gutter={[16, 16]}>
+ <Col span={12} order={1}>
+ <ProFormSelect
+ name="treeName"
+ label="树名称字段"
+ options={treeColumns}
+ rules={[
+ {
+ required: tlpType === 'tree',
+ message: '树名称字段',
+ },
+ ]}
+ />
+ </Col>
+ </Row>
+ </div>
+ <div hidden={tlpType !== 'sub'}>
+ <Divider orientation="left">关联信息</Divider>
+ <Row gutter={[16, 16]}>
+ <Col span={12} order={1}>
+ <ProFormSelect
+ name="subTableName"
+ label="关联子表的表名"
+ options={tablesOptions}
+ rules={[
+ {
+ required: tlpType === 'sub',
+ message: '关联子表的表名',
+ },
+ ]}
+ fieldProps={{
+ onChange: (val) => {
+ form.setFieldsValue({
+ subTableFkName: '',
+ });
+ if (tableInfo) {
+ for (let index = 0; index < tableInfo?.length; index += 1) {
+ const tbl = tableInfo[index];
+ if (tbl.tableName === val) {
+ const opts: any[] = [];
+ tbl.columns.forEach((item) => {
+ opts.push({
+ value: item.columnName,
+ label: `${item.columnName}:${item.columnComment}`,
+ });
+ });
+ setSubTablesColumnOptions(opts);
+ break;
+ }
+ }
+ }
+ },
+ }}
+ />
+ </Col>
+ <Col span={12} order={2}>
+ <ProFormSelect
+ name="subTableFkName"
+ options={subTablesColumnOptions}
+ label="子表关联的外键名"
+ rules={[
+ {
+ required: tlpType === 'sub',
+ message: '子表关联的外键名',
+ },
+ ]}
+ />
+ </Col>
+ </Row>
+ </div>
+ </ProForm>
+ </Col>
+ </Row>
+ <Row justify="center">
+ <Col span={4}>
+ <Button
+ type="primary"
+ onClick={() => {
+ history.back();
+ }}
+ >
+ 返回
+ </Button>
+ </Col>
+ <Col span={4}>
+ <Button
+ type="primary"
+ className={styles.step_buttons}
+ onClick={() => {
+ onSubmit('prev');
+ }}
+ >
+ 上一步
+ </Button>
+ </Col>
+ <Col span={4}>
+ <Button
+ type="primary"
+ onClick={() => {
+ onSubmit('next');
+ }}
+ >
+ 提交
+ </Button>
+ </Col>
+ </Row>
+ </Fragment>
+ );
+};
+
+export default GenInfo;
diff --git a/src/pages/Tool/Gen/components/PreviewCode.tsx b/src/pages/Tool/Gen/components/PreviewCode.tsx
new file mode 100644
index 0000000..f3611ad
--- /dev/null
+++ b/src/pages/Tool/Gen/components/PreviewCode.tsx
@@ -0,0 +1,50 @@
+import React, { useEffect } from 'react';
+import { useIntl } from '@umijs/max';
+import { Modal, Tabs } from 'antd';
+import type { TabsProps } from 'antd';
+import Highlight from 'react-highlight';
+import 'highlight.js/styles/base16/material.css';
+
+interface PreviewTableProps {
+ open: boolean;
+ data?: any;
+ onHide: () => void;
+}
+
+const PreviewTableCode: React.FC<PreviewTableProps> = (props) => {
+ const intl = useIntl();
+ const panes: any = [];
+ const keys = Object.keys(props.data);
+ keys.forEach((key) => {
+ panes.push({
+ key: key + '1',
+ label: key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm')),
+ children: <Highlight className="java">{props.data[key]}</Highlight>,
+ } as TabsProps);
+ });
+
+ useEffect(() => {}, []);
+
+ return (
+ <Modal
+ width={900}
+ title={intl.formatMessage({
+ id: 'gen.preview',
+ defaultMessage: '预览',
+ })}
+ open={props.open}
+ destroyOnClose
+ footer={false}
+ onOk={() => {
+ props.onHide();
+ }}
+ onCancel={() => {
+ props.onHide();
+ }}
+ >
+ <Tabs defaultActiveKey="1" items={panes}></Tabs>
+ </Modal>
+ );
+};
+
+export default PreviewTableCode;
diff --git a/src/pages/Tool/Gen/data.d.ts b/src/pages/Tool/Gen/data.d.ts
new file mode 100644
index 0000000..a63ea9d
--- /dev/null
+++ b/src/pages/Tool/Gen/data.d.ts
@@ -0,0 +1,106 @@
+export type GenCodeType = {
+ columnId: string;
+ businessName: string;
+ className: string;
+ createBy: string;
+ createTime: string;
+ crud: string;
+ functionAuthor: string;
+ functionName: string;
+ genPath: string;
+ genType: string;
+ moduleName: string;
+ options: string;
+ packageName: string;
+ params: string;
+ parentMenuId: string;
+ parentMenuName: string;
+ remark: string;
+ status: string;
+ isList: string;
+ isEdit: string;
+ isQuery: string;
+ isInsert: string;
+ isRequired: string;
+ tableComment: string;
+ tableId: string;
+ tableName: string;
+ tplCategory: string;
+ tree: string;
+ treeCode: number;
+ treeName: string;
+ treeParentCode: number;
+ updateBy: string;
+ updateTime: string;
+ columns: any;
+};
+
+export type GenCodePagination = {
+ total: number;
+ pageSize: number;
+ current: number;
+};
+
+export type GenCodeListData = {
+ list: GenCodeType[];
+ pagination: Partial<GenCodePagination>;
+};
+
+export type GenCodeTableListParams = {
+ businessName?: string;
+ className?: string;
+ createBy?: string;
+ createTime?: string;
+ crud?: string;
+ functionAuthor?: string;
+ functionName?: string;
+ genPath?: string;
+ genType?: string;
+ moduleName?: string;
+ isList?: string;
+ isEdit?: string;
+ isQuery?: string;
+ isInsert?: string;
+ isRequired?: string;
+ options?: string;
+ params?: any;
+ packageName?: string;
+ parentMenuId?: string;
+ columns?: any;
+ parentMenuName?: string;
+ remark?: string;
+ tableComment?: string;
+ tableId?: string;
+ tableName?: string;
+ tplCategory?: string;
+ tree?: string;
+ treeCode?: string;
+ treeName?: string;
+ treeParentCode?: string;
+ updateBy?: string;
+ updateTime?: string;
+ pageSize?: string;
+ currentPage?: string;
+ filter?: string;
+ sorter?: string;
+};
+
+export type TableColumnInfo = {
+ columnName: string;
+ columnComment: string;
+};
+
+export type TableInfo = {
+ tableName: string;
+ tableComment: string;
+ columns: TableColumnInfo[];
+};
+
+export type ImportTableListItem = {
+ tableName: string;
+ tableComment: string;
+};
+
+export type ImportTableParams = {
+ tables: string;
+};
diff --git a/src/pages/Tool/Gen/edit.tsx b/src/pages/Tool/Gen/edit.tsx
new file mode 100644
index 0000000..99a0d1a
--- /dev/null
+++ b/src/pages/Tool/Gen/edit.tsx
@@ -0,0 +1,183 @@
+import React, { useEffect, useState } from 'react';
+import BaseInfo from './components/BaseInfo';
+import { Card, Layout, message, Steps } from 'antd';
+import ColumnInfo from './components/ColumnInfo';
+import GenInfo from './components/GenInfo';
+import { getGenCode, updateData } from './service';
+import { formatTreeData } from '@/utils/tree';
+import styles from './style.less';
+import type { GenCodeType } from './data';
+import { getMenuTree } from '@/services/system/menu';
+import { getDictTypeList } from '@/services/system/dict';
+import queryString from 'query-string';
+import { useLocation } from '@umijs/max';
+
+const { Content } = Layout;
+
+export type GenCodeArgs = {
+ id: string;
+};
+
+const TableList: React.FC = () => {
+ const location = useLocation();
+ const query = queryString.parse(location.search);
+ const { id } = query as GenCodeArgs;
+ const tableId = id;
+
+ const [currentStep, setCurrentStep] = useState<number>(0);
+ const [columnData, setColumnData] = useState<any>([]);
+ const [baseInfoData, setBaseInfoData] = useState<any>([]);
+ const [genInfoData, setGenInfoData] = useState<any>([]);
+ const [menuTree, setMenuTree] = useState<any>([]);
+ const [dictData, setDictData] = useState<any>([]);
+ const [tableInfo, setTableInfo] = useState<any>([]);
+ const [formData, setFormData] = useState<any>([]);
+ const [stepComponent, setStepComponent] = useState<any>([]);
+ const [stepKey, setStepKey] = useState<string>('');
+
+ const getCurrentStepAndComponent = (key?: string) => {
+ if (key === 'base') {
+ return (
+ <BaseInfo
+ values={baseInfoData}
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ onStepSubmit={onNextStep}
+ />
+ );
+ }
+ if (key === 'column') {
+ return (
+ <ColumnInfo
+ data={columnData}
+ dictData={dictData}
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ onStepSubmit={onNextStep}
+ />
+ );
+ }
+ if (key === 'gen') {
+ return (
+ <GenInfo
+ values={genInfoData}
+ menuData={menuTree}
+ tableInfo={tableInfo}
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ onStepSubmit={onNextStep}
+ />
+ );
+ }
+ return null;
+ };
+
+ const onNextStep = (step: string, values: any, direction: string) => {
+ let stepKey = 'base';
+ if (step === 'base') {
+ setStepKey('column');
+ setCurrentStep(1);
+ setFormData(values);
+ setStepComponent(getCurrentStepAndComponent(stepKey));
+ } else if (step === 'column') {
+ if (direction === 'prev') {
+ setStepKey('base');
+ setCurrentStep(0);
+ } else {
+ setStepKey('gen');
+ const tableData: GenCodeType = formData || ({} as GenCodeType);
+ tableData.columns = values;
+ setCurrentStep(2);
+ setFormData(tableData);
+ }
+ setStepComponent(getCurrentStepAndComponent(stepKey));
+ } else if (step === 'gen') {
+ if (direction === 'prev') {
+ setStepKey('column');
+ setCurrentStep(1);
+ setStepComponent(getCurrentStepAndComponent(stepKey));
+ } else {
+ const postData: GenCodeType = {
+ ...formData,
+ ...values,
+ params: values,
+ tableId: tableId,
+ };
+ setFormData(postData);
+ updateData({ ...postData } as GenCodeType).then((res) => {
+ if (res.code === 200) {
+ message.success('提交成功');
+ history.back();
+ } else {
+ message.success('提交失败');
+ }
+ });
+ }
+ }
+ };
+ useEffect(() => {
+ setStepComponent(getCurrentStepAndComponent(stepKey));
+ }, [stepKey]);
+
+ useEffect(() => {
+ getGenCode(tableId).then((res) => {
+ if (res.code === 200) {
+ setBaseInfoData(res.data.info);
+ setColumnData(res.data.rows);
+ setGenInfoData(res.data.info);
+ setTableInfo(res.data.tables);
+ setStepKey('base');
+ } else {
+ message.error(res.msg);
+ }
+ });
+ getMenuTree().then((res) => {
+ if (res.code === 200) {
+ const treeData = formatTreeData(res.data);
+ setMenuTree(treeData);
+ } else {
+ message.error(res.msg);
+ }
+ });
+
+ getDictTypeList().then((res: any) => {
+ if (res.code === 200) {
+ const dicts = res.rows.map((item: any) => {
+ return {
+ label: item.dictName,
+ value: item.dictType,
+ };
+ });
+ setDictData(dicts);
+ } else {
+ message.error(res.msg);
+ }
+ });
+ }, []);
+
+ // const onFinish = (values: any) => {
+ // console.log('Success:', values);
+ // };
+
+ // const onFinishFailed = (errorInfo: any) => {
+ // console.log('Failed:', errorInfo);
+ // };
+
+ return (
+ <Content>
+ <Card className={styles.tabsCard} bordered={false}>
+ <Steps current={currentStep} className={styles.steps} items={[
+ {
+ title: '基本信息',
+ },
+ {
+ title: '字段信息',
+ },
+ {
+ title: '生成信息',
+ },
+ ]} />
+ {stepComponent}
+ </Card>
+ </Content>
+ );
+};
+
+export default TableList;
diff --git a/src/pages/Tool/Gen/import.tsx b/src/pages/Tool/Gen/import.tsx
new file mode 100644
index 0000000..6e2c8df
--- /dev/null
+++ b/src/pages/Tool/Gen/import.tsx
@@ -0,0 +1,107 @@
+import { Button, Card, message, Layout } from 'antd';
+import React, { useState } from 'react';
+import { history, FormattedMessage } from '@umijs/max';
+import { importTables, queryTableList } from './service';
+import type { GenCodeType } from './data.d';
+import { ProColumns, ProTable } from '@ant-design/pro-components';
+import { PlusOutlined, RollbackOutlined } from '@ant-design/icons';
+
+const { Content } = Layout;
+
+const handleImport = async (tables: string) => {
+ const hide = message.loading('正在配置');
+ try {
+ await importTables(tables);
+ hide();
+ message.success('配置成功');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('配置失败请重试!');
+ return false;
+ }
+};
+
+const ImportTableList: React.FC = () => {
+ const [selectTables, setSelectTables] = useState<string[]>([]);
+
+ const columns: ProColumns<GenCodeType>[] = [
+ {
+ title: '表名称',
+ dataIndex: 'tableName',
+ },
+ {
+ title: '表描述',
+ dataIndex: 'tableComment',
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'createTime',
+ valueType: 'textarea',
+ hideInSearch: true,
+ },
+ ];
+
+ return (
+ <Content>
+ <Card bordered={false}>
+ <ProTable<GenCodeType>
+ headerTitle="代码生成信息"
+ rowKey="tableName"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="primary"
+ onClick={async () => {
+ if (selectTables.length < 1) {
+ message.error('请选择要导入的表!');
+ return;
+ }
+ const success = await handleImport(selectTables.join(','));
+ if (success) {
+ history.back();
+ }
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="gen.submit" defaultMessage="提交" />
+ </Button>,
+ <Button
+ type="primary"
+ key="goback"
+ onClick={() => {
+ history.back();
+ }}
+ >
+ <RollbackOutlined /> <FormattedMessage id="gen.goback" defaultMessage="返回" />
+ </Button>,
+ ]}
+ request={(params) =>
+ queryTableList({ ...params }).then((res) => {
+ return {
+ data: res.rows,
+ total: res.total,
+ success: true,
+ };
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ if (selectedRows && selectedRows.length > 0) {
+ const tables = selectedRows.map((row) => {
+ return row.tableName;
+ });
+ setSelectTables(tables);
+ }
+ },
+ }}
+ />
+ </Card>
+ </Content>
+ );
+};
+
+export default ImportTableList;
diff --git a/src/pages/Tool/Gen/index.tsx b/src/pages/Tool/Gen/index.tsx
new file mode 100644
index 0000000..9db00d7
--- /dev/null
+++ b/src/pages/Tool/Gen/index.tsx
@@ -0,0 +1,360 @@
+import { DownloadOutlined, PlusOutlined } from '@ant-design/icons';
+import { Button, message, Drawer, Modal, Card, Layout } from 'antd';
+import type { FormInstance } from 'antd';
+import React, { useState, useRef } from 'react';
+import { history, FormattedMessage, useAccess } from '@umijs/max';
+import PreviewForm from './components/PreviewCode';
+import type { GenCodeTableListParams, GenCodeType } from './data.d';
+import {
+ batchGenCode,
+ genCode,
+ previewCode,
+ getGenCodeList,
+ removeData,
+ syncDbInfo,
+} from './service';
+import {
+ ActionType,
+ FooterToolbar,
+ ProColumns,
+ ProDescriptions,
+ ProDescriptionsItemProps,
+ ProTable,
+} from '@ant-design/pro-components';
+
+const { Content } = Layout;
+
+/**
+ * 删除节点
+ *
+ * @param selectedRows
+ */
+const handleRemove = async (selectedRows: GenCodeType[]) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRows) return true;
+ try {
+ await removeData({
+ ids: selectedRows.map((row) => row.tableId),
+ });
+ hide();
+ message.success('删除成功,即将刷新');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const handleRemoveOne = async (selectedRow: GenCodeType) => {
+ const hide = message.loading('正在删除');
+ if (!selectedRow) return true;
+ try {
+ const params = [selectedRow.tableId];
+ await removeData({
+ ids: params,
+ });
+ hide();
+ message.success('删除成功,即将刷新');
+ return true;
+ } catch (error) {
+ hide();
+ message.error('删除失败,请重试');
+ return false;
+ }
+};
+
+const GenCodeView: React.FC = () => {
+ const formTableRef = useRef<FormInstance>();
+
+ const [showDetail, setShowDetail] = useState<boolean>(false);
+ const [showPreview, setShowPreview] = useState<boolean>(false);
+ const [preivewData, setPreivewData] = useState<boolean>(false);
+
+ const actionRef = useRef<ActionType>();
+ const [currentRow, setCurrentRow] = useState<GenCodeType>();
+ const [selectedRows, setSelectedRows] = useState<GenCodeType[]>([]);
+
+ const access = useAccess();
+
+ const columns: ProColumns<GenCodeType>[] = [
+ {
+ title: '编号',
+ dataIndex: 'tableId',
+ tip: '编号',
+ render: (dom, entity) => {
+ return (
+ <a
+ onClick={() => {
+ setCurrentRow(entity);
+ setShowDetail(true);
+ }}
+ >
+ {dom}
+ </a>
+ );
+ },
+ },
+ {
+ title: '表名',
+ dataIndex: 'tableName',
+ valueType: 'textarea',
+ },
+ {
+ title: '表描述',
+ dataIndex: 'tableComment',
+ hideInForm: true,
+ hideInSearch: true,
+ },
+ {
+ title: '实体',
+ dataIndex: 'className',
+ valueType: 'textarea',
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'createTime',
+ valueType: 'textarea',
+ hideInSearch: true,
+ },
+ {
+ title: '更新时间',
+ dataIndex: 'updateTime',
+ valueType: 'textarea',
+ hideInSearch: true,
+ },
+ {
+ title: '操作',
+ dataIndex: 'option',
+ width: '220px',
+ valueType: 'option',
+ render: (_, record) => [
+ <Button
+ type="link"
+ size="small"
+ key="preview"
+ hidden={!access.hasPerms('tool:gen:edit')}
+ onClick={() => {
+ previewCode(record.tableId).then((res) => {
+ if (res.code === 200) {
+ setPreivewData(res.data);
+ setShowPreview(true);
+ } else {
+ message.error('获取数据失败');
+ }
+ });
+ }}
+ >
+ 预览
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ key="config"
+ hidden={!access.hasPerms('tool:gen:edit')}
+ onClick={() => {
+ history.push(`/tool/gen/edit?id=${record.tableId}`);
+ }}
+ >
+ 编辑
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ danger
+ key="delete"
+ hidden={!access.hasPerms('tool:gen:del')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除任务',
+ content: '确定删除该任务吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemoveOne(record);
+ if (success) {
+ if (actionRef.current) {
+ actionRef.current.reload();
+ }
+ }
+ },
+ });
+ }}
+ >
+ 删除
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ key="sync"
+ hidden={!access.hasPerms('tool:gen:edit')}
+ onClick={() => {
+ syncDbInfo(record.tableName).then((res) => {
+ if (res.code === 200) {
+ message.success('同步成功');
+ } else {
+ message.error('同步失败');
+ }
+ });
+ }}
+ >
+ 同步
+ </Button>,
+ <Button
+ type="link"
+ size="small"
+ key="gencode"
+ hidden={!access.hasPerms('tool:gen:edit')}
+ onClick={() => {
+ if (record.genType === '1') {
+ genCode(record.tableName).then((res) => {
+ if (res.code === 200) {
+ message.success(`成功生成到自定义路径:${record.genPath}`);
+ } else {
+ message.error(res.msg);
+ }
+ });
+ } else {
+ batchGenCode(record.tableName);
+ }
+ }}
+ >
+ 生成代码
+ </Button>,
+ ],
+ },
+ ];
+
+ return (
+ <Content>
+ <Card bordered={false}>
+ <ProTable<GenCodeType>
+ headerTitle="代码生成信息"
+ actionRef={actionRef}
+ formRef={formTableRef}
+ rowKey="tableId"
+ search={{
+ labelWidth: 120,
+ }}
+ toolBarRender={() => [
+ <Button
+ type="primary"
+ key="gen"
+ hidden={!access.hasPerms('tool:gen:edit')}
+ onClick={() => {
+ if (selectedRows.length === 0) {
+ message.error('请选择要生成的数据');
+ return;
+ }
+ const tableNames = selectedRows.map((row) => row.tableName);
+ if (selectedRows[0].genType === '1') {
+ genCode(tableNames.join(',')).then((res) => {
+ if (res.code === 200) {
+ message.success(`成功生成到自定义路径:${selectedRows[0].genPath}`);
+ } else {
+ message.error(res.msg);
+ }
+ });
+ } else {
+ batchGenCode(tableNames.join(','));
+ }
+ }}
+ >
+ <DownloadOutlined /> <FormattedMessage id="gen.gencode" defaultMessage="生成" />
+ </Button>,
+ <Button
+ type="primary"
+ key="import"
+ hidden={!access.hasPerms('tool:gen:add')}
+ onClick={() => {
+ history.push('/tool/gen/import');
+ }}
+ >
+ <PlusOutlined /> <FormattedMessage id="gen.import" defaultMessage="导入" />
+ </Button>,
+ ]}
+ request={(params) =>
+ getGenCodeList({ ...params } as GenCodeTableListParams).then((res) => {
+ return {
+ data: res.rows,
+ total: res.rows.length,
+ success: true,
+ };
+ })
+ }
+ columns={columns}
+ rowSelection={{
+ onChange: (_, selectedRows) => {
+ setSelectedRows(selectedRows);
+ },
+ }}
+ />
+ {selectedRows?.length > 0 && (
+ <FooterToolbar
+ extra={
+ <div>
+ <FormattedMessage id="pages.searchTable.chosen" defaultMessage="已选择" />{' '}
+ <a style={{ fontWeight: 600 }}>{selectedRows.length}</a>{' '}
+ <FormattedMessage id="pages.searchTable.item" defaultMessage="项" />
+ </div>
+ }
+ >
+ <Button
+ key="delete"
+ hidden={!access.hasPerms('tool:gen:remove')}
+ onClick={async () => {
+ Modal.confirm({
+ title: '删除任务',
+ content: '确定删除该任务吗?',
+ okText: '确认',
+ cancelText: '取消',
+ onOk: async () => {
+ const success = await handleRemove(selectedRows);
+ if (success) {
+ setSelectedRows([]);
+ actionRef.current?.reloadAndRest?.();
+ }
+ },
+ });
+ }}
+ >
+ <FormattedMessage id="pages.searchTable.batchDeletion" defaultMessage="批量删除" />
+ </Button>
+ </FooterToolbar>
+ )}
+ <PreviewForm
+ open={showPreview}
+ data={preivewData}
+ onHide={() => {
+ setShowPreview(false);
+ }}
+ />
+ <Drawer
+ width={600}
+ open={showDetail}
+ onClose={() => {
+ setCurrentRow(undefined);
+ setShowDetail(false);
+ }}
+ closable={false}
+ >
+ {currentRow?.tableName && (
+ <ProDescriptions<GenCodeType>
+ column={2}
+ title={currentRow?.tableName}
+ request={async () => ({
+ data: currentRow || {},
+ })}
+ params={{
+ id: currentRow?.tableName,
+ }}
+ columns={columns as ProDescriptionsItemProps<GenCodeType>[]}
+ />
+ )}
+ </Drawer>
+ </Card>
+ </Content>
+ );
+};
+
+export default GenCodeView;
diff --git a/src/pages/Tool/Gen/locales/zh-CN.ts b/src/pages/Tool/Gen/locales/zh-CN.ts
new file mode 100644
index 0000000..9c3a5d4
--- /dev/null
+++ b/src/pages/Tool/Gen/locales/zh-CN.ts
@@ -0,0 +1,8 @@
+export default {
+ 'gen.import': '导入',
+ 'gen.title': '表信息',
+ 'gen.goback': '返回',
+ 'gen.submit': '提交',
+ 'gen.gencode': '生成',
+ 'gen.preview': '预览',
+};
diff --git a/src/pages/Tool/Gen/service.ts b/src/pages/Tool/Gen/service.ts
new file mode 100644
index 0000000..0e472ee
--- /dev/null
+++ b/src/pages/Tool/Gen/service.ts
@@ -0,0 +1,106 @@
+import { request } from '@umijs/max';
+import { downLoadZip } from '@/utils/downloadfile';
+import type { GenCodeType, GenCodeTableListParams } from './data.d';
+
+// 查询分页列表
+export async function getGenCodeList(params?: GenCodeTableListParams) {
+ const queryString = new URLSearchParams(params).toString();
+ return request(`/api/code/gen/list?${queryString}`, {
+ data: params,
+ method: 'get',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ });
+}
+
+// 查询表信息
+export async function getGenCode(id?: string) {
+ return request(`/api/code/gen/${id}`, {
+ method: 'get',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ });
+}
+
+// 查询数据表信息
+export async function queryTableList(params?: any) {
+ const queryString = new URLSearchParams(params).toString();
+ return request(`/api/code/gen/db/list?${queryString}`, {
+ data: params,
+ method: 'get',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ });
+}
+
+// 导入数据表信息
+export async function importTables(tables?: string) {
+ return request(`/api/code/gen/importTable?tables=${tables}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ });
+}
+
+// 删除
+export async function removeData(params: { ids: string[] }) {
+ return request(`/api/code/gen/${params.ids}`, {
+ method: 'delete',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ });
+}
+
+// 添加数据
+export async function addData(params: GenCodeType) {
+ return request('/api/code/gen', {
+ method: 'POST',
+ data: {
+ ...params,
+ },
+ });
+}
+
+// 更新数据
+export async function updateData(params: GenCodeType) {
+ return request('/api/code/gen', {
+ method: 'PUT',
+ data: {
+ ...params,
+ },
+ });
+}
+
+// 更新状态
+export async function syncDbInfo(tableName: string) {
+ return request(`/api/code/gen/synchDb/${tableName}`, {
+ method: 'GET',
+ });
+}
+
+// 生成代码(自定义路径)
+export async function genCode(tableName: string) {
+ return request(`/api/code/gen/genCode/${tableName}`, {
+ method: 'GET',
+ });
+}
+
+// 生成代码(压缩包)
+export async function batchGenCode(tableName: string) {
+ return downLoadZip(`/api/code/gen/batchGenCode?tables=${tableName}`);
+}
+
+// 预览
+export async function previewCode(id: string) {
+ return request(`/api/code/gen/preview/${id}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json;charset=UTF-8',
+ },
+ });
+}
diff --git a/src/pages/Tool/Gen/style.less b/src/pages/Tool/Gen/style.less
new file mode 100644
index 0000000..6999d7a
--- /dev/null
+++ b/src/pages/Tool/Gen/style.less
@@ -0,0 +1,4 @@
+.steps:global(.ant-steps) {
+ max-width: 750px;
+ margin: 16px auto;
+}
diff --git a/src/pages/Tool/Swagger/index.tsx b/src/pages/Tool/Swagger/index.tsx
new file mode 100644
index 0000000..adcc968
--- /dev/null
+++ b/src/pages/Tool/Swagger/index.tsx
@@ -0,0 +1,31 @@
+import React, { useEffect } from 'react';
+
+/**
+ *
+ * @author whiteshader@163.com
+ *
+ * */
+
+const CacheInfo: React.FC = () => {
+ useEffect(() => {
+ const frame = document.getElementById('bdIframe');
+ if (frame) {
+ const deviceWidth = document.documentElement.clientWidth;
+ const deviceHeight = document.documentElement.clientHeight;
+ frame.style.width = `${Number(deviceWidth) - 260}px`;
+ frame.style.height = `${Number(deviceHeight) - 120}px`;
+ }
+ });
+
+ return (
+ <div style={{}}>
+ <iframe
+ style={{ width: '100%', border: '0px', height: '100%' }}
+ src={`/api/swagger-ui/index.html`}
+ id="bdIframe"
+ />
+ </div>
+ );
+};
+
+export default CacheInfo;
diff --git a/src/pages/User/Center/Center.less b/src/pages/User/Center/Center.less
new file mode 100644
index 0000000..430d88e
--- /dev/null
+++ b/src/pages/User/Center/Center.less
@@ -0,0 +1,61 @@
+
+.avatarHolder {
+ margin-bottom: 16px;
+ text-align: center;
+ position: relative;
+ display: inline-block;
+ height: 120px;
+
+ & > img {
+ width: 120px;
+ height: 120px;
+ margin-bottom: 20px;
+ border-radius: 50%;
+ }
+ &:hover:after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ color: #eee;
+ font-size: 24px;
+ font-style: normal;
+ line-height: 110px;
+ background: rgba(0, 0, 0, 0.5);
+ border-radius: 50%;
+ cursor: pointer;
+ content: '+';
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+}
+
+.teamTitle {
+ margin-bottom: 12px;
+ color: @heading-color;
+ font-weight: 500;
+}
+
+.team {
+ :global {
+ .ant-avatar {
+ margin-right: 12px;
+ }
+ }
+
+ a {
+ display: block;
+ margin-bottom: 24px;
+ overflow: hidden;
+ color: @text-color;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ word-break: break-all;
+ transition: color 0.3s;
+
+ &:hover {
+ color: @primary-color;
+ }
+ }
+}
diff --git a/src/pages/User/Center/components/AvatarCropper/cropper.css b/src/pages/User/Center/components/AvatarCropper/cropper.css
new file mode 100644
index 0000000..7f2f350
--- /dev/null
+++ b/src/pages/User/Center/components/AvatarCropper/cropper.css
@@ -0,0 +1,309 @@
+/*!
+ * Cropper.js v1.5.13
+ * https://fengyuanchen.github.io/cropperjs
+ *
+ * Copyright 2015-present Chen Fengyuan
+ * Released under the MIT license
+ *
+ * Date: 2022-11-20T05:30:43.444Z
+ */
+
+.cropper-container {
+ direction: ltr;
+ font-size: 0;
+ line-height: 0;
+ position: relative;
+ -ms-touch-action: none;
+ touch-action: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.cropper-container img {
+ -webkit-backface-visibility: hidden;
+ backface-visibility: hidden;
+ display: block;
+ height: 100%;
+ image-orientation: 0deg;
+ max-height: none !important;
+ max-width: none !important;
+ min-height: 0 !important;
+ min-width: 0 !important;
+ width: 100%;
+ }
+
+.cropper-wrap-box,
+.cropper-canvas,
+.cropper-drag-box,
+.cropper-crop-box,
+.cropper-modal {
+ bottom: 0;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.cropper-wrap-box,
+.cropper-canvas {
+ overflow: hidden;
+}
+
+.cropper-drag-box {
+ background-color: #fff;
+ opacity: 0;
+}
+
+.cropper-modal {
+ background-color: #000;
+ opacity: 0.5;
+}
+
+.cropper-view-box {
+ display: block;
+ height: 100%;
+ outline: 1px solid #39f;
+ outline-color: rgba(51, 153, 255, 75%);
+ overflow: hidden;
+ width: 100%;
+}
+
+.cropper-dashed {
+ border: 0 dashed #eee;
+ display: block;
+ opacity: 0.5;
+ position: absolute;
+}
+
+.cropper-dashed.dashed-h {
+ border-bottom-width: 1px;
+ border-top-width: 1px;
+ height: calc(100% / 3);
+ left: 0;
+ top: calc(100% / 3);
+ width: 100%;
+ }
+
+.cropper-dashed.dashed-v {
+ border-left-width: 1px;
+ border-right-width: 1px;
+ height: 100%;
+ left: calc(100% / 3);
+ top: 0;
+ width: calc(100% / 3);
+ }
+
+.cropper-center {
+ display: block;
+ height: 0;
+ left: 50%;
+ opacity: 0.75;
+ position: absolute;
+ top: 50%;
+ width: 0;
+}
+
+.cropper-center::before,
+ .cropper-center::after {
+ background-color: #eee;
+ content: " ";
+ display: block;
+ position: absolute;
+ }
+
+.cropper-center::before {
+ height: 1px;
+ left: -3px;
+ top: 0;
+ width: 7px;
+ }
+
+.cropper-center::after {
+ height: 7px;
+ left: 0;
+ top: -3px;
+ width: 1px;
+ }
+
+.cropper-face,
+.cropper-line,
+.cropper-point {
+ display: block;
+ height: 100%;
+ opacity: 0.1;
+ position: absolute;
+ width: 100%;
+}
+
+.cropper-face {
+ background-color: #fff;
+ left: 0;
+ top: 0;
+}
+
+.cropper-line {
+ background-color: #39f;
+}
+
+.cropper-line.line-e {
+ cursor: ew-resize;
+ right: -3px;
+ top: 0;
+ width: 5px;
+ }
+
+.cropper-line.line-n {
+ cursor: ns-resize;
+ height: 5px;
+ left: 0;
+ top: -3px;
+ }
+
+.cropper-line.line-w {
+ cursor: ew-resize;
+ left: -3px;
+ top: 0;
+ width: 5px;
+ }
+
+.cropper-line.line-s {
+ bottom: -3px;
+ cursor: ns-resize;
+ height: 5px;
+ left: 0;
+ }
+
+.cropper-point {
+ background-color: #39f;
+ height: 5px;
+ opacity: 0.75;
+ width: 5px;
+}
+
+.cropper-point.point-e {
+ cursor: ew-resize;
+ margin-top: -3px;
+ right: -3px;
+ top: 50%;
+ }
+
+.cropper-point.point-n {
+ cursor: ns-resize;
+ left: 50%;
+ margin-left: -3px;
+ top: -3px;
+ }
+
+.cropper-point.point-w {
+ cursor: ew-resize;
+ left: -3px;
+ margin-top: -3px;
+ top: 50%;
+ }
+
+.cropper-point.point-s {
+ bottom: -3px;
+ cursor: s-resize;
+ left: 50%;
+ margin-left: -3px;
+ }
+
+.cropper-point.point-ne {
+ cursor: nesw-resize;
+ right: -3px;
+ top: -3px;
+ }
+
+.cropper-point.point-nw {
+ cursor: nwse-resize;
+ left: -3px;
+ top: -3px;
+ }
+
+.cropper-point.point-sw {
+ bottom: -3px;
+ cursor: nesw-resize;
+ left: -3px;
+ }
+
+.cropper-point.point-se {
+ bottom: -3px;
+ cursor: nwse-resize;
+ height: 20px;
+ opacity: 1;
+ right: -3px;
+ width: 20px;
+ }
+
+@media (min-width: 768px) {
+
+.cropper-point.point-se {
+ height: 15px;
+ width: 15px;
+ }
+ }
+
+@media (min-width: 992px) {
+
+.cropper-point.point-se {
+ height: 10px;
+ width: 10px;
+ }
+ }
+
+@media (min-width: 1200px) {
+
+.cropper-point.point-se {
+ height: 5px;
+ opacity: 0.75;
+ width: 5px;
+ }
+ }
+
+.cropper-point.point-se::before {
+ background-color: #39f;
+ bottom: -50%;
+ content: " ";
+ display: block;
+ height: 200%;
+ opacity: 0;
+ position: absolute;
+ right: -50%;
+ width: 200%;
+ }
+
+.cropper-invisible {
+ opacity: 0;
+}
+
+.cropper-bg {
+ background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC");
+}
+
+.cropper-hide {
+ display: block;
+ height: 0;
+ position: absolute;
+ width: 0;
+}
+
+.cropper-hidden {
+ display: none !important;
+}
+
+.cropper-move {
+ cursor: move;
+}
+
+.cropper-crop {
+ cursor: crosshair;
+}
+
+.cropper-disabled .cropper-drag-box,
+.cropper-disabled .cropper-face,
+.cropper-disabled .cropper-line,
+.cropper-disabled .cropper-point {
+ cursor: not-allowed;
+}
diff --git a/src/pages/User/Center/components/AvatarCropper/images/bg.png b/src/pages/User/Center/components/AvatarCropper/images/bg.png
new file mode 100644
index 0000000..3c7056b
--- /dev/null
+++ b/src/pages/User/Center/components/AvatarCropper/images/bg.png
Binary files differ
diff --git a/src/pages/User/Center/components/AvatarCropper/index.less b/src/pages/User/Center/components/AvatarCropper/index.less
new file mode 100644
index 0000000..dc3fecf
--- /dev/null
+++ b/src/pages/User/Center/components/AvatarCropper/index.less
@@ -0,0 +1,10 @@
+.avatarPreview {
+ position: absolute;
+ top: 50%;
+ transform: translate(50%, -50%);
+ width: 200px;
+ height: 200px;
+ border-radius: 50%;
+ box-shadow: 0 0 4px #ccc;
+ overflow: hidden;
+}
\ No newline at end of file
diff --git a/src/pages/User/Center/components/AvatarCropper/index.tsx b/src/pages/User/Center/components/AvatarCropper/index.tsx
new file mode 100644
index 0000000..83d0bcf
--- /dev/null
+++ b/src/pages/User/Center/components/AvatarCropper/index.tsx
@@ -0,0 +1,144 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { Modal, Row, Col, Button, Space, Upload, message } from 'antd';
+import { useIntl } from '@umijs/max';
+import { uploadAvatar } from '@/services/system/user';
+import { Cropper } from 'react-cropper';
+import './cropper.css';
+import styles from './index.less';
+import {
+ MinusOutlined,
+ PlusOutlined,
+ RedoOutlined,
+ UndoOutlined,
+ UploadOutlined,
+} from '@ant-design/icons';
+
+/* *
+ *
+ * @author whiteshader@163.com
+ * @datetime 2022/02/24
+ *
+ * */
+
+export type AvatarCropperProps = {
+ onFinished: (isSuccess: boolean) => void;
+ open: boolean;
+ data: any;
+};
+
+const AvatarCropperForm: React.FC<AvatarCropperProps> = (props) => {
+ const cropperRef = useRef<HTMLImageElement>(null);
+ const [avatarData, setAvatarData] = useState<any>();
+ const [previewData, setPreviewData] = useState();
+
+ useEffect(() => {
+ setAvatarData(props.data);
+ }, [props]);
+
+ const intl = useIntl();
+ const handleOk = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.getCroppedCanvas().toBlob((blob: Blob) => {
+ const formData = new FormData();
+ formData.append('avatarfile', blob);
+ uploadAvatar(formData).then((res) => {
+ if (res.code === 200) {
+ message.success(res.msg);
+ props.onFinished(true);
+ } else {
+ message.warning(res.msg);
+ }
+ });
+ }, 'image/png');
+ };
+ const handleCancel = () => {
+ props.onFinished(false);
+ };
+ const onCrop = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ setPreviewData(cropper.getCroppedCanvas().toDataURL());
+ };
+ const onRotateRight = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.rotate(90);
+ };
+ const onRotateLeft = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.rotate(-90);
+ };
+ const onZoomIn = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.zoom(0.1);
+ };
+ const onZoomOut = () => {
+ const imageElement: any = cropperRef?.current;
+ const cropper: any = imageElement?.cropper;
+ cropper.zoom(-0.1);
+ };
+ const beforeUpload = (file: any) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = () => {
+ setAvatarData(reader.result);
+ };
+ };
+ return (
+ <Modal
+ width={800}
+ title={intl.formatMessage({
+ id: 'system.user.modify_avatar',
+ defaultMessage: '修改头像',
+ })}
+ open={props.open}
+ destroyOnClose
+ onOk={handleOk}
+ onCancel={handleCancel}
+ >
+ <Row gutter={[16, 16]}>
+ <Col span={12} order={1}>
+ <Cropper
+ ref={cropperRef}
+ src={avatarData}
+ style={{ height: 350, width: '100%', marginBottom: '16px' }}
+ initialAspectRatio={1}
+ guides={false}
+ crop={onCrop}
+ zoomable={true}
+ zoomOnWheel={true}
+ rotatable={true}
+ />
+ </Col>
+ <Col span={12} order={2}>
+ <div className={styles.avatarPreview}>
+ <img src={previewData} style={{ height: '100%', width: '100%' }} />
+ </div>
+ </Col>
+ </Row>
+ <Row gutter={[16, 16]}>
+ <Col span={6}>
+ <Upload beforeUpload={beforeUpload} maxCount={1}>
+ <Button>
+ <UploadOutlined />
+ 上传
+ </Button>
+ </Upload>
+ </Col>
+ <Col>
+ <Space>
+ <Button icon={<RedoOutlined />} onClick={onRotateRight} />
+ <Button icon={<UndoOutlined />} onClick={onRotateLeft} />
+ <Button icon={<PlusOutlined />} onClick={onZoomIn} />
+ <Button icon={<MinusOutlined />} onClick={onZoomOut} />
+ </Space>
+ </Col>
+ </Row>
+ </Modal>
+ );
+};
+
+export default AvatarCropperForm;
diff --git a/src/pages/User/Center/components/BaseInfo/index.tsx b/src/pages/User/Center/components/BaseInfo/index.tsx
new file mode 100644
index 0000000..5cdca5f
--- /dev/null
+++ b/src/pages/User/Center/components/BaseInfo/index.tsx
@@ -0,0 +1,119 @@
+import React from 'react';
+import { Form, message, Row } from 'antd';
+import { FormattedMessage, useIntl } from '@umijs/max';
+import { ProForm, ProFormRadio, ProFormText } from '@ant-design/pro-components';
+import { updateUserProfile } from '@/services/system/user';
+
+
+export type BaseInfoProps = {
+ values: Partial<API.CurrentUser> | undefined;
+};
+
+const BaseInfo: React.FC<BaseInfoProps> = (props) => {
+ const [form] = Form.useForm();
+ const intl = useIntl();
+
+ const handleFinish = async (values: Record<string, any>) => {
+ const data = { ...props.values, ...values } as API.CurrentUser;
+ const resp = await updateUserProfile(data);
+ if (resp.code === 200) {
+ message.success('修改成功');
+ } else {
+ message.warning(resp.msg);
+ }
+ };
+
+ return (
+ <>
+ <ProForm form={form} onFinish={handleFinish} initialValues={props.values}>
+ <Row>
+ <ProFormText
+ name="nickName"
+ label={intl.formatMessage({
+ id: 'system.user.nick_name',
+ defaultMessage: '用户昵称',
+ })}
+ width="xl"
+ placeholder="请输入用户昵称"
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage id="请输入用户昵称!" defaultMessage="请输入用户昵称!" />
+ ),
+ },
+ ]}
+ />
+ </Row>
+ <Row>
+ <ProFormText
+ name="phonenumber"
+ label={intl.formatMessage({
+ id: 'system.user.phonenumber',
+ defaultMessage: '手机号码',
+ })}
+ width="xl"
+ placeholder="请输入手机号码"
+ rules={[
+ {
+ required: false,
+ message: (
+ <FormattedMessage id="请输入手机号码!" defaultMessage="请输入手机号码!" />
+ ),
+ },
+ ]}
+ />
+ </Row>
+ <Row>
+ <ProFormText
+ name="email"
+ label={intl.formatMessage({
+ id: 'system.user.email',
+ defaultMessage: '邮箱',
+ })}
+ width="xl"
+ placeholder="请输入邮箱"
+ rules={[
+ {
+ type: 'email',
+ message: '无效的邮箱地址!',
+ },
+ {
+ required: false,
+ message: <FormattedMessage id="请输入邮箱!" defaultMessage="请输入邮箱!" />,
+ },
+ ]}
+ />
+ </Row>
+ <Row>
+ <ProFormRadio.Group
+ options={[
+ {
+ label: '男',
+ value: '0',
+ },
+ {
+ label: '女',
+ value: '1',
+ },
+ ]}
+ name="sex"
+ label={intl.formatMessage({
+ id: 'system.user.sex',
+ defaultMessage: 'sex',
+ })}
+ width="xl"
+ rules={[
+ {
+ required: false,
+ message: <FormattedMessage id="请输入性别!" defaultMessage="请输入性别!" />,
+ },
+ ]}
+ />
+ </Row>
+ </ProForm>
+ </>
+ );
+};
+
+export default BaseInfo;
diff --git a/src/pages/User/Center/components/ResetPassword/index.tsx b/src/pages/User/Center/components/ResetPassword/index.tsx
new file mode 100644
index 0000000..26825d4
--- /dev/null
+++ b/src/pages/User/Center/components/ResetPassword/index.tsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { Form, message } from 'antd';
+import { FormattedMessage, useIntl } from '@umijs/max';
+import { updateUserPwd } from '@/services/system/user';
+import { ProForm, ProFormText } from '@ant-design/pro-components';
+
+const ResetPassword: React.FC = () => {
+ const [form] = Form.useForm();
+ const intl = useIntl();
+
+ const handleFinish = async (values: Record<string, any>) => {
+ const resp = await updateUserPwd(values.oldPassword, values.newPassword);
+ if (resp.code === 200) {
+ message.success('密码重置成功。');
+ } else {
+ message.warning(resp.msg);
+ }
+ };
+
+ const checkPassword = (rule: any, value: string) => {
+ const login_password = form.getFieldValue('newPassword');
+ if (value === login_password) {
+ return Promise.resolve();
+ }
+ return Promise.reject(new Error('两次密码输入不一致'));
+ };
+
+ return (
+ <>
+ <ProForm form={form} onFinish={handleFinish}>
+ <ProFormText.Password
+ name="oldPassword"
+ label={intl.formatMessage({
+ id: 'system.user.old_password',
+ defaultMessage: '旧密码',
+ })}
+ width="xl"
+ placeholder="请输入旧密码"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入旧密码!" defaultMessage="请输入旧密码!" />,
+ },
+ ]}
+ />
+ <ProFormText.Password
+ name="newPassword"
+ label={intl.formatMessage({
+ id: 'system.user.new_password',
+ defaultMessage: '新密码',
+ })}
+ width="xl"
+ placeholder="请输入新密码"
+ rules={[
+ {
+ required: true,
+ message: <FormattedMessage id="请输入新密码!" defaultMessage="请输入新密码!" />,
+ },
+ ]}
+ />
+ <ProFormText.Password
+ name="confirmPassword"
+ label={intl.formatMessage({
+ id: 'system.user.confirm_password',
+ defaultMessage: '确认密码',
+ })}
+ width="xl"
+ placeholder="请输入确认密码"
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage id="请输入确认密码!" defaultMessage="请输入确认密码!" />
+ ),
+ },
+ { validator: checkPassword },
+ ]}
+ />
+ </ProForm>
+ </>
+ );
+};
+
+export default ResetPassword;
diff --git a/src/pages/User/Center/index.tsx b/src/pages/User/Center/index.tsx
new file mode 100644
index 0000000..2ce308c
--- /dev/null
+++ b/src/pages/User/Center/index.tsx
@@ -0,0 +1,200 @@
+import {
+ ClusterOutlined,
+ MailOutlined,
+ TeamOutlined,
+ UserOutlined,
+ MobileOutlined,
+ ManOutlined,
+} from '@ant-design/icons';
+import { Card, Col, Divider, List, Row } from 'antd';
+import React, { useState } from 'react';
+import styles from './Center.less';
+import BaseInfo from './components/BaseInfo';
+import ResetPassword from './components/ResetPassword';
+import AvatarCropper from './components/AvatarCropper';
+import { useRequest } from '@umijs/max';
+import { getUserInfo } from '@/services/session';
+import { PageLoading } from '@ant-design/pro-components';
+
+const operationTabList = [
+ {
+ key: 'base',
+ tab: (
+ <span>
+ 基本资料
+ </span>
+ ),
+ },
+ {
+ key: 'password',
+ tab: (
+ <span>
+ 重置密码
+ </span>
+ ),
+ },
+];
+
+export type tabKeyType = 'base' | 'password';
+
+const Center: React.FC = () => {
+
+ const [tabKey, setTabKey] = useState<tabKeyType>('base');
+
+ const [cropperModalOpen, setCropperModalOpen] = useState<boolean>(false);
+
+ // 获取用户信息
+ const { data: userInfo, loading } = useRequest(async () => {
+ return { data: await getUserInfo()};
+ });
+ if (loading) {
+ return <div>loading...</div>;
+ }
+
+ const currentUser = userInfo?.user;
+
+ // 渲染用户信息
+ const renderUserInfo = ({
+ userName,
+ phonenumber,
+ email,
+ sex,
+ dept,
+ }: Partial<API.CurrentUser>) => {
+ return (
+ <List>
+ <List.Item>
+ <div>
+ <UserOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 用户名
+ </div>
+ <div>{userName}</div>
+ </List.Item>
+ <List.Item>
+ <div>
+ <ManOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 性别
+ </div>
+ <div>{sex === '1' ? '女' : '男'}</div>
+ </List.Item>
+ <List.Item>
+ <div>
+ <MobileOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 电话
+ </div>
+ <div>{phonenumber}</div>
+ </List.Item>
+ <List.Item>
+ <div>
+ <MailOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 邮箱
+ </div>
+ <div>{email}</div>
+ </List.Item>
+ <List.Item>
+ <div>
+ <ClusterOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ 部门
+ </div>
+ <div>{dept?.deptName}</div>
+ </List.Item>
+ </List>
+ );
+ };
+
+ // 渲染tab切换
+ const renderChildrenByTabKey = (tabValue: tabKeyType) => {
+ if (tabValue === 'base') {
+ return <BaseInfo values={currentUser} />;
+ }
+ if (tabValue === 'password') {
+ return <ResetPassword />;
+ }
+ return null;
+ };
+
+ if (!currentUser) {
+ return <PageLoading />;
+ }
+
+ return (
+ <div>
+ <Row gutter={[16, 24]}>
+ <Col lg={8} md={24}>
+ <Card
+ title="个人信息"
+ bordered={false}
+ loading={loading}
+ >
+ {!loading && (
+ <div style={{ textAlign: "center"}}>
+ <div className={styles.avatarHolder} onClick={()=>{setCropperModalOpen(true)}}>
+ <img alt="" src={currentUser.avatar} />
+ </div>
+ {renderUserInfo(currentUser)}
+ <Divider dashed />
+ <div className={styles.team}>
+ <div className={styles.teamTitle}>角色</div>
+ <Row gutter={36}>
+ {currentUser.roles &&
+ currentUser.roles.map((item: any) => (
+ <Col key={item.roleId} lg={24} xl={12}>
+ <TeamOutlined
+ style={{
+ marginRight: 8,
+ }}
+ />
+ {item.roleName}
+ </Col>
+ ))}
+ </Row>
+ </div>
+ </div>
+ )}
+ </Card>
+ </Col>
+ <Col lg={16} md={24}>
+ <Card
+ bordered={false}
+ tabList={operationTabList}
+ activeTabKey={tabKey}
+ onTabChange={(_tabKey: string) => {
+ setTabKey(_tabKey as tabKeyType);
+ }}
+ >
+ {renderChildrenByTabKey(tabKey)}
+ </Card>
+ </Col>
+ </Row>
+ <AvatarCropper
+ onFinished={() => {
+ setCropperModalOpen(false);
+ }}
+ open={cropperModalOpen}
+ data={currentUser.avatar}
+ />
+ </div>
+ );
+};
+
+export default Center;
diff --git a/src/pages/User/Login/index.tsx b/src/pages/User/Login/index.tsx
new file mode 100644
index 0000000..a18f785
--- /dev/null
+++ b/src/pages/User/Login/index.tsx
@@ -0,0 +1,436 @@
+import Footer from '@/components/Footer';
+import { getCaptchaImg, login } from '@/services/system/auth';
+import { getFakeCaptcha } from '@/services/ant-design-pro/login';
+import {
+ AlipayCircleOutlined,
+ LockOutlined,
+ MobileOutlined,
+ TaobaoCircleOutlined,
+ UserOutlined,
+ WeiboCircleOutlined,
+} from '@ant-design/icons';
+import {
+ LoginForm,
+ ProFormCaptcha,
+ ProFormCheckbox,
+ ProFormText,
+} from '@ant-design/pro-components';
+import { useEmotionCss } from '@ant-design/use-emotion-css';
+import { FormattedMessage, history, SelectLang, useIntl, useModel, Helmet } from '@umijs/max';
+import { Alert, Col, message, Row, Tabs, Image } from 'antd';
+import Settings from '../../../../config/defaultSettings';
+import React, { useEffect, useState } from 'react';
+import { flushSync } from 'react-dom';
+import { clearSessionToken, setSessionToken } from '@/access';
+
+const ActionIcons = () => {
+ const langClassName = useEmotionCss(({ token }) => {
+ return {
+ marginLeft: '8px',
+ color: 'rgba(0, 0, 0, 0.2)',
+ fontSize: '24px',
+ verticalAlign: 'middle',
+ cursor: 'pointer',
+ transition: 'color 0.3s',
+ '&:hover': {
+ color: token.colorPrimaryActive,
+ },
+ };
+ });
+
+ return (
+ <>
+ <AlipayCircleOutlined key="AlipayCircleOutlined" className={langClassName} />
+ <TaobaoCircleOutlined key="TaobaoCircleOutlined" className={langClassName} />
+ <WeiboCircleOutlined key="WeiboCircleOutlined" className={langClassName} />
+ </>
+ );
+};
+
+const Lang = () => {
+ const langClassName = useEmotionCss(({ token }) => {
+ return {
+ width: 42,
+ height: 42,
+ lineHeight: '42px',
+ position: 'fixed',
+ right: 16,
+ borderRadius: token.borderRadius,
+ ':hover': {
+ backgroundColor: token.colorBgTextHover,
+ },
+ };
+ });
+
+ return (
+ <div className={langClassName} data-lang>
+ {SelectLang && <SelectLang />}
+ </div>
+ );
+};
+
+const LoginMessage: React.FC<{
+ content: string;
+}> = ({ content }) => {
+ return (
+ <Alert
+ style={{
+ marginBottom: 24,
+ }}
+ message={content}
+ type="error"
+ showIcon
+ />
+ );
+};
+
+const Login: React.FC = () => {
+ const [userLoginState, setUserLoginState] = useState<API.LoginResult>({code: 200});
+ const [type, setType] = useState<string>('account');
+ const { initialState, setInitialState } = useModel('@@initialState');
+ const [captchaCode, setCaptchaCode] = useState<string>('');
+ const [uuid, setUuid] = useState<string>('');
+
+ const containerClassName = useEmotionCss(() => {
+ return {
+ display: 'flex',
+ flexDirection: 'column',
+ height: '100vh',
+ overflow: 'auto',
+ backgroundImage:
+ "url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
+ backgroundSize: '100% 100%',
+ };
+ });
+
+ const intl = useIntl();
+
+ const getCaptchaCode = async () => {
+ const response = await getCaptchaImg();
+ const imgdata = `data:image/png;base64,${response.img}`;
+ setCaptchaCode(imgdata);
+ setUuid(response.uuid);
+ };
+
+ const fetchUserInfo = async () => {
+ const userInfo = await initialState?.fetchUserInfo?.();
+ if (userInfo) {
+ flushSync(() => {
+ setInitialState((s) => ({
+ ...s,
+ currentUser: userInfo,
+ }));
+ });
+ }
+ };
+
+ const handleSubmit = async (values: API.LoginParams) => {
+ try {
+ // 登录
+ const response = await login({ ...values, uuid });
+ if (response.code === 200) {
+ const defaultLoginSuccessMessage = intl.formatMessage({
+ id: 'pages.login.success',
+ defaultMessage: '登录成功!',
+ });
+ const current = new Date();
+ const expireTime = current.setTime(current.getTime() + 1000 * 12 * 60 * 60);
+ console.log('login response: ', response);
+ setSessionToken(response?.token, response?.token, expireTime);
+ message.success(defaultLoginSuccessMessage);
+ await fetchUserInfo();
+ console.log('login ok');
+ const urlParams = new URL(window.location.href).searchParams;
+ history.push(urlParams.get('redirect') || '/');
+ return;
+ } else {
+ console.log(response.msg);
+ clearSessionToken();
+ // 如果失败去设置用户错误信息
+ setUserLoginState({ ...response, type });
+ getCaptchaCode();
+ }
+ } catch (error) {
+ const defaultLoginFailureMessage = intl.formatMessage({
+ id: 'pages.login.failure',
+ defaultMessage: '登录失败,请重试!',
+ });
+ console.log(error);
+ message.error(defaultLoginFailureMessage);
+ }
+ };
+ const { code } = userLoginState;
+ const loginType = type;
+
+ useEffect(() => {
+ getCaptchaCode();
+ }, []);
+
+ return (
+ <div className={containerClassName}>
+ <Helmet>
+ <title>
+ {intl.formatMessage({
+ id: 'menu.login',
+ defaultMessage: '登录页',
+ })}
+ - {Settings.title}
+ </title>
+ </Helmet>
+ <Lang />
+ <div
+ style={{
+ flex: '1',
+ padding: '32px 0',
+ }}
+ >
+ <LoginForm
+ contentStyle={{
+ minWidth: 280,
+ maxWidth: '75vw',
+ }}
+ logo={<img alt="logo" src="/logo.svg" />}
+ title="Ant Design"
+ subTitle={intl.formatMessage({ id: 'pages.layouts.userLayout.title' })}
+ initialValues={{
+ autoLogin: true,
+ }}
+ actions={[
+ <FormattedMessage
+ key="loginWith"
+ id="pages.login.loginWith"
+ defaultMessage="其他登录方式"
+ />,
+ <ActionIcons key="icons" />,
+ ]}
+ onFinish={async (values) => {
+ await handleSubmit(values as API.LoginParams);
+ }}
+ >
+ <Tabs
+ activeKey={type}
+ onChange={setType}
+ centered
+ items={[
+ {
+ key: 'account',
+ label: intl.formatMessage({
+ id: 'pages.login.accountLogin.tab',
+ defaultMessage: '账户密码登录',
+ }),
+ },
+ {
+ key: 'mobile',
+ label: intl.formatMessage({
+ id: 'pages.login.phoneLogin.tab',
+ defaultMessage: '手机号登录',
+ }),
+ },
+ ]}
+ />
+
+ {code !== 200 && loginType === 'account' && (
+ <LoginMessage
+ content={intl.formatMessage({
+ id: 'pages.login.accountLogin.errorMessage',
+ defaultMessage: '账户或密码错误(admin/admin123)',
+ })}
+ />
+ )}
+ {type === 'account' && (
+ <>
+ <ProFormText
+ name="username"
+ initialValue="admin"
+ fieldProps={{
+ size: 'large',
+ prefix: <UserOutlined />,
+ }}
+ placeholder={intl.formatMessage({
+ id: 'pages.login.username.placeholder',
+ defaultMessage: '用户名: admin',
+ })}
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage
+ id="pages.login.username.required"
+ defaultMessage="请输入用户名!"
+ />
+ ),
+ },
+ ]}
+ />
+ <ProFormText.Password
+ name="password"
+ initialValue="admin123"
+ fieldProps={{
+ size: 'large',
+ prefix: <LockOutlined />,
+ }}
+ placeholder={intl.formatMessage({
+ id: 'pages.login.password.placeholder',
+ defaultMessage: '密码: admin123',
+ })}
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage
+ id="pages.login.password.required"
+ defaultMessage="请输入密码!"
+ />
+ ),
+ },
+ ]}
+ />
+ <Row>
+ <Col flex={3}>
+ <ProFormText
+ style={{
+ float: 'right',
+ }}
+ name="code"
+ placeholder={intl.formatMessage({
+ id: 'pages.login.captcha.placeholder',
+ defaultMessage: '请输入验证',
+ })}
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage
+ id="pages.searchTable.updateForm.ruleName.nameRules"
+ defaultMessage="请输入验证啊"
+ />
+ ),
+ },
+ ]}
+ />
+ </Col>
+ <Col flex={2}>
+ <Image
+ src={captchaCode}
+ alt="验证码"
+ style={{
+ display: 'inline-block',
+ verticalAlign: 'top',
+ cursor: 'pointer',
+ paddingLeft: '10px',
+ width: '100px',
+ }}
+ preview={false}
+ onClick={() => getCaptchaCode()}
+ />
+ </Col>
+ </Row>
+ </>
+ )}
+
+ {code !== 200 && loginType === 'mobile' && <LoginMessage content="验证码错误" />}
+ {type === 'mobile' && (
+ <>
+ <ProFormText
+ fieldProps={{
+ size: 'large',
+ prefix: <MobileOutlined />,
+ }}
+ name="mobile"
+ placeholder={intl.formatMessage({
+ id: 'pages.login.phoneNumber.placeholder',
+ defaultMessage: '手机号',
+ })}
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage
+ id="pages.login.phoneNumber.required"
+ defaultMessage="请输入手机号!"
+ />
+ ),
+ },
+ {
+ pattern: /^1\d{10}$/,
+ message: (
+ <FormattedMessage
+ id="pages.login.phoneNumber.invalid"
+ defaultMessage="手机号格式错误!"
+ />
+ ),
+ },
+ ]}
+ />
+ <ProFormCaptcha
+ fieldProps={{
+ size: 'large',
+ prefix: <LockOutlined />,
+ }}
+ captchaProps={{
+ size: 'large',
+ }}
+ placeholder={intl.formatMessage({
+ id: 'pages.login.captcha.placeholder',
+ defaultMessage: '请输入验证码',
+ })}
+ captchaTextRender={(timing, count) => {
+ if (timing) {
+ return `${count} ${intl.formatMessage({
+ id: 'pages.getCaptchaSecondText',
+ defaultMessage: '获取验证码',
+ })}`;
+ }
+ return intl.formatMessage({
+ id: 'pages.login.phoneLogin.getVerificationCode',
+ defaultMessage: '获取验证码',
+ });
+ }}
+ name="captcha"
+ rules={[
+ {
+ required: true,
+ message: (
+ <FormattedMessage
+ id="pages.login.captcha.required"
+ defaultMessage="请输入验证码!"
+ />
+ ),
+ },
+ ]}
+ onGetCaptcha={async (phone) => {
+ const result = await getFakeCaptcha({
+ phone,
+ });
+ if (!result) {
+ return;
+ }
+ message.success('获取验证码成功!验证码为:1234');
+ }}
+ />
+ </>
+ )}
+ <div
+ style={{
+ marginBottom: 24,
+ }}
+ >
+ <ProFormCheckbox noStyle name="autoLogin">
+ <FormattedMessage id="pages.login.rememberMe" defaultMessage="自动登录" />
+ </ProFormCheckbox>
+ <a
+ style={{
+ float: 'right',
+ }}
+ >
+ <FormattedMessage id="pages.login.forgotPassword" defaultMessage="忘记密码" />
+ </a>
+ </div>
+ </LoginForm>
+ </div>
+ <Footer />
+ </div>
+ );
+};
+
+export default Login;
diff --git a/src/pages/User/Settings/index.tsx b/src/pages/User/Settings/index.tsx
new file mode 100644
index 0000000..f29d0b9
--- /dev/null
+++ b/src/pages/User/Settings/index.tsx
@@ -0,0 +1,20 @@
+import { PageContainer } from '@ant-design/pro-components';
+import { Card } from 'antd';
+import React from 'react';
+
+/**
+ *
+ * @author whiteshader@163.com
+ *
+ * */
+
+
+const Settings: React.FC = () => {
+ return (
+ <PageContainer>
+ <Card title="Developing" />
+ </PageContainer>
+ );
+};
+
+export default Settings;
diff --git a/src/pages/Welcome.tsx b/src/pages/Welcome.tsx
new file mode 100644
index 0000000..d0c49f7
--- /dev/null
+++ b/src/pages/Welcome.tsx
@@ -0,0 +1,164 @@
+import { PageContainer } from '@ant-design/pro-components';
+import { useModel } from '@umijs/max';
+import { Card, theme } from 'antd';
+import React from 'react';
+
+/**
+ * 每个单独的卡片,为了复用样式抽成了组件
+ * @param param0
+ * @returns
+ */
+const InfoCard: React.FC<{
+ title: string;
+ index: number;
+ desc: string;
+ href: string;
+}> = ({ title, href, index, desc }) => {
+ const { useToken } = theme;
+
+ const { token } = useToken();
+
+ return (
+ <div
+ style={{
+ backgroundColor: token.colorBgContainer,
+ boxShadow: token.boxShadow,
+ borderRadius: '8px',
+ fontSize: '14px',
+ color: token.colorTextSecondary,
+ lineHeight: '22px',
+ padding: '16px 19px',
+ minWidth: '220px',
+ flex: 1,
+ }}
+ >
+ <div
+ style={{
+ display: 'flex',
+ gap: '4px',
+ alignItems: 'center',
+ }}
+ >
+ <div
+ style={{
+ width: 48,
+ height: 48,
+ lineHeight: '22px',
+ backgroundSize: '100%',
+ textAlign: 'center',
+ padding: '8px 16px 16px 12px',
+ color: '#FFF',
+ fontWeight: 'bold',
+ backgroundImage:
+ "url('https://gw.alipayobjects.com/zos/bmw-prod/daaf8d50-8e6d-4251-905d-676a24ddfa12.svg')",
+ }}
+ >
+ {index}
+ </div>
+ <div
+ style={{
+ fontSize: '16px',
+ color: token.colorText,
+ paddingBottom: 8,
+ }}
+ >
+ {title}
+ </div>
+ </div>
+ <div
+ style={{
+ fontSize: '14px',
+ color: token.colorTextSecondary,
+ textAlign: 'justify',
+ lineHeight: '22px',
+ marginBottom: 8,
+ }}
+ >
+ {desc}
+ </div>
+ <a href={href} target="_blank" rel="noreferrer">
+ 了解更多 {'>'}
+ </a>
+ </div>
+ );
+};
+
+const Welcome: React.FC = () => {
+ const { token } = theme.useToken();
+ const { initialState } = useModel('@@initialState');
+ return (
+ <PageContainer>
+ <Card
+ style={{
+ borderRadius: 8,
+ }}
+ bodyStyle={{
+ backgroundImage:
+ initialState?.settings?.navTheme === 'realDark'
+ ? 'background-image: linear-gradient(75deg, #1A1B1F 0%, #191C1F 100%)'
+ : 'background-image: linear-gradient(75deg, #FBFDFF 0%, #F5F7FF 100%)',
+ }}
+ >
+ <div
+ style={{
+ backgroundPosition: '100% -30%',
+ backgroundRepeat: 'no-repeat',
+ backgroundSize: '274px auto',
+ backgroundImage:
+ "url('https://gw.alipayobjects.com/mdn/rms_a9745b/afts/img/A*BuFmQqsB2iAAAAAAAAAAAAAAARQnAQ')",
+ }}
+ >
+ <div
+ style={{
+ fontSize: '20px',
+ color: token.colorTextHeading,
+ }}
+ >
+ 欢迎使用 Ant Design Pro
+ </div>
+ <p
+ style={{
+ fontSize: '14px',
+ color: token.colorTextSecondary,
+ lineHeight: '22px',
+ marginTop: 16,
+ marginBottom: 32,
+ width: '65%',
+ }}
+ >
+ Ant Design Pro 是一个整合了 umi,Ant Design 和 ProComponents
+ 的脚手架方案。致力于在设计规范和基础组件的基础上,继续向上构建,提炼出典型模板/业务组件/配套设计资源,进一步提升企业级中后台产品设计研发过程中的『用户』和『设计者』的体验。
+ </p>
+ <div
+ style={{
+ display: 'flex',
+ flexWrap: 'wrap',
+ gap: 16,
+ }}
+ >
+ <InfoCard
+ index={1}
+ href="https://umijs.org/docs/introduce/introduce"
+ title="了解 umi"
+ desc="umi 是一个可扩展的企业级前端应用框架,umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。"
+ />
+ <InfoCard
+ index={2}
+ title="了解 ant design"
+ href="https://ant.design"
+ desc="antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。"
+ />
+ <InfoCard
+ index={3}
+ title="了解 Pro Components"
+ href="https://procomponents.ant.design"
+ desc="ProComponents 是一个基于 Ant Design 做了更高抽象的模板组件,以 一个组件就是一个页面为开发理念,为中后台开发带来更好的体验。"
+ />
+ </div>
+ </div>
+ </Card>
+ </PageContainer>
+ );
+};
+
+export default Welcome;