wjx hai 1 mes
pai
achega
3498452d65

+ 12 - 0
config/routerConfig.ts

@@ -498,6 +498,18 @@ const gsData = {
             name: '角色等级表(仙剑)',
             name: '角色等级表(仙剑)',
             access: 'xjRoleGrade',
             access: 'xjRoleGrade',
             component: './gsData/xjRoleGrade',
             component: './gsData/xjRoleGrade',
+        },
+        {
+            path: '/gsData/roleIpMonitor',
+            name: '角色IP风险监控',
+            access: 'roleIpMonitor',
+            component: './gsData/roleIpMonitor',
+        },
+        {
+            path: '/gsData/gandRole',
+            name: '游戏帮派角色表(仙剑)',
+            access: 'gandRole',
+            component: './gsData/gandRole',
         }
         }
     ]
     ]
 }
 }

+ 2 - 0
src/app.tsx

@@ -17,6 +17,7 @@ import { ReactComponent as GameServerSvg } from '@/assets/gameServer.svg'
 import { ReactComponent as MediaSvg } from '@/assets/media.svg'
 import { ReactComponent as MediaSvg } from '@/assets/media.svg'
 import { ReactComponent as PlayerSvg } from '@/assets/player.svg'
 import { ReactComponent as PlayerSvg } from '@/assets/player.svg'
 import { ReactComponent as RoleManageSvg } from '@/assets/roleManage.svg'
 import { ReactComponent as RoleManageSvg } from '@/assets/roleManage.svg'
+import { ReactComponent as MonitorSvg } from '@/assets/monitor.svg'
 import versions from './utils/versions';
 import versions from './utils/versions';
 
 
 
 
@@ -111,6 +112,7 @@ const IconMap = {
     media: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><MediaSvg /></span>,
     media: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><MediaSvg /></span>,
     player: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><PlayerSvg /></span>,
     player: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><PlayerSvg /></span>,
     roleManage: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><RoleManageSvg /></span>,
     roleManage: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><RoleManageSvg /></span>,
+    monitor: <span role="img" aria-label="fund-view" className="anticon anticon-fund-view"><MonitorSvg /></span>,
     eye: <EyeOutlined />
     eye: <EyeOutlined />
 };
 };
 //处理菜单
 //处理菜单

+ 2 - 0
src/assets/monitor.svg

@@ -0,0 +1,2 @@
+<svg viewBox="64 64 896 896" focusable="false" data-icon="fund-view" width="1em" height="1em" fill="currentColor" aria-hidden="true"><defs><style type="text/css">@font-face { font-family: rbicon; src: url("chrome-extension://dipiagiiohfljcicegpgffpbnjmgjcnf/fonts/rbicon.woff2") format("woff2"); font-weight: normal; font-style: normal; }
+</style></defs><path d="M64 64V640H896V64H64zM0 0h960v704H0V0z" p-id="4696"></path><path d="M192 896H768v64H192zM448 640H512v256h-64z" p-id="4697"></path><path d="M479.232 561.604267l309.9904-348.330667-47.803733-42.5472-259.566934 291.669333L303.957333 240.008533 163.208533 438.6048l52.224 37.009067 91.6224-129.28z" p-id="4698"></path></svg>

+ 1 - 1
src/components/QueryForm/index.tsx

@@ -1333,7 +1333,7 @@ const QueryForm: React.FC<Props> = (props) => {
 
 
             {/* 用户id */}
             {/* 用户id */}
             {isUserId && <Col><Form.Item name='userId'>
             {isUserId && <Col><Form.Item name='userId'>
-                <Input placeholder="用户ID" allowClear style={{ width: 140 }} />
+                <Input placeholder="玩家ID" allowClear style={{ width: 140 }} />
             </Form.Item></Col>}
             </Form.Item></Col>}
 
 
             {/* 玩家昵称 */}
             {/* 玩家昵称 */}

+ 43 - 37
src/pages/gameDataStatistics/player/list/look.tsx

@@ -293,24 +293,6 @@ export const gameRoleTableColumns: any = [
     //     align: 'center',
     //     align: 'center',
     //     width: 20,
     //     width: 20,
     // },
     // },
-    {
-        title: '游戏名称',
-        dataIndex: 'gameName',
-        key: 'gameName',
-        align: 'center',
-        fixed: 'left',
-        width: 100,
-        ellipsis: true,
-    },
-    {
-        title: '游戏应用类型',
-        dataIndex: 'gameCategoryName',
-        key: 'gameCategoryName',
-        align: 'center',
-        fixed: 'left',
-        width: 100,
-        ellipsis: true,
-    },
     {
     {
         title: '游戏角色名称',
         title: '游戏角色名称',
         dataIndex: 'roleName',
         dataIndex: 'roleName',
@@ -325,14 +307,53 @@ export const gameRoleTableColumns: any = [
         dataIndex: 'roleId',
         dataIndex: 'roleId',
         key: 'roleId',
         key: 'roleId',
         align: 'center',
         align: 'center',
-        width: 200,
+        width: 120,
+        fixed: 'left',
         render: (a: string, b: any) => (<WidthEllipsis value={a} />)
         render: (a: string, b: any) => (<WidthEllipsis value={a} />)
     },
     },
+    {
+        title: '战力',
+        dataIndex: 'rolePower',
+        key: 'rolePower',
+        align: 'center',
+        width: 90
+    },
+    {
+        title: '操作系统',
+        dataIndex: 'os',
+        key: 'os',
+        align: 'center',
+        width: 90
+    },
+    {
+        title: '充值金额',
+        dataIndex: 'rechargeMoney',
+        key: 'rechargeMoney',
+        align: 'center',
+        width: 90
+    },
+    {
+        title: '游戏名称',
+        dataIndex: 'gameName',
+        key: 'gameName',
+        align: 'center',
+        width: 100,
+        ellipsis: true,
+    },
+    {
+        title: '游戏应用类型',
+        dataIndex: 'gameCategoryName',
+        key: 'gameCategoryName',
+        align: 'center',
+        width: 100,
+        ellipsis: true,
+    },
     {
     {
         title: '注册渠道',
         title: '注册渠道',
         dataIndex: 'agentName',
         dataIndex: 'agentName',
         key: 'agentName',
         key: 'agentName',
         align: 'center',
         align: 'center',
+        width: 100,
         render: (a: string, b: any) => (<WidthEllipsis value={a} />)
         render: (a: string, b: any) => (<WidthEllipsis value={a} />)
     },
     },
     {
     {
@@ -340,31 +361,16 @@ export const gameRoleTableColumns: any = [
         dataIndex: 'serverName',
         dataIndex: 'serverName',
         key: 'serverName',
         key: 'serverName',
         align: 'center',
         align: 'center',
+        width: 100
     },
     },
     {
     {
         title: '角色等级',
         title: '角色等级',
         dataIndex: 'roleLevel',
         dataIndex: 'roleLevel',
         key: 'roleLevel',
         key: 'roleLevel',
         align: 'center',
         align: 'center',
+        width: 90
     },
     },
-    {
-        title: '战力',
-        dataIndex: 'rolePower',
-        key: 'rolePower',
-        align: 'center',
-    },
-    {
-        title: '操作系统',
-        dataIndex: 'os',
-        key: 'os',
-        align: 'center',
-    },
-    {
-        title: '充值金额',
-        dataIndex: 'rechargeMoney',
-        key: 'rechargeMoney',
-        align: 'center',
-    },
+    
     {
     {
         title: '角色创建时间',
         title: '角色创建时间',
         dataIndex: 'createTime',
         dataIndex: 'createTime',

+ 3 - 3
src/pages/gameDataStatistics/player/role/lookRoleDetails.tsx

@@ -6,7 +6,7 @@ import React, { useEffect, useState } from "react"
 import { gameRoleTableColumns } from "../list/look"
 import { gameRoleTableColumns } from "../list/look"
 
 
 
 
-const LookRoleDetails: React.FC<{ userId: any }> = ({ userId }) => {
+const LookRoleDetails: React.FC<{ userId: any, icon?: React.ReactNode }> = ({ userId, icon }) => {
 
 
     /*********************************/
     /*********************************/
     const [visible, setVisible] = useState<boolean>(false)
     const [visible, setVisible] = useState<boolean>(false)
@@ -20,7 +20,7 @@ const LookRoleDetails: React.FC<{ userId: any }> = ({ userId }) => {
     }, [userId, visible])
     }, [userId, visible])
 
 
     return <>
     return <>
-        <a onClick={() => setVisible(true)}>角色</a>
+        <a onClick={() => setVisible(true)}>{icon || '角色'}</a>
         {visible && <Modal
         {visible && <Modal
             title={<strong>游戏角色(玩家ID:{userId})</strong>}
             title={<strong>游戏角色(玩家ID:{userId})</strong>}
             visible={visible}
             visible={visible}
@@ -33,7 +33,7 @@ const LookRoleDetails: React.FC<{ userId: any }> = ({ userId }) => {
                 dataSource={getUserGameRoleList?.data}
                 dataSource={getUserGameRoleList?.data}
                 loading={getUserGameRoleList?.loading}
                 loading={getUserGameRoleList?.loading}
                 size="small"
                 size="small"
-                scroll={{ x: 2000 }}
+                scroll={{ x: 1000 }}
                 bordered
                 bordered
             />
             />
         </Modal>}
         </Modal>}

+ 134 - 0
src/pages/gsData/gandRole/countryList.tsx

@@ -0,0 +1,134 @@
+import { useAjax } from '@/Hook/useAjax';
+import { getGameCountryRoleListApi, getGameCountryRoleListProps } from '@/services/gsData';
+import { Modal, Table } from 'antd';
+import React, { useEffect, useState } from 'react';
+
+interface Props {
+    country: string
+    serverId: string
+}
+/**
+ * 帮派成员
+ * @returns 
+ */
+const CountryList: React.FC<Props> = ({ country, serverId }) => {
+
+    /*********************************/
+    const [visible, setVisible] = useState<boolean>(false)
+    const [queryForm, setQueryForm] = useState<getGameCountryRoleListProps>({ country, serverId, pageNum: 1, pageSize: 20 })
+
+    const getGameCountryRoleList = useAjax((params) => getGameCountryRoleListApi(params))
+    /*********************************/
+
+    useEffect(() => {
+        if (visible) {
+            getGameCountryRoleList.run(queryForm)
+        }
+    }, [visible, queryForm])
+
+    return (<>
+        <a style={{ fontSize: 12 }} onClick={() => setVisible(true)}>帮派成员</a>
+        {visible && <Modal
+            title={<strong>{country}</strong>}
+            visible={visible}
+            onCancel={() => setVisible(false)}
+            footer={null}
+            width={900}
+        >
+            <Table
+                dataSource={getGameCountryRoleList?.data?.records}
+                size="small"
+                bordered
+                loading={getGameCountryRoleList.loading}
+                rowKey={'roleId'}
+                scroll={{ x: 500 }}
+                columns={[
+                    {
+                        title: '角色名称',
+                        dataIndex: 'roleName',
+                        key: 'roleName',
+                        width: 90,
+                        ellipsis: true
+                    },
+                    {
+                        title: '角色ID',
+                        dataIndex: 'roleId',
+                        key: 'roleId',
+                        width: 80,
+                        align: 'center'
+                    },
+                    {
+                        title: '用户ID',
+                        dataIndex: 'userId',
+                        key: 'userId',
+                        width: 80,
+                        align: 'center'
+                    },
+                    {
+                        title: '区服ID',
+                        dataIndex: 'serverId',
+                        key: 'serverId',
+                        width: 60,
+                        align: 'center'
+                    },
+                    {
+                        title: '帮派名称',
+                        dataIndex: 'country',
+                        key: 'country',
+                        width: 90,
+                        ellipsis: true,
+                        align: 'center'
+                    },
+                    {
+                        title: '角色等级',
+                        dataIndex: 'roleLevel',
+                        key: 'roleLevel',
+                        width: 70,
+                        align: 'center',
+                        sorter: true
+                    },
+                    {
+                        title: '玩家角色战力',
+                        dataIndex: 'combatNum',
+                        key: 'combatNum',
+                        width: 80,
+                        align: 'center',
+                        sorter: true
+                    },
+                    {
+                        title: '24小时内角色累计充值',
+                        dataIndex: 'roleTotalAmount',
+                        key: 'roleTotalAmount',
+                        width: 90,
+                        align: 'center',
+                        sorter: true
+                    },
+                    {
+                        title: '创角时间',
+                        dataIndex: 'createTime',
+                        key: 'createTime',
+                        width: 140,
+                        align: 'center',
+                        ellipsis: true
+                    }
+                ]}
+                onChange={(pagination, filters, sorter: any) => {
+                    let { current, pageSize } = pagination
+                    let newQueryForm = JSON.parse(JSON.stringify(queryForm))
+                    if (sorter && sorter?.order) {
+                        newQueryForm['sortType'] = sorter?.order === 'ascend' ? 'asc' : 'desc'
+                        newQueryForm['sortFiled'] = sorter?.field
+                    } else {
+                        delete newQueryForm['sortType']
+                        delete newQueryForm['sortFiled']
+                    }
+                    newQueryForm.pageNum = current || newQueryForm.pageNum
+                    newQueryForm.pageSize = pageSize || newQueryForm.pageSize
+                    setQueryForm({ ...newQueryForm })
+                }}
+            />
+        </Modal>}
+    </>);
+};
+
+export default React.memo(CountryList);

+ 234 - 0
src/pages/gsData/gandRole/index.tsx

@@ -0,0 +1,234 @@
+import { useAjax } from '@/Hook/useAjax';
+import { getGameListNewApi, getGameServerListApi } from '@/services/gameData';
+import { getGameCountryDataListApi, GetGameCountryDataListProps, getGameCountryListApi } from '@/services/gsData';
+import { groupBy } from '@/utils/utils';
+import { Button, Card, Col, Form, Row, Select, Space, Spin, Table, Typography } from 'antd';
+import React, { useEffect, useState } from 'react';
+import CountryList from './countryList';
+
+const GandRole: React.FC = () => {
+
+    /****************************************/
+    const [form] = Form.useForm()
+    const gameId = Form.useWatch('gameId', form)
+    const serverId = Form.useWatch('serverId', form)
+    const gameCountryList = Form.useWatch('gameCountryList', form)
+
+    const [gameList, setGameList] = useState<any[]>([])
+    const [parentGameList, setParentGameList] = useState<any[]>([])
+    const [superGameList, setSuperGameList] = useState<any[]>([])
+    const [countryList, setCountryList] = useState<any[]>([])
+
+    const [queryForm, setQueryForm] = useState<GetGameCountryDataListProps>({})
+    const [dataList, setDataList] = useState<{ [x: string]: any[] }>({})
+
+    const getGameListNew = useAjax((params) => getGameListNewApi(params))
+    const getGameServerList = useAjax((params) => getGameServerListApi(params))
+    const getGameCountryList = useAjax((params) => getGameCountryListApi(params))
+    const getGameCountryDataList = useAjax((params) => getGameCountryDataListApi(params))
+    /****************************************/
+
+    useEffect(() => {
+        getGameCountryDataList.run({ ...queryForm, gameId: queryForm?.gameId ? [queryForm.gameId] : undefined }).then(res => {
+            if (res) {
+                setDataList(res)
+            } else {
+                setDataList({})
+            }
+        }).catch(() => {
+            setDataList({})
+        })
+    }, [queryForm])
+
+    /** 游戏列表 */
+    useEffect(() => {
+        getGameListNew.run({ sourceSystem: 'ZX_ONE' }).then((res: { gameList: any; parentGameList: any; superGameList: any }) => {
+            if (res) {
+                const { gameList, parentGameList, superGameList } = res
+                setGameList(gameList?.map((item: { id: any; game_name: any }) => ({ id: item.id, name: item.game_name })) || [])
+                setParentGameList(parentGameList)
+                setSuperGameList(superGameList)
+            }
+        })
+    }, [])
+
+    useEffect(() => {
+        getGameCountryList.run({ gameId: gameId ? [gameId] : undefined, serverId }).then(res => {
+            if (res?.length) {
+                const data = res.map((item: { country: string; serverId: string; }) => ({ ...item, id: item.country + ',' + item.serverId }))
+                const countryData = groupBy(data, (item) => item.serverId, true)
+                const countryList = Object.keys(countryData).map(key => {
+                    return {
+                        label: countryData[key]?.[0]?.gameName + '_' + countryData[key]?.[0]?.serverName,
+                        value: JSON.parse(key),
+                        options: countryData[key]?.map((item: { country: any; id: any; }) => ({
+                            label: item.country,
+                            value: item.id
+                        }))
+                    }
+                })
+                setCountryList(countryList)
+            } else {
+                setCountryList([])
+            }
+        })
+    }, [serverId, gameId])
+
+    useEffect(() => {
+        setCountryList(data => data.map(i => {
+            return { ...i, options: i.options.map((c: any) => ({ ...c, disabled: gameCountryList?.length >= 4 ? gameCountryList?.includes(c.value) ? false : true : false })) }
+        }))
+    }, [gameCountryList])
+
+    const onFinish = (data: any) => {
+        setQueryForm(data)
+    }
+
+    return (
+        <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
+            <Card bodyStyle={{ padding: 10 }}>
+                <Form layout="inline" className='queryForm' initialValues={{}} name="basicGandRole" form={form} onFinish={onFinish}>
+                    <Form.Item name='gameId'>
+                        <Select
+                            showSearch
+                            style={{ minWidth: 140 }}
+                            allowClear
+                            placeholder={'请选择父游戏'}
+                            filterOption={(input, option) =>
+                                (option?.children as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                            }
+                            onChange={(e, option: any) => {
+                                if (option?.['data-super-id']) {
+                                    getGameServerList.run({ gameId: option['data-super-id'] })
+                                } else {
+                                    getGameServerList?.data && getGameServerList.mutate([])
+                                }
+                                form.setFieldsValue({ gameCountryList: undefined })
+                            }}
+                        >
+                            {parentGameList?.filter(item => [35, 36].includes(item.parent_game_id))?.map((item: any) => <Select.Option value={item.parent_game_id} key={item.parent_game_id} data-super-id={item.super_game_id}>{item.parent_game_name}</Select.Option>)}
+                        </Select>
+                    </Form.Item>
+                    {gameId && <>
+                        <Form.Item name='serverId'>
+                            <Select
+                                showSearch
+                                maxTagCount={1}
+                                mode='multiple'
+                                style={{ minWidth: 140 }}
+                                allowClear
+                                placeholder={'请选择区服'}
+                                filterOption={(input, option) =>
+                                    (option?.children as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                                }
+                                loading={getGameServerList.loading}
+                                onChange={() => {
+                                    form.setFieldsValue({ gameCountryList: undefined })
+                                }}
+                            >
+                                {getGameServerList?.data?.map((item: any) => <Select.Option value={item.serverId} key={item.serverId}>{item.serverName}</Select.Option>)}
+                            </Select>
+                        </Form.Item>
+                    </>}
+                    <Form.Item name='gameCountryList'>
+                        <Select
+                            maxTagCount={1}
+                            mode='multiple'
+                            showSearch
+                            style={{ minWidth: 160 }}
+                            allowClear
+                            placeholder={'请选择帮派'}
+                            filterOption={(input, option) =>
+                                (option?.children as any)?.toLowerCase().indexOf(input.toLowerCase()) >= 0
+                            }
+                            loading={getGameCountryList.loading}
+                            options={countryList}
+                        />
+                    </Form.Item>
+                    <Space>
+                        <Button type="primary" htmlType="submit">搜索</Button>
+                        <Button onClick={() => form.resetFields()}>重置</Button>
+                    </Space>
+                </Form>
+            </Card>
+            <Card bodyStyle={{ padding: 10 }}>
+                <Spin spinning={getGameCountryDataList.loading}>
+                    <Row gutter={[20, 20]}>
+                        {Object.keys(dataList).map((d, index) => {
+                            const [country] = d.split(',')
+                            return <Col key={d + index} span={12}>
+                                <Typography.Title level={3} style={{ textAlign: 'center' }}>{country}</Typography.Title>
+                                <Table
+                                    dataSource={dataList[d].map((item, index) => ({ ...item, id: index + 1 }))}
+                                    size="small"
+                                    bordered
+                                    rowKey={'id'}
+                                    scroll={{ x: 500 }}
+                                    columns={[
+                                        {
+                                            title: '等级',
+                                            dataIndex: 'countryLevel',
+                                            key: 'countryLevel',
+                                            render(value) {
+                                                return ({ 0: '等级1-13级', 1: '等级13级以上(不包含13级)' } as any)[value]
+                                            },
+                                            width: 150
+                                        },
+                                        {
+                                            title: '区服ID',
+                                            dataIndex: 'serverId',
+                                            key: 'serverId',
+                                            width: 70,
+                                            align: 'center'
+                                        },
+                                        {
+                                            title: '帮派名称',
+                                            dataIndex: 'country',
+                                            key: 'country',
+                                            width: 160,
+                                            ellipsis: true
+                                        },
+                                        {
+                                            title: '角色总数量',
+                                            dataIndex: 'roleCount',
+                                            key: 'roleCount',
+                                            width: 70,
+                                            align: 'center'
+                                        },
+                                        {
+                                            title: '今日活跃角色数量',
+                                            dataIndex: 'activeCount',
+                                            key: 'activeCount',
+                                            width: 80,
+                                            align: 'center'
+                                        },
+                                        {
+                                            title: '角色累计充值',
+                                            dataIndex: 'roleTotalAmount',
+                                            key: 'roleTotalAmount',
+                                            width: 70,
+                                            align: 'center'
+                                        },
+                                        {
+                                            title: '操作',
+                                            dataIndex: 'cz',
+                                            key: 'cz',
+                                            width: 70,
+                                            align: 'center',
+                                            render(_, record) {
+                                                return <CountryList country={record.country} serverId={record.serverId} />
+                                            },
+                                        }
+                                    ]}
+                                    pagination={false}
+                                />
+                            </Col>
+                        })}
+                    </Row>
+                </Spin>
+            </Card>
+        </div>
+    );
+};
+
+export default GandRole;

+ 100 - 0
src/pages/gsData/roleIpMonitor/index.tsx

@@ -0,0 +1,100 @@
+import QueryForm from "@/components/QueryForm"
+import { useAjax } from "@/Hook/useAjax"
+import TablePro from "@/pages/gameDataStatistics/components/TablePro"
+import { getRoleIpInfoListApi, GetRoleIpInfoListProps } from "@/services/gsData"
+import React, { useEffect, useState } from "react"
+import moment from "moment"
+import columns12 from "./tableConfig"
+
+/**
+ * 角色IP风险监控
+ * @returns 
+ */
+const RoleIpMonitor: React.FC = () => {
+
+
+    /****************************************/
+    const [queryForm, setQueryForm] = useState<GetRoleIpInfoListProps>({
+        pageNum: 1,
+        pageSize: 30,
+        roleLevelMin: 16,
+        roleLevelMax: 16
+    })
+    const getRoleIpInfoList = useAjax((params) => getRoleIpInfoListApi(params))
+    /****************************************/
+
+    useEffect(() => {
+        getRoleIpInfoList.run(queryForm)
+    }, [queryForm])
+
+    return <div>
+        <TablePro
+            leftChild={<QueryForm
+                initialValues={{ regPayIntervalTime: [16, 16] }}
+                isUserId
+                isNickname
+                isGameRoleName
+                isGameRoleId
+                isGameName
+                isSuperParentGameId
+                isGameIds
+                isGameServerName
+                isServerIds
+                isCreateRoleDay={{}}
+                isPayIntervalTime={{ tips: '角色等级区间' }}
+                isIp
+                onChange={(data: any) => {
+                    console.log(data)
+                    const { nickname, superParentGameId, createRoleDay, serverIds, regPayIntervalTime, lastActiveTime, ip, ...params } = data
+                    let newQueryForm = JSON.parse(JSON.stringify(queryForm))
+                    newQueryForm.pageNum = 1
+                    newQueryForm.serverId = serverIds
+                    newQueryForm.userName = nickname
+                    newQueryForm.regIp = ip
+                    if (createRoleDay && createRoleDay?.length === 2) {
+                        newQueryForm['createTimeMin'] = moment(createRoleDay[0]).format('YYYY-MM-DD')
+                        newQueryForm['createTimeMax'] = moment(createRoleDay[1]).format('YYYY-MM-DD')
+                    } else {
+                        delete newQueryForm['createTimeMin']
+                        delete newQueryForm['createTimeMax']
+                    }
+                    if (regPayIntervalTime?.length > 0 && (regPayIntervalTime[0] || regPayIntervalTime[1])) {
+                        newQueryForm.roleLevelMin = regPayIntervalTime[0]
+                        newQueryForm.roleLevelMax = regPayIntervalTime[1]
+                    } else {
+                        delete newQueryForm.roleLevelMin
+                        delete newQueryForm.roleLevelMax
+                    }
+                    setQueryForm({ ...newQueryForm, ...params })
+                }}
+            />}
+            config={columns12()}
+            configName={'角色IP风险监控'}
+            fixed={{ left: 2, right: 1 }}
+            scroll={{ x: 1000, y: 620 }}
+            title='角色IP风险监控'
+            loading={getRoleIpInfoList.loading}
+            ajax={getRoleIpInfoList}
+            page={getRoleIpInfoList?.data?.current || 1}
+            pageSize={getRoleIpInfoList?.data?.size || 20}
+            total={getRoleIpInfoList?.data?.total || 0}
+            dataSource={getRoleIpInfoList?.data?.records?.map((item: any, index: number) => ({ ...item, id: Number(queryForm.pageNum.toString() + (index + '')) }))}
+            onChange={(pagination: any, _: any, sortData: any) => {
+                let { current, pageSize } = pagination
+                let newQueryForm = JSON.parse(JSON.stringify(queryForm))
+                if (sortData && sortData?.order) {
+                    newQueryForm['sortType'] = sortData?.order === 'ascend' ? 'asc' : 'desc'
+                    newQueryForm['sortFiled'] = sortData?.field
+                } else {
+                    delete newQueryForm['sortType']
+                    delete newQueryForm['sortFiled']
+                }
+                newQueryForm.pageNum = current || newQueryForm.pageNum
+                newQueryForm.pageSize = pageSize || newQueryForm.pageSize
+                setQueryForm({ ...newQueryForm })
+            }}
+        />
+    </div>
+}
+
+export default RoleIpMonitor

+ 52 - 0
src/pages/gsData/roleIpMonitor/loginIpDetails.tsx

@@ -0,0 +1,52 @@
+import { useAjax } from '@/Hook/useAjax';
+import { getRoleIpDetailListApi } from '@/services/gsData';
+import { Modal, Table } from 'antd';
+import React, { useEffect, useState } from 'react';
+
+const LoginIpDetails: React.FC<{ roleId: any, icon?: React.ReactNode }> = ({ roleId, icon }) => {
+
+    /*********************************/
+    const [visible, setVisible] = useState<boolean>(false)
+    const getRoleIpDetailList = useAjax((params) => getRoleIpDetailListApi(params))
+    /*********************************/
+
+    useEffect(() => {
+        if (visible) {
+            getRoleIpDetailList.run({ roleId })
+        }
+    }, [roleId, visible])
+
+    return <>
+        <a onClick={() => setVisible(true)}>{icon}</a>
+        {visible && <Modal
+            title={<strong>登录IP(角色ID:{roleId})</strong>}
+            visible={visible}
+            onCancel={() => setVisible(false)}
+            footer={null}
+            width={700}
+        >
+            <Table
+                columns={[
+                    {
+                        title: 'IP',
+                        dataIndex: 'ip',
+                        key: 'ip',
+                    },
+                    {
+                        title: 'IP角色数量',
+                        dataIndex: 'ipRoleCount',
+                        key: 'ipRoleCount',
+                    }
+                ]}
+                rowKey={'ip'}
+                dataSource={getRoleIpDetailList?.data?.records}
+                loading={getRoleIpDetailList?.loading}
+                size="small"
+                bordered
+                pagination={false}
+            />
+        </Modal>}
+    </>
+};
+
+export default React.memo(LoginIpDetails);

+ 107 - 0
src/pages/gsData/roleIpMonitor/regIpRoleDetails.tsx

@@ -0,0 +1,107 @@
+import { useAjax } from '@/Hook/useAjax';
+import { getRoleDetailListApi } from '@/services/gsData';
+import { Modal, Table } from 'antd';
+import React, { useEffect, useState } from 'react';
+
+
+/**
+ * 查询同ip角色详情列表
+ * @param param0 
+ * @returns 
+ */
+const RegIpRoleDetails: React.FC<{ ip: any, userId?: any, icon?: React.ReactNode }> = ({ ip, userId, icon }) => {
+
+    /*********************************/
+    const [visible, setVisible] = useState<boolean>(false)
+    const getRoleDetailList = useAjax((params) => getRoleDetailListApi(params))
+    /*********************************/
+
+    useEffect(() => {
+        if (visible) {
+            const params: { regIp: string, excludeUserId?: any } = { regIp: ip }
+            if (userId) {
+                params.excludeUserId = userId
+            }
+            getRoleDetailList.run(params)
+        }
+    }, [ip, userId, visible])
+
+    return <>
+        <a onClick={() => setVisible(true)}>{icon}</a>
+        {visible && <Modal
+            title={<strong>同IP下角色列表{userId ? '(排除同玩家)' : ''}({ip})</strong>}
+            visible={visible}
+            onCancel={() => setVisible(false)}
+            footer={null}
+            width={700}
+        >
+            <Table
+                columns={[
+                    {
+                        title: '角色名称',
+                        dataIndex: 'roleName',
+                        key: 'roleName',
+                        ellipsis: true
+                    },
+                    {
+                        title: '角色ID',
+                        dataIndex: 'roleId',
+                        key: 'roleId',
+                        align: 'center',
+                        ellipsis: true
+                    },
+                    {
+                        title: '玩家名称',
+                        dataIndex: 'userName',
+                        key: 'userName',
+                        align: 'center',
+                        ellipsis: true
+                    },
+                    {
+                        title: '玩家ID',
+                        dataIndex: 'userId',
+                        key: 'userId',
+                        align: 'center',
+                        ellipsis: true
+                    },
+                    {
+                        title: '游戏名称',
+                        dataIndex: 'gameName',
+                        key: 'gameName',
+                        align: 'center',
+                        ellipsis: true
+                    },
+                    {
+                        title: '游戏ID',
+                        dataIndex: 'gameId',
+                        key: 'gameId',
+                        align: 'center',
+                        ellipsis: true
+                    },
+                    {
+                        title: '区服名称',
+                        dataIndex: 'serverName',
+                        key: 'serverName',
+                        align: 'center',
+                        ellipsis: true
+                    },
+                    {
+                        title: '区服ID',
+                        dataIndex: 'serverId',
+                        key: 'serverId',
+                        align: 'center',
+                        ellipsis: true
+                    }
+                ]}
+                rowKey={'roleId'}
+                dataSource={getRoleDetailList?.data?.records}
+                loading={getRoleDetailList?.loading}
+                size="small"
+                bordered
+                pagination={false}
+            />
+        </Modal>}
+    </>
+};
+
+export default React.memo(RegIpRoleDetails);

+ 111 - 0
src/pages/gsData/roleIpMonitor/tableConfig.tsx

@@ -0,0 +1,111 @@
+import WidthEllipsis from "@/components/widthEllipsis"
+import LookRoleDetails from "@/pages/gameDataStatistics/player/role/lookRoleDetails"
+import { EyeOutlined } from "@ant-design/icons"
+import { Statistic } from "antd"
+import React from "react"
+import LoginIpDetails from "./loginIpDetails"
+import RegIpRoleDetails from "./regIpRoleDetails"
+
+function columns12(): { label: string, fieldSHow?: { label: string, saveField: string, defaultValue: any[], data: any[] }, data: any[] }[] {
+
+    return [
+        {
+            label: '基本信息',
+            data: [
+                {
+                    title: '角色名称', dataIndex: 'role_name', label: '基本信息', align: 'center', width: 85, default: 1,
+                    render: (a: string) => (<WidthEllipsis value={a} />)
+                },
+                {
+                    title: '角色ID', dataIndex: 'role_id', label: '基本信息', align: 'center', width: 85,
+                    render: (a: string) => (<WidthEllipsis value={a} />)
+                },
+                {
+                    title: '玩家名称', dataIndex: 'user_name', label: '基本信息', align: 'center', width: 90, default: 2,
+                    render: (a: string) => (<WidthEllipsis value={a} />)
+                },
+                {
+                    title: '玩家ID', dataIndex: 'user_id', label: '基本信息', align: 'center', width: 85,
+                    render: (a: string) => (<WidthEllipsis value={a} />)
+                },
+                {
+                    title: '游戏名称', dataIndex: 'game_name', label: '基本信息', align: 'center', width: 85, default: 3,
+                    render: (a: string) => (<WidthEllipsis value={a} />)
+                },
+                {
+                    title: '游戏ID', dataIndex: 'game_id', label: '基本信息', align: 'center', width: 85,
+                    render: (a: string) => (<WidthEllipsis value={a} />)
+                },
+                {
+                    title: '区服名称', dataIndex: 'server_name', label: '基本信息', align: 'center', width: 80, default: 4,
+                    render: (a: string) => (<WidthEllipsis value={a} />)
+                },
+                {
+                    title: '区服ID', dataIndex: 'server_id', label: '基本信息', align: 'center', width: 80,
+                    render: (a: string) => (<WidthEllipsis value={a} />)
+                },
+                {
+                    title: '角色创建时间', dataIndex: 'create_time', label: '基本信息', align: 'center', width: 125, default: 5, sorter: true,
+                    render: (a: string, b: any) => (<WidthEllipsis value={a} />)
+                },
+                {
+                    title: '角色等级', dataIndex: 'role_level', label: '基本信息', align: 'center', width: 60, sorter: true, default: 6,
+                    render: (a: string) => <Statistic value={a || 0} />
+                },
+                {
+                    title: '角色注册IP', dataIndex: 'ip', label: '基本信息', align: 'center', width: 100, default: 7,
+                    render: (a: string) => <Statistic value={a || 0} />
+                },
+                {
+                    title: '角色登录IP数量', dataIndex: 'ip_count', label: '基本信息', align: 'center', width: 65, sorter: true, default: 8,
+                    render: (a: number, b: any) => <Statistic value={a || 0} suffix={<LoginIpDetails roleId={b.role_id} icon={<EyeOutlined />} />} />
+                },
+                {
+                    title: '同玩家角色数量', dataIndex: 'user_role_count', label: '基本信息', align: 'center', width: 65, sorter: true, default: 9,
+                    render: (a: number, b: any) => <Statistic value={a || 0} valueStyle={a > 1 ? { color: '#52C41A' } : {}} suffix={<LookRoleDetails userId={b.association_user_id} icon={<EyeOutlined />} />} />
+                },
+                {
+                    title: '角色同IP的玩家数量', dataIndex: 'user_count', label: '基本信息', align: 'center', width: 70, sorter: true, default: 10,
+                    render: (a: string) => <Statistic value={a || 0} />
+                },
+                {
+                    title: '同IP的角色数量', dataIndex: 'role_count', label: '基本信息', align: 'center', width: 70, sorter: true, default: 11,
+                    render: (a: number, b: any) => <Statistic value={a || 0} suffix={<RegIpRoleDetails ip={b.ip} icon={<EyeOutlined />} />} />
+                },
+                {
+                    title: '同IP的角色数量(排除同玩家)', dataIndex: 'ip_role_count_filter', label: '基本信息', align: 'center', width: 90, sorter: true, default: 12,
+                    render: (a: number, b: any) => <Statistic value={a || 0} suffix={<RegIpRoleDetails ip={b.ip} userId={b?.user_id} icon={<EyeOutlined />} />} />
+                },
+                {
+                    title: '同玩家IP的角色数量(排除同玩家)', dataIndex: 'role_user_ip_count_filter', label: '基本信息', align: 'center', width: 100, sorter: true, default: 13,
+                    render: (a: string) => <Statistic value={a || 0} />
+                },
+                {
+                    title: '角色所在玩家登录过的所有IP的统计数量', dataIndex: 'role_user_ip_count', label: '基本信息', align: 'center', width: 100, sorter: true, default: 14,
+                    render: (a: string) => <Statistic value={a || 0} />
+                },
+                {
+                    title: '创角时间至今', dataIndex: 'createTimeDiff', label: '角色游戏数据', align: 'center', width: 130, default: 15,
+                    render: (_: any, b: any) => {
+                        if (b?.create_time) {
+                            let a = (new Date().getTime() / 1000) - (new Date(b?.create_time).getTime() / 1000)
+                            function secondsToDhms(seconds: any) {
+                                const days = Math.floor(seconds / (3600 * 24));
+                                const hours = Math.floor((seconds % (3600 * 24)) / 3600);
+                                const minutes = Math.floor((seconds % 3600) / 60);
+                                const remainingSeconds = seconds % 60;
+                                return <span style={{ fontSize: 12, color: days > 5 ? 'red' : '#000' }}>{`${days ? days + "天" : ''}${hours ? hours + "小时" : ''}${minutes ? minutes + "分" : ''}${remainingSeconds ? remainingSeconds.toFixed(0) + "秒" : ''}`}</span>
+                            }
+                            return a ? secondsToDhms(a) : '--'
+                        } else {
+                            return '--'
+                        }
+                    }
+                }
+
+            ]
+        }
+    ]
+}
+
+export default columns12

+ 3 - 0
src/pages/gsData/xjRoleGrade/index.tsx

@@ -82,6 +82,9 @@ const XjRoleGrade: React.FC = () => {
                     if (regPayIntervalTime?.length > 0 && (regPayIntervalTime[0] || regPayIntervalTime[1])) {
                     if (regPayIntervalTime?.length > 0 && (regPayIntervalTime[0] || regPayIntervalTime[1])) {
                         newQueryForm.roleLevelMin = regPayIntervalTime[0]
                         newQueryForm.roleLevelMin = regPayIntervalTime[0]
                         newQueryForm.roleLevelMax = regPayIntervalTime[1]
                         newQueryForm.roleLevelMax = regPayIntervalTime[1]
+                    } else {
+                        delete newQueryForm.roleLevelMin
+                        delete newQueryForm.roleLevelMax
                     }
                     }
                     setQueryForm({ ...newQueryForm, ...params })
                     setQueryForm({ ...newQueryForm, ...params })
                 }}
                 }}

+ 104 - 5
src/services/gsData/index.ts

@@ -104,7 +104,7 @@ export interface GetServerPayRetainedProps extends Paging, SortProps {
     serveStatus?: 1 | 2,     // 服务状态:1:服务中;2:服务完成
     serveStatus?: 1 | 2,     // 服务状态:1:服务中;2:服务完成
     serverIdList?: number[]  // 角色当前所在区服id列表
     serverIdList?: number[]  // 角色当前所在区服id列表
     serverStartBegin?: string, // 开服日期
     serverStartBegin?: string, // 开服日期
-    serverStartEnd?: string, 
+    serverStartEnd?: string,
 }
 }
 
 
 /**
 /**
@@ -136,7 +136,7 @@ export interface getRoleManageProps extends Paging, SortProps {
     parentGameId?: number,
     parentGameId?: number,
     superGameId?: number,
     superGameId?: number,
     roleCreateDayBegin?: string, // 角色创建日期开始
     roleCreateDayBegin?: string, // 角色创建日期开始
-    roleCreateDayEnd?: string, 
+    roleCreateDayEnd?: string,
     roleName?: string,
     roleName?: string,
     serveDayBegin?: string,  // 服务开始日期
     serveDayBegin?: string,  // 服务开始日期
     serveDayEnd?: string,    // 服务结束日期
     serveDayEnd?: string,    // 服务结束日期
@@ -157,7 +157,7 @@ export async function getRoleManageListApi(data: getRoleManageProps) {
 }
 }
 
 
 
 
-export interface GetServerManageProps extends Paging, SortProps{
+export interface GetServerManageProps extends Paging, SortProps {
     gsIdList?: number[],
     gsIdList?: number[],
     gsStatus?: 1 | 2,        // GS运营状态:1:独立运营;2:联合运营
     gsStatus?: 1 | 2,        // GS运营状态:1:独立运营;2:联合运营
     parentGameId?: number,
     parentGameId?: number,
@@ -167,7 +167,7 @@ export interface GetServerManageProps extends Paging, SortProps{
     serveStatus?: 1 | 2,     // 服务状态:1:服务中;2:服务完成
     serveStatus?: 1 | 2,     // 服务状态:1:服务中;2:服务完成
     serverIdList?: number[]  // 角色当前所在区服id列表
     serverIdList?: number[]  // 角色当前所在区服id列表
     serverStartBegin?: string, // 开服日期
     serverStartBegin?: string, // 开服日期
-    serverStartEnd?: string, 
+    serverStartEnd?: string,
 }
 }
 /**
 /**
  * 游戏区服管理
  * 游戏区服管理
@@ -182,7 +182,7 @@ export async function getServerManageListApi(data: GetServerManageProps) {
 }
 }
 
 
 
 
-export interface GetRoleLevelListProps extends Paging, SortProps{
+export interface GetRoleLevelListProps extends Paging, SortProps {
     roleName?: string
     roleName?: string
     roleId?: string
     roleId?: string
     gameName?: string
     gameName?: string
@@ -206,4 +206,103 @@ export async function getRoleLevelListApi(data: GetRoleLevelListProps) {
         method: 'POST',
         method: 'POST',
         data
         data
     });
     });
+}
+
+export interface GetRoleIpInfoListProps extends Paging, SortProps {
+    userId?: number
+    userName?: string
+    roleId?: number
+    roleName?: string
+    gameId?: number
+    gameName?: string
+    serverId?: number
+    serverName?: string
+    createTimeMin?: string
+    createTimeMax?: string
+    roleLevelMin?: number
+    roleLevelMax?: number
+    regIp?: string
+}
+/**
+ * 角色IP监控信息列表
+ * @param data 
+ * @returns 
+ */
+export async function getRoleIpInfoListApi(data: GetRoleIpInfoListProps) {
+    return request(api + `/gameData/role/ipInfo/listOfPage`, {
+        method: 'POST',
+        data
+    });
+}
+
+/**
+ * 游戏帮派详情
+ * @param data 
+ * @returns 
+ */
+export async function getGameCountryListApi(data: { gameId?: number, serverId?: number }) {
+    return request(api + `/gameData/role/gameCountryList`, {
+        method: 'POST',
+        data
+    });
+}
+
+export interface GetGameCountryDataListProps {
+    gameCountryList?: string[],
+    gameId?: number[]
+    serverId?: number[]
+}
+/**
+ * 游戏帮派数据列表
+ * @param data 
+ * @returns 
+ */
+export async function getGameCountryDataListApi(data: GetGameCountryDataListProps) {
+    return request(api + `/gameData/role/gameCountryDataList`, {
+        method: 'POST',
+        data
+    });
+}
+
+
+export interface getGameCountryRoleListProps extends Paging, SortProps {
+    country: string
+    serverId: string
+}
+/**
+ * 游戏帮派角色列表(分页)
+ * @param data 
+ * @returns 
+ */
+export async function getGameCountryRoleListApi(data: getGameCountryRoleListProps) {
+    return request(api + `/gameData/role/gameCountryRoleList`, {
+        method: 'POST',
+        data
+    });
+}
+
+
+/**
+ * 角色ip登录详情列表
+ * @param data 
+ * @returns 
+ */
+export async function getRoleIpDetailListApi(data: { roleId: string }) {
+    return request(api + `/gameData/role/roleIpDetail/listOfPage`, {
+        method: 'POST',
+        data
+    });
+}
+
+
+/**
+ * 同IP角色数量
+ * @param data 
+ * @returns 
+ */
+export async function getRoleDetailListApi(data: { regIp: string }) {
+    return request(api + `/gameData/role/roleDetail/listOfPage`, {
+        method: 'POST',
+        data
+    });
 }
 }

+ 20 - 4
src/utils/utils.ts

@@ -73,11 +73,11 @@ export const getChannelName = (name: string) => {
 // 输入文案时判断
 // 输入文案时判断
 export const txtLength = (value?: string) => {
 export const txtLength = (value?: string) => {
   if (value) {
   if (value) {
-      let length = value?.length
-      let text = value?.replace(/[\x00-\xff]/g, '')
-      return text?.length + Number(((length - text?.length) / 2).toFixed())
+    let length = value?.length
+    let text = value?.replace(/[\x00-\xff]/g, '')
+    return text?.length + Number(((length - text?.length) / 2).toFixed())
   } else {
   } else {
-      return 0
+    return 0
   }
   }
 }
 }
 
 
@@ -113,4 +113,20 @@ export const getNumber = (num1: number, num2: number, operator: '+' | '-' | '*'
     return '0.00'
     return '0.00'
   }
   }
   return Number(eval(`${num1.toFixed(10)}${operator}${num2.toFixed(10)}`).toFixed(10))
   return Number(eval(`${num1.toFixed(10)}${operator}${num2.toFixed(10)}`).toFixed(10))
+}
+
+// 数组分组
+export const groupBy = (array: any[], f: (item: any) => any[], isObject?: boolean) => {
+  const groups: any = {};
+  array.forEach(function (o) { //注意这里必须是forEach 大写
+    const group = JSON.stringify(f(o));
+    groups[group] = groups[group] || [];
+    groups[group].push(o);
+  });
+  if (isObject) {
+    return groups
+  }
+  return Object.keys(groups).map(function (group) {
+    return groups[group];
+  });
 }
 }