Browse Source

refactor: extract binding modal and polish binding management UX

Seefs 1 tuần trước cách đây
mục cha
commit
2f4d38fefd

+ 3 - 3
web/src/components/table/users/modals/EditUserModal.jsx

@@ -207,7 +207,7 @@ const EditUserModal = (props) => {
             onSubmit={submit}
           >
             {({ values }) => (
-              <div className='p-2'>
+              <div className='p-2 space-y-3'>
                 {/* 基本信息 */}
                 <Card className='!rounded-2xl shadow-sm border-0'>
                   <div className='flex items-center mb-2'>
@@ -344,7 +344,7 @@ const EditUserModal = (props) => {
                             {t('绑定信息')}
                           </Text>
                           <div className='text-xs text-gray-600'>
-                            {t('第三方账户绑定状态(只读)')}
+                            {t('管理用户已绑定的第三方账户,支持筛选与解绑')}
                           </div>
                         </div>
                       </div>
@@ -353,7 +353,7 @@ const EditUserModal = (props) => {
                         theme='outline'
                         onClick={openBindingModal}
                       >
-                        {t('修改绑定')}
+                        {t('管理绑定')}
                       </Button>
                     </div>
                   </Card>

+ 88 - 74
web/src/components/table/users/modals/UserBindingManagementModal.jsx

@@ -53,7 +53,7 @@ const UserBindingManagementModal = ({
 }) => {
   const { t } = useTranslation();
   const [bindingLoading, setBindingLoading] = React.useState(false);
-  const [showUnboundOnly, setShowUnboundOnly] = React.useState(false);
+  const [showBoundOnly, setShowBoundOnly] = React.useState(true);
   const [statusInfo, setStatusInfo] = React.useState({});
   const [customOAuthBindings, setCustomOAuthBindings] = React.useState([]);
   const [bindingActionLoading, setBindingActionLoading] = React.useState({});
@@ -90,7 +90,7 @@ const UserBindingManagementModal = ({
 
   React.useEffect(() => {
     if (!visible) return;
-    setShowUnboundOnly(false);
+    setShowBoundOnly(true);
     setBindingActionLoading({});
     loadBindingData();
   }, [visible, loadBindingData]);
@@ -294,8 +294,12 @@ const UserBindingManagementModal = ({
     ...customBindingItems.map((item) => ({ ...item, type: 'custom' })),
   ];
 
-  const visibleBindingItems = showUnboundOnly
-    ? allBindingItems.filter((item) => !item.value)
+  const boundCount = allBindingItems.filter((item) =>
+    Boolean(item.value),
+  ).length;
+
+  const visibleBindingItems = showBoundOnly
+    ? allBindingItems.filter((item) => Boolean(item.value))
     : allBindingItems;
 
   return (
@@ -308,86 +312,96 @@ const UserBindingManagementModal = ({
       title={
         <div className='flex items-center'>
           <IconLink className='mr-2' />
-          {t('绑定信息')}
+          {t('账户绑定管理')}
         </div>
       }
     >
       <Spin spinning={bindingLoading}>
-        <div className='flex items-center justify-between mb-4 gap-3 flex-wrap'>
-          <Checkbox
-            checked={showUnboundOnly}
-            onChange={(e) => setShowUnboundOnly(Boolean(e.target.checked))}
-          >
-            {`${t('筛选')} ${t('未绑定')}`}
-          </Checkbox>
-          <Text type='tertiary'>
-            {t('筛选')} · {visibleBindingItems.length}
-          </Text>
-        </div>
+        <div className='max-h-[68vh] overflow-y-auto pr-1 pb-2'>
+          <div className='flex items-center justify-between mb-4 gap-3 flex-wrap'>
+            <Checkbox
+              checked={showBoundOnly}
+              onChange={(e) => setShowBoundOnly(Boolean(e.target.checked))}
+            >
+              {t('仅显示已绑定')}
+            </Checkbox>
+            <Text type='tertiary'>
+              {t('已绑定')} {boundCount} / {allBindingItems.length}
+            </Text>
+          </div>
 
-        {visibleBindingItems.length === 0 ? (
-          <Card className='!rounded-xl border-dashed'>
-            <Text type='tertiary'>{t('暂无自定义 OAuth 提供商')}</Text>
-          </Card>
-        ) : (
-          <div className='grid grid-cols-1 lg:grid-cols-2 gap-3'>
-            {visibleBindingItems.map((item) => {
-              const isBound = Boolean(item.value);
-              const loadingKey =
-                item.type === 'builtin'
-                  ? `builtin-${item.key}`
-                  : `custom-${item.providerId}`;
-              const statusText = isBound
-                ? item.value
-                : item.enabled
-                  ? t('未绑定')
-                  : t('未启用');
+          {visibleBindingItems.length === 0 ? (
+            <Card className='!rounded-xl border-dashed'>
+              <Text type='tertiary'>{t('暂无已绑定项')}</Text>
+            </Card>
+          ) : (
+            <div className='grid grid-cols-1 lg:grid-cols-2 gap-4'>
+              {visibleBindingItems.map((item, index) => {
+                const isBound = Boolean(item.value);
+                const loadingKey =
+                  item.type === 'builtin'
+                    ? `builtin-${item.key}`
+                    : `custom-${item.providerId}`;
+                const statusText = isBound
+                  ? item.value
+                  : item.enabled
+                    ? t('未绑定')
+                    : t('未启用');
+                const shouldSpanTwoColsOnDesktop =
+                  visibleBindingItems.length % 2 === 1 &&
+                  index === visibleBindingItems.length - 1;
 
-              return (
-                <Card key={item.key} className='!rounded-xl'>
-                  <div className='flex items-center justify-between gap-3'>
-                    <div className='flex items-center flex-1 min-w-0'>
-                      <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
-                        {item.icon}
-                      </div>
-                      <div className='min-w-0 flex-1'>
-                        <div className='font-medium text-gray-900 flex items-center gap-2'>
-                          <span>{item.name}</span>
-                          <Tag size='small' color='white'>
-                            {item.type === 'builtin' ? 'Built-in' : 'Custom'}
-                          </Tag>
+                return (
+                  <Card
+                    key={item.key}
+                    className={`!rounded-xl ${shouldSpanTwoColsOnDesktop ? 'lg:col-span-2' : ''}`}
+                  >
+                    <div className='flex items-center justify-between gap-3 min-h-[92px]'>
+                      <div className='flex items-center flex-1 min-w-0'>
+                        <div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
+                          {item.icon}
                         </div>
-                        <div className='text-sm text-gray-500 truncate'>
-                          {statusText}
+                        <div className='min-w-0 flex-1'>
+                          <div className='font-medium text-gray-900 flex items-center gap-2'>
+                            <span>{item.name}</span>
+                            <Tag size='small' color='white'>
+                              {item.type === 'builtin'
+                                ? t('内置')
+                                : t('自定义')}
+                            </Tag>
+                          </div>
+                          <div className='text-sm text-gray-500 truncate'>
+                            {statusText}
+                          </div>
                         </div>
                       </div>
+                      <Button
+                        type='danger'
+                        theme='borderless'
+                        icon={<IconDelete />}
+                        size='small'
+                        disabled={!isBound}
+                        loading={Boolean(bindingActionLoading[loadingKey])}
+                        onClick={() => {
+                          if (item.type === 'builtin') {
+                            handleUnbindBuiltInAccount(item);
+                            return;
+                          }
+                          handleUnbindCustomOAuthAccount({
+                            id: item.providerId,
+                            name: item.name,
+                          });
+                        }}
+                      >
+                        {t('解绑')}
+                      </Button>
                     </div>
-                    <Button
-                      type='danger'
-                      theme='borderless'
-                      icon={<IconDelete />}
-                      size='small'
-                      disabled={!isBound}
-                      loading={Boolean(bindingActionLoading[loadingKey])}
-                      onClick={() => {
-                        if (item.type === 'builtin') {
-                          handleUnbindBuiltInAccount(item);
-                          return;
-                        }
-                        handleUnbindCustomOAuthAccount({
-                          id: item.providerId,
-                          name: item.name,
-                        });
-                      }}
-                    >
-                      {t('解绑')}
-                    </Button>
-                  </div>
-                </Card>
-              );
-            })}
-          </div>
-        )}
+                  </Card>
+                );
+              })}
+            </div>
+          )}
+        </div>
       </Spin>
     </Modal>
   );