UsersColumnDefs.jsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact [email protected]
  14. */
  15. import React from 'react';
  16. import {
  17. Button,
  18. Space,
  19. Tag,
  20. Tooltip,
  21. Progress,
  22. Popover,
  23. Typography,
  24. } from '@douyinfe/semi-ui';
  25. import { renderGroup, renderNumber, renderQuota } from '../../../helpers';
  26. /**
  27. * Render user role
  28. */
  29. const renderRole = (role, t) => {
  30. switch (role) {
  31. case 1:
  32. return (
  33. <Tag color='blue' shape='circle'>
  34. {t('普通用户')}
  35. </Tag>
  36. );
  37. case 10:
  38. return (
  39. <Tag color='yellow' shape='circle'>
  40. {t('管理员')}
  41. </Tag>
  42. );
  43. case 100:
  44. return (
  45. <Tag color='orange' shape='circle'>
  46. {t('超级管理员')}
  47. </Tag>
  48. );
  49. default:
  50. return (
  51. <Tag color='red' shape='circle'>
  52. {t('未知身份')}
  53. </Tag>
  54. );
  55. }
  56. };
  57. /**
  58. * Render username with remark
  59. */
  60. const renderUsername = (text, record) => {
  61. const remark = record.remark;
  62. if (!remark) {
  63. return <span>{text}</span>;
  64. }
  65. const maxLen = 10;
  66. const displayRemark =
  67. remark.length > maxLen ? remark.slice(0, maxLen) + '…' : remark;
  68. return (
  69. <Space spacing={2}>
  70. <span>{text}</span>
  71. <Tooltip content={remark} position='top' showArrow>
  72. <Tag color='white' shape='circle' className='!text-xs'>
  73. <div className='flex items-center gap-1'>
  74. <div
  75. className='w-2 h-2 flex-shrink-0 rounded-full'
  76. style={{ backgroundColor: '#10b981' }}
  77. />
  78. {displayRemark}
  79. </div>
  80. </Tag>
  81. </Tooltip>
  82. </Space>
  83. );
  84. };
  85. /**
  86. * Render user statistics
  87. */
  88. const renderStatistics = (text, record, showEnableDisableModal, t) => {
  89. const isDeleted = record.DeletedAt !== null;
  90. // Determine tag text & color like original status column
  91. let tagColor = 'grey';
  92. let tagText = t('未知状态');
  93. if (isDeleted) {
  94. tagColor = 'red';
  95. tagText = t('已注销');
  96. } else if (record.status === 1) {
  97. tagColor = 'green';
  98. tagText = t('已启用');
  99. } else if (record.status === 2) {
  100. tagColor = 'red';
  101. tagText = t('已禁用');
  102. }
  103. const content = (
  104. <Tag color={tagColor} shape='circle' size='small'>
  105. {tagText}
  106. </Tag>
  107. );
  108. const tooltipContent = (
  109. <div className='text-xs'>
  110. <div>
  111. {t('调用次数')}: {renderNumber(record.request_count)}
  112. </div>
  113. </div>
  114. );
  115. return (
  116. <Tooltip content={tooltipContent} position='top'>
  117. {content}
  118. </Tooltip>
  119. );
  120. };
  121. // Render separate quota usage column
  122. const renderQuotaUsage = (text, record, t) => {
  123. const { Paragraph } = Typography;
  124. const used = parseInt(record.used_quota) || 0;
  125. const remain = parseInt(record.quota) || 0;
  126. const total = used + remain;
  127. const percent = total > 0 ? (remain / total) * 100 : 0;
  128. const popoverContent = (
  129. <div className='text-xs p-2'>
  130. <Paragraph copyable={{ content: renderQuota(used) }}>
  131. {t('已用额度')}: {renderQuota(used)}
  132. </Paragraph>
  133. <Paragraph copyable={{ content: renderQuota(remain) }}>
  134. {t('剩余额度')}: {renderQuota(remain)} ({percent.toFixed(0)}%)
  135. </Paragraph>
  136. <Paragraph copyable={{ content: renderQuota(total) }}>
  137. {t('总额度')}: {renderQuota(total)}
  138. </Paragraph>
  139. </div>
  140. );
  141. return (
  142. <Popover content={popoverContent} position='top'>
  143. <Tag color='white' shape='circle'>
  144. <div className='flex flex-col items-end'>
  145. <span className='text-xs leading-none'>{`${renderQuota(remain)} / ${renderQuota(total)}`}</span>
  146. <Progress
  147. percent={percent}
  148. aria-label='quota usage'
  149. format={() => `${percent.toFixed(0)}%`}
  150. style={{ width: '100%', marginTop: '1px', marginBottom: 0 }}
  151. />
  152. </div>
  153. </Tag>
  154. </Popover>
  155. );
  156. };
  157. /**
  158. * Render invite information
  159. */
  160. const renderInviteInfo = (text, record, t) => {
  161. return (
  162. <div>
  163. <Space spacing={1}>
  164. <Tag color='white' shape='circle' className='!text-xs'>
  165. {t('邀请')}: {renderNumber(record.aff_count)}
  166. </Tag>
  167. <Tag color='white' shape='circle' className='!text-xs'>
  168. {t('收益')}: {renderQuota(record.aff_history_quota)}
  169. </Tag>
  170. <Tag color='white' shape='circle' className='!text-xs'>
  171. {record.inviter_id === 0
  172. ? t('无邀请人')
  173. : `${t('邀请人')}: ${record.inviter_id}`}
  174. </Tag>
  175. </Space>
  176. </div>
  177. );
  178. };
  179. /**
  180. * Render operations column
  181. */
  182. const renderOperations = (
  183. text,
  184. record,
  185. {
  186. setEditingUser,
  187. setShowEditUser,
  188. showPromoteModal,
  189. showDemoteModal,
  190. showEnableDisableModal,
  191. showDeleteModal,
  192. showResetPasskeyModal,
  193. showResetTwoFAModal,
  194. t,
  195. },
  196. ) => {
  197. if (record.DeletedAt !== null) {
  198. return <></>;
  199. }
  200. return (
  201. <Space>
  202. {record.status === 1 ? (
  203. <Button
  204. type='danger'
  205. size='small'
  206. onClick={() => showEnableDisableModal(record, 'disable')}
  207. >
  208. {t('禁用')}
  209. </Button>
  210. ) : (
  211. <Button
  212. size='small'
  213. onClick={() => showEnableDisableModal(record, 'enable')}
  214. >
  215. {t('启用')}
  216. </Button>
  217. )}
  218. <Button
  219. type='tertiary'
  220. size='small'
  221. onClick={() => {
  222. setEditingUser(record);
  223. setShowEditUser(true);
  224. }}
  225. >
  226. {t('编辑')}
  227. </Button>
  228. <Button
  229. type='warning'
  230. size='small'
  231. onClick={() => showPromoteModal(record)}
  232. >
  233. {t('提升')}
  234. </Button>
  235. <Button
  236. type='secondary'
  237. size='small'
  238. onClick={() => showDemoteModal(record)}
  239. >
  240. {t('降级')}
  241. </Button>
  242. <Button
  243. type='warning'
  244. size='small'
  245. onClick={() => showResetPasskeyModal(record)}
  246. >
  247. {t('重置 Passkey')}
  248. </Button>
  249. <Button
  250. type='warning'
  251. size='small'
  252. onClick={() => showResetTwoFAModal(record)}
  253. >
  254. {t('重置 2FA')}
  255. </Button>
  256. <Button
  257. type='danger'
  258. size='small'
  259. onClick={() => showDeleteModal(record)}
  260. >
  261. {t('注销')}
  262. </Button>
  263. </Space>
  264. );
  265. };
  266. /**
  267. * Get users table column definitions
  268. */
  269. export const getUsersColumns = ({
  270. t,
  271. setEditingUser,
  272. setShowEditUser,
  273. showPromoteModal,
  274. showDemoteModal,
  275. showEnableDisableModal,
  276. showDeleteModal,
  277. showResetPasskeyModal,
  278. showResetTwoFAModal,
  279. }) => {
  280. return [
  281. {
  282. title: 'ID',
  283. dataIndex: 'id',
  284. },
  285. {
  286. title: t('用户名'),
  287. dataIndex: 'username',
  288. render: (text, record) => renderUsername(text, record),
  289. },
  290. {
  291. title: t('状态'),
  292. dataIndex: 'info',
  293. render: (text, record, index) =>
  294. renderStatistics(text, record, showEnableDisableModal, t),
  295. },
  296. {
  297. title: t('剩余额度/总额度'),
  298. key: 'quota_usage',
  299. render: (text, record) => renderQuotaUsage(text, record, t),
  300. },
  301. {
  302. title: t('分组'),
  303. dataIndex: 'group',
  304. render: (text, record, index) => {
  305. return <div>{renderGroup(text)}</div>;
  306. },
  307. },
  308. {
  309. title: t('角色'),
  310. dataIndex: 'role',
  311. render: (text, record, index) => {
  312. return <div>{renderRole(text, t)}</div>;
  313. },
  314. },
  315. {
  316. title: t('邀请信息'),
  317. dataIndex: 'invite',
  318. render: (text, record, index) => renderInviteInfo(text, record, t),
  319. },
  320. {
  321. title: '',
  322. dataIndex: 'operate',
  323. fixed: 'right',
  324. width: 200,
  325. render: (text, record, index) =>
  326. renderOperations(text, record, {
  327. setEditingUser,
  328. setShowEditUser,
  329. showPromoteModal,
  330. showDemoteModal,
  331. showEnableDisableModal,
  332. showDeleteModal,
  333. showResetPasskeyModal,
  334. showResetTwoFAModal,
  335. t,
  336. }),
  337. },
  338. ];
  339. };