useRoutingColumns.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import { useMemo } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Button, Dropdown, Switch, Tag } from 'antd';
  4. import {
  5. MoreOutlined,
  6. EditOutlined,
  7. DeleteOutlined,
  8. ExportOutlined,
  9. ClusterOutlined,
  10. ArrowUpOutlined,
  11. ArrowDownOutlined,
  12. HolderOutlined,
  13. } from '@ant-design/icons';
  14. import type { ColumnsType } from 'antd/es/table';
  15. import { useInboundOptions } from '@/api/queries/useInboundOptions';
  16. import CriterionRow from './CriterionRow';
  17. import { buildRemarkByTag, formatInboundTagList, inboundTagsDisplayTitle, isApiRule } from './helpers';
  18. import type { RuleRow } from './types';
  19. interface RoutingColumnsParams {
  20. isMobile: boolean;
  21. rowsLength: number;
  22. showSource: boolean;
  23. showBalancer: boolean;
  24. onHandlePointerDown: (idx: number, ev: React.PointerEvent) => void;
  25. openEdit: (idx: number) => void;
  26. moveUp: (idx: number) => void;
  27. moveDown: (idx: number) => void;
  28. confirmDelete: (idx: number) => void;
  29. toggleRule: (idx: number, enabled: boolean) => void;
  30. }
  31. export function useRoutingColumns({
  32. isMobile,
  33. rowsLength,
  34. showSource,
  35. showBalancer,
  36. onHandlePointerDown,
  37. openEdit,
  38. moveUp,
  39. moveDown,
  40. confirmDelete,
  41. toggleRule,
  42. }: RoutingColumnsParams): ColumnsType<RuleRow> {
  43. const { t } = useTranslation();
  44. const { data: inboundOptions } = useInboundOptions();
  45. const remarkByTag = useMemo(() => buildRemarkByTag(inboundOptions || []), [inboundOptions]);
  46. return useMemo(
  47. () => [
  48. {
  49. title: '#',
  50. align: 'center',
  51. width: 60,
  52. key: 'index',
  53. render: (_v, _r, index) => (
  54. <div className="action-cell" style={{ justifyContent: 'center' }}>
  55. <HolderOutlined
  56. className="drag-handle"
  57. title={t('pages.xray.routing.dragToReorder')}
  58. onPointerDown={(ev: React.PointerEvent) => onHandlePointerDown(index, ev)}
  59. />
  60. <span className="row-index">{index + 1}</span>
  61. </div>
  62. ),
  63. },
  64. {
  65. title: t('pages.clients.actions'),
  66. align: 'center',
  67. width: 80,
  68. key: 'action',
  69. render: (_v, _r, index) => (
  70. <div className={!isMobile ? 'action-buttons' : ''} style={{ justifyContent: 'center', margin: 0 }}>
  71. {!isMobile && (
  72. <Button shape="circle" size="small" icon={<EditOutlined />} onClick={() => openEdit(index)} />
  73. )}
  74. <Dropdown
  75. trigger={['click']}
  76. menu={{
  77. items: [
  78. ...(isMobile
  79. ? [{ key: 'edit', label: <><EditOutlined /> {t('edit')}</>, onClick: () => openEdit(index) }]
  80. : []),
  81. { key: 'up', label: <ArrowUpOutlined />, disabled: index === 0, onClick: () => moveUp(index) },
  82. {
  83. key: 'down',
  84. label: <ArrowDownOutlined />,
  85. disabled: index === rowsLength - 1,
  86. onClick: () => moveDown(index),
  87. },
  88. { key: 'del', danger: true, label: <><DeleteOutlined /> {t('delete')}</>, onClick: () => confirmDelete(index) },
  89. ],
  90. }}
  91. >
  92. <Button shape="circle" size="small" icon={<MoreOutlined />} />
  93. </Dropdown>
  94. </div>
  95. ),
  96. },
  97. {
  98. title: t('enable'),
  99. align: 'center',
  100. width: 80,
  101. key: 'enabled',
  102. render: (_v, _r, index) => (
  103. <Switch
  104. size="small"
  105. checked={_r.enabled !== false}
  106. onChange={(checked) => toggleRule(index, checked)}
  107. disabled={isApiRule(_r)}
  108. />
  109. ),
  110. },
  111. {
  112. title: t('pages.xray.rules.source'),
  113. align: 'left',
  114. width: 180,
  115. key: 'source',
  116. hidden: !showSource,
  117. render: (_v, record) => (
  118. <div className="criterion-flow">
  119. {record.sourceIP && <CriterionRow label="IP" value={record.sourceIP} title={`Source IP: ${record.sourceIP}`} />}
  120. {record.sourcePort && <CriterionRow label="Port" value={record.sourcePort} title={`Source port: ${record.sourcePort}`} />}
  121. {record.vlessRoute && <CriterionRow label="VLESS" value={record.vlessRoute} title={`VLESS route: ${record.vlessRoute}`} />}
  122. {!record.sourceIP && !record.sourcePort && !record.vlessRoute && <span className="criterion-empty">—</span>}
  123. </div>
  124. ),
  125. },
  126. {
  127. title: t('pages.inbounds.network'),
  128. align: 'left',
  129. width: 180,
  130. key: 'network',
  131. render: (_v, record) => (
  132. <div className="criterion-flow">
  133. {record.network && <CriterionRow label="L4" value={record.network} title={`L4: ${record.network}`} />}
  134. {record.protocol && <CriterionRow label="Protocol" value={record.protocol} title={`Protocol: ${record.protocol}`} />}
  135. {record.attrs && <CriterionRow label="Attrs" value={record.attrs} title={`Attrs: ${record.attrs}`} />}
  136. {!record.network && !record.protocol && !record.attrs && <span className="criterion-empty">—</span>}
  137. </div>
  138. ),
  139. },
  140. {
  141. title: t('pages.xray.rules.dest'),
  142. align: 'left',
  143. width: 200,
  144. key: 'destination',
  145. render: (_v, record) => (
  146. <div className="criterion-flow">
  147. {record.ip && <CriterionRow label="IP" value={record.ip} title={`Destination IP: ${record.ip}`} />}
  148. {record.domain && <CriterionRow label="Domain" value={record.domain} title={`Domain: ${record.domain}`} />}
  149. {record.port && <CriterionRow label="Port" value={record.port} title={`Destination port: ${record.port}`} />}
  150. {!record.ip && !record.domain && !record.port && <span className="criterion-empty">—</span>}
  151. </div>
  152. ),
  153. },
  154. {
  155. title: t('pages.xray.Inbounds'),
  156. align: 'left',
  157. width: 180,
  158. key: 'inbound',
  159. render: (_v, record) => {
  160. const inboundParts = formatInboundTagList(record.inboundTag, remarkByTag);
  161. return (
  162. <div className="criterion-flow">
  163. {inboundParts.length > 0 && (
  164. <CriterionRow
  165. label="Tag"
  166. values={inboundParts}
  167. title={`Inbound tag: ${inboundTagsDisplayTitle(record.inboundTag, remarkByTag) ?? inboundParts.join(', ')}`}
  168. />
  169. )}
  170. {record.user && <CriterionRow label="User" value={record.user} title={`User: ${record.user}`} />}
  171. {inboundParts.length === 0 && !record.user && <span className="criterion-empty">—</span>}
  172. </div>
  173. );
  174. },
  175. },
  176. {
  177. title: t('pages.xray.Outbounds'),
  178. align: 'left',
  179. width: 170,
  180. key: 'outbound',
  181. render: (_v, record) =>
  182. record.outboundTag ? (
  183. <div className="target-row">
  184. <ExportOutlined className="target-icon" />
  185. <Tag color="green">{record.outboundTag}</Tag>
  186. </div>
  187. ) : (
  188. <span className="criterion-empty">—</span>
  189. ),
  190. },
  191. {
  192. title: t('pages.xray.Balancers'),
  193. align: 'left',
  194. width: 150,
  195. key: 'balancer',
  196. hidden: !showBalancer,
  197. render: (_v, record) =>
  198. record.balancerTag ? (
  199. <div className="target-row">
  200. <ClusterOutlined className="target-icon" />
  201. <Tag color="purple">{record.balancerTag}</Tag>
  202. </div>
  203. ) : (
  204. <span className="criterion-empty">—</span>
  205. ),
  206. },
  207. ],
  208. [t, isMobile, rowsLength, showSource, showBalancer, remarkByTag, onHandlePointerDown, openEdit, moveUp, moveDown, confirmDelete, toggleRule],
  209. );
  210. }