UsersColumnDefs.jsx 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  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 support@quantumnous.com
  14. */
  15. import React from 'react';
  16. import {
  17. Button,
  18. Space,
  19. Tag,
  20. Tooltip,
  21. Progress,
  22. Popover,
  23. Typography,
  24. Dropdown,
  25. } from '@douyinfe/semi-ui';
  26. import { IconMore } from '@douyinfe/semi-icons';
  27. import {
  28. renderGroup,
  29. renderNumber,
  30. renderQuota,
  31. timestamp2string,
  32. } from '../../../helpers';
  33. const renderTimestamp = (text) => (text ? timestamp2string(text) : '-');
  34. /**
  35. * Render user role
  36. */
  37. const renderRole = (role, t) => {
  38. switch (role) {
  39. case 1:
  40. return (
  41. <Tag color='blue' shape='circle'>
  42. {t('普通用户')}
  43. </Tag>
  44. );
  45. case 10:
  46. return (
  47. <Tag color='yellow' shape='circle'>
  48. {t('管理员')}
  49. </Tag>
  50. );
  51. case 100:
  52. return (
  53. <Tag color='orange' shape='circle'>
  54. {t('超级管理员')}
  55. </Tag>
  56. );
  57. default:
  58. return (
  59. <Tag color='red' shape='circle'>
  60. {t('未知身份')}
  61. </Tag>
  62. );
  63. }
  64. };
  65. /**
  66. * Render username with remark
  67. */
  68. const renderUsername = (text, record) => {
  69. const remark = record.remark;
  70. if (!remark) {
  71. return <span>{text}</span>;
  72. }
  73. const maxLen = 10;
  74. const displayRemark =
  75. remark.length > maxLen ? remark.slice(0, maxLen) + '…' : remark;
  76. return (
  77. <Space spacing={2}>
  78. <span>{text}</span>
  79. <Tooltip content={remark} position='top' showArrow>
  80. <Tag color='white' shape='circle' className='!text-xs'>
  81. <div className='flex items-center gap-1'>
  82. <div
  83. className='w-2 h-2 flex-shrink-0 rounded-full'
  84. style={{ backgroundColor: '#10b981' }}
  85. />
  86. {displayRemark}
  87. </div>
  88. </Tag>
  89. </Tooltip>
  90. </Space>
  91. );
  92. };
  93. /**
  94. * Render user statistics
  95. */
  96. const renderStatistics = (text, record, showEnableDisableModal, t) => {
  97. const isDeleted = record.DeletedAt !== null;
  98. // Determine tag text & color like original status column
  99. let tagColor = 'grey';
  100. let tagText = t('未知状态');
  101. if (isDeleted) {
  102. tagColor = 'red';
  103. tagText = t('已注销');
  104. } else if (record.status === 1) {
  105. tagColor = 'green';
  106. tagText = t('已启用');
  107. } else if (record.status === 2) {
  108. tagColor = 'red';
  109. tagText = t('已禁用');
  110. }
  111. const content = (
  112. <Tag color={tagColor} shape='circle' size='small'>
  113. {tagText}
  114. </Tag>
  115. );
  116. const tooltipContent = (
  117. <div className='text-xs'>
  118. <div>
  119. {t('调用次数')}: {renderNumber(record.request_count)}
  120. </div>
  121. </div>
  122. );
  123. return (
  124. <Tooltip content={tooltipContent} position='top'>
  125. {content}
  126. </Tooltip>
  127. );
  128. };
  129. // Render separate quota usage column
  130. const renderQuotaUsage = (text, record, t) => {
  131. const { Paragraph } = Typography;
  132. const used = parseInt(record.used_quota) || 0;
  133. const remain = parseInt(record.quota) || 0;
  134. const total = used + remain;
  135. const percent = total > 0 ? (remain / total) * 100 : 0;
  136. const popoverContent = (
  137. <div className='text-xs p-2'>
  138. <Paragraph copyable={{ content: renderQuota(used) }}>
  139. {t('已用额度')}: {renderQuota(used)}
  140. </Paragraph>
  141. <Paragraph copyable={{ content: renderQuota(remain) }}>
  142. {t('剩余额度')}: {renderQuota(remain)} ({percent.toFixed(0)}%)
  143. </Paragraph>
  144. <Paragraph copyable={{ content: renderQuota(total) }}>
  145. {t('总额度')}: {renderQuota(total)}
  146. </Paragraph>
  147. </div>
  148. );
  149. return (
  150. <Popover content={popoverContent} position='top'>
  151. <Tag color='white' shape='circle'>
  152. <div className='flex flex-col items-end'>
  153. <span className='text-xs leading-none'>{`${renderQuota(remain)} / ${renderQuota(total)}`}</span>
  154. <Progress
  155. percent={percent}
  156. aria-label='quota usage'
  157. format={() => `${percent.toFixed(0)}%`}
  158. style={{ width: '100%', marginTop: '1px', marginBottom: 0 }}
  159. />
  160. </div>
  161. </Tag>
  162. </Popover>
  163. );
  164. };
  165. /**
  166. * Render invite information
  167. */
  168. const renderInviteInfo = (text, record, t) => {
  169. return (
  170. <div>
  171. <Space spacing={1}>
  172. <Tag color='white' shape='circle' className='!text-xs'>
  173. {t('邀请')}: {renderNumber(record.aff_count)}
  174. </Tag>
  175. <Tag color='white' shape='circle' className='!text-xs'>
  176. {t('收益')}: {renderQuota(record.aff_history_quota)}
  177. </Tag>
  178. <Tag color='white' shape='circle' className='!text-xs'>
  179. {record.inviter_id === 0
  180. ? t('无邀请人')
  181. : `${t('邀请人')}: ${record.inviter_id}`}
  182. </Tag>
  183. </Space>
  184. </div>
  185. );
  186. };
  187. /**
  188. * Render operations column
  189. */
  190. const renderOperations = (
  191. text,
  192. record,
  193. {
  194. setEditingUser,
  195. setShowEditUser,
  196. showPromoteModal,
  197. showDemoteModal,
  198. showEnableDisableModal,
  199. showDeleteModal,
  200. showResetPasskeyModal,
  201. showResetTwoFAModal,
  202. showUserSubscriptionsModal,
  203. t,
  204. },
  205. ) => {
  206. if (record.DeletedAt !== null) {
  207. return <></>;
  208. }
  209. const moreMenu = [
  210. {
  211. node: 'item',
  212. name: t('订阅管理'),
  213. onClick: () => showUserSubscriptionsModal(record),
  214. },
  215. {
  216. node: 'divider',
  217. },
  218. {
  219. node: 'item',
  220. name: t('重置 Passkey'),
  221. onClick: () => showResetPasskeyModal(record),
  222. },
  223. {
  224. node: 'item',
  225. name: t('重置 2FA'),
  226. onClick: () => showResetTwoFAModal(record),
  227. },
  228. {
  229. node: 'divider',
  230. },
  231. {
  232. node: 'item',
  233. name: t('注销'),
  234. type: 'danger',
  235. onClick: () => showDeleteModal(record),
  236. },
  237. ];
  238. return (
  239. <Space>
  240. {record.status === 1 ? (
  241. <Button
  242. type='danger'
  243. size='small'
  244. onClick={() => showEnableDisableModal(record, 'disable')}
  245. >
  246. {t('禁用')}
  247. </Button>
  248. ) : (
  249. <Button
  250. size='small'
  251. onClick={() => showEnableDisableModal(record, 'enable')}
  252. >
  253. {t('启用')}
  254. </Button>
  255. )}
  256. <Button
  257. type='tertiary'
  258. size='small'
  259. onClick={() => {
  260. setEditingUser(record);
  261. setShowEditUser(true);
  262. }}
  263. >
  264. {t('编辑')}
  265. </Button>
  266. <Button
  267. type='warning'
  268. size='small'
  269. onClick={() => showPromoteModal(record)}
  270. >
  271. {t('提升')}
  272. </Button>
  273. <Button
  274. type='secondary'
  275. size='small'
  276. onClick={() => showDemoteModal(record)}
  277. >
  278. {t('降级')}
  279. </Button>
  280. <Dropdown menu={moreMenu} trigger='click' position='bottomRight'>
  281. <Button type='tertiary' size='small' icon={<IconMore />} />
  282. </Dropdown>
  283. </Space>
  284. );
  285. };
  286. /**
  287. * Get users table column definitions
  288. */
  289. export const getUsersColumns = ({
  290. t,
  291. setEditingUser,
  292. setShowEditUser,
  293. showPromoteModal,
  294. showDemoteModal,
  295. showEnableDisableModal,
  296. showDeleteModal,
  297. showResetPasskeyModal,
  298. showResetTwoFAModal,
  299. showUserSubscriptionsModal,
  300. }) => {
  301. return [
  302. {
  303. title: 'ID',
  304. dataIndex: 'id',
  305. },
  306. {
  307. title: t('用户名'),
  308. dataIndex: 'username',
  309. render: (text, record) => renderUsername(text, record),
  310. },
  311. {
  312. title: t('状态'),
  313. dataIndex: 'info',
  314. render: (text, record, index) =>
  315. renderStatistics(text, record, showEnableDisableModal, t),
  316. },
  317. {
  318. title: t('剩余额度/总额度'),
  319. key: 'quota_usage',
  320. render: (text, record) => renderQuotaUsage(text, record, t),
  321. },
  322. {
  323. title: t('分组'),
  324. dataIndex: 'group',
  325. render: (text, record, index) => {
  326. return <div>{renderGroup(text)}</div>;
  327. },
  328. },
  329. {
  330. title: t('角色'),
  331. dataIndex: 'role',
  332. render: (text, record, index) => {
  333. return <div>{renderRole(text, t)}</div>;
  334. },
  335. },
  336. {
  337. title: t('邀请信息'),
  338. dataIndex: 'invite',
  339. render: (text, record, index) => renderInviteInfo(text, record, t),
  340. },
  341. {
  342. title: t('创建时间'),
  343. dataIndex: 'created_at',
  344. render: renderTimestamp,
  345. },
  346. {
  347. title: t('最后登录'),
  348. dataIndex: 'last_login_at',
  349. render: renderTimestamp,
  350. },
  351. {
  352. title: '',
  353. dataIndex: 'operate',
  354. fixed: 'right',
  355. width: 200,
  356. render: (text, record, index) =>
  357. renderOperations(text, record, {
  358. setEditingUser,
  359. setShowEditUser,
  360. showPromoteModal,
  361. showDemoteModal,
  362. showEnableDisableModal,
  363. showDeleteModal,
  364. showResetPasskeyModal,
  365. showResetTwoFAModal,
  366. showUserSubscriptionsModal,
  367. t,
  368. }),
  369. },
  370. ];
  371. };