| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- import { useMemo } from 'react';
- import { useTranslation } from 'react-i18next';
- import { Button, Dropdown, Switch, Tag } from 'antd';
- import {
- MoreOutlined,
- EditOutlined,
- DeleteOutlined,
- ExportOutlined,
- ClusterOutlined,
- ArrowUpOutlined,
- ArrowDownOutlined,
- HolderOutlined,
- } from '@ant-design/icons';
- import type { ColumnsType } from 'antd/es/table';
- import { useInboundOptions } from '@/api/queries/useInboundOptions';
- import CriterionRow from './CriterionRow';
- import { buildRemarkByTag, formatInboundTagList, inboundTagsDisplayTitle, isApiRule } from './helpers';
- import type { RuleRow } from './types';
- interface RoutingColumnsParams {
- isMobile: boolean;
- rowsLength: number;
- showSource: boolean;
- showBalancer: boolean;
- onHandlePointerDown: (idx: number, ev: React.PointerEvent) => void;
- openEdit: (idx: number) => void;
- moveUp: (idx: number) => void;
- moveDown: (idx: number) => void;
- confirmDelete: (idx: number) => void;
- toggleRule: (idx: number, enabled: boolean) => void;
- }
- export function useRoutingColumns({
- isMobile,
- rowsLength,
- showSource,
- showBalancer,
- onHandlePointerDown,
- openEdit,
- moveUp,
- moveDown,
- confirmDelete,
- toggleRule,
- }: RoutingColumnsParams): ColumnsType<RuleRow> {
- const { t } = useTranslation();
- const { data: inboundOptions } = useInboundOptions();
- const remarkByTag = useMemo(() => buildRemarkByTag(inboundOptions || []), [inboundOptions]);
- return useMemo(
- () => [
- {
- title: '#',
- align: 'center',
- width: 60,
- key: 'index',
- render: (_v, _r, index) => (
- <div className="action-cell" style={{ justifyContent: 'center' }}>
- <HolderOutlined
- className="drag-handle"
- title={t('pages.xray.routing.dragToReorder')}
- onPointerDown={(ev: React.PointerEvent) => onHandlePointerDown(index, ev)}
- />
- <span className="row-index">{index + 1}</span>
- </div>
- ),
- },
- {
- title: t('pages.clients.actions'),
- align: 'center',
- width: 80,
- key: 'action',
- render: (_v, _r, index) => (
- <div className={!isMobile ? 'action-buttons' : ''} style={{ justifyContent: 'center', margin: 0 }}>
- {!isMobile && (
- <Button shape="circle" size="small" icon={<EditOutlined />} onClick={() => openEdit(index)} />
- )}
- <Dropdown
- trigger={['click']}
- menu={{
- items: [
- ...(isMobile
- ? [{ key: 'edit', label: <><EditOutlined /> {t('edit')}</>, onClick: () => openEdit(index) }]
- : []),
- { key: 'up', label: <ArrowUpOutlined />, disabled: index === 0, onClick: () => moveUp(index) },
- {
- key: 'down',
- label: <ArrowDownOutlined />,
- disabled: index === rowsLength - 1,
- onClick: () => moveDown(index),
- },
- { key: 'del', danger: true, label: <><DeleteOutlined /> {t('delete')}</>, onClick: () => confirmDelete(index) },
- ],
- }}
- >
- <Button shape="circle" size="small" icon={<MoreOutlined />} />
- </Dropdown>
- </div>
- ),
- },
- {
- title: t('enable'),
- align: 'center',
- width: 80,
- key: 'enabled',
- render: (_v, _r, index) => (
- <Switch
- size="small"
- checked={_r.enabled !== false}
- onChange={(checked) => toggleRule(index, checked)}
- disabled={isApiRule(_r)}
- />
- ),
- },
- {
- title: t('pages.xray.rules.source'),
- align: 'left',
- width: 180,
- key: 'source',
- hidden: !showSource,
- render: (_v, record) => (
- <div className="criterion-flow">
- {record.sourceIP && <CriterionRow label="IP" value={record.sourceIP} title={`Source IP: ${record.sourceIP}`} />}
- {record.sourcePort && <CriterionRow label="Port" value={record.sourcePort} title={`Source port: ${record.sourcePort}`} />}
- {record.vlessRoute && <CriterionRow label="VLESS" value={record.vlessRoute} title={`VLESS route: ${record.vlessRoute}`} />}
- {!record.sourceIP && !record.sourcePort && !record.vlessRoute && <span className="criterion-empty">—</span>}
- </div>
- ),
- },
- {
- title: t('pages.inbounds.network'),
- align: 'left',
- width: 180,
- key: 'network',
- render: (_v, record) => (
- <div className="criterion-flow">
- {record.network && <CriterionRow label="L4" value={record.network} title={`L4: ${record.network}`} />}
- {record.protocol && <CriterionRow label="Protocol" value={record.protocol} title={`Protocol: ${record.protocol}`} />}
- {record.attrs && <CriterionRow label="Attrs" value={record.attrs} title={`Attrs: ${record.attrs}`} />}
- {!record.network && !record.protocol && !record.attrs && <span className="criterion-empty">—</span>}
- </div>
- ),
- },
- {
- title: t('pages.xray.rules.dest'),
- align: 'left',
- width: 200,
- key: 'destination',
- render: (_v, record) => (
- <div className="criterion-flow">
- {record.ip && <CriterionRow label="IP" value={record.ip} title={`Destination IP: ${record.ip}`} />}
- {record.domain && <CriterionRow label="Domain" value={record.domain} title={`Domain: ${record.domain}`} />}
- {record.port && <CriterionRow label="Port" value={record.port} title={`Destination port: ${record.port}`} />}
- {!record.ip && !record.domain && !record.port && <span className="criterion-empty">—</span>}
- </div>
- ),
- },
- {
- title: t('pages.xray.Inbounds'),
- align: 'left',
- width: 180,
- key: 'inbound',
- render: (_v, record) => {
- const inboundParts = formatInboundTagList(record.inboundTag, remarkByTag);
- return (
- <div className="criterion-flow">
- {inboundParts.length > 0 && (
- <CriterionRow
- label="Tag"
- values={inboundParts}
- title={`Inbound tag: ${inboundTagsDisplayTitle(record.inboundTag, remarkByTag) ?? inboundParts.join(', ')}`}
- />
- )}
- {record.user && <CriterionRow label="User" value={record.user} title={`User: ${record.user}`} />}
- {inboundParts.length === 0 && !record.user && <span className="criterion-empty">—</span>}
- </div>
- );
- },
- },
- {
- title: t('pages.xray.Outbounds'),
- align: 'left',
- width: 170,
- key: 'outbound',
- render: (_v, record) =>
- record.outboundTag ? (
- <div className="target-row">
- <ExportOutlined className="target-icon" />
- <Tag color="green">{record.outboundTag}</Tag>
- </div>
- ) : (
- <span className="criterion-empty">—</span>
- ),
- },
- {
- title: t('pages.xray.Balancers'),
- align: 'left',
- width: 150,
- key: 'balancer',
- hidden: !showBalancer,
- render: (_v, record) =>
- record.balancerTag ? (
- <div className="target-row">
- <ClusterOutlined className="target-icon" />
- <Tag color="purple">{record.balancerTag}</Tag>
- </div>
- ) : (
- <span className="criterion-empty">—</span>
- ),
- },
- ],
- [t, isMobile, rowsLength, showSource, showBalancer, remarkByTag, onHandlePointerDown, openEdit, moveUp, moveDown, confirmDelete, toggleRule],
- );
- }
|