From 91bfcf61f44f353dfebc54a57b5b774721de3e9a Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期一, 19 十月 2026 09:39:22 +0800
Subject: [PATCH] feat(auth): 更新门户认证页面布局和样式

---
 src/views/_core/profile/modules/user-social.vue |  249 ++++++++++++-------------------------------------
 1 files changed, 63 insertions(+), 186 deletions(-)

diff --git a/src/views/_core/profile/modules/user-social.vue b/src/views/_core/profile/modules/user-social.vue
index 4c96f99..39c0867 100644
--- a/src/views/_core/profile/modules/user-social.vue
+++ b/src/views/_core/profile/modules/user-social.vue
@@ -1,207 +1,84 @@
-<script setup lang="tsx">
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+<script setup lang="ts">
 import type { SystemSocialUserApi } from '#/api/system/social/user';
-
 import { computed, onMounted, ref } from 'vue';
-import { useRoute } from 'vue-router';
-
+import { useRoute, useRouter } from 'vue-router';
 import { confirm } from '@vben/common-ui';
-import { DICT_TYPE, SystemUserSocialTypeEnum } from '@vben/constants';
-import { getDictLabel } from '@vben/hooks';
+import { SystemUserSocialTypeEnum } from '@vben/constants';
+import { IconifyIcon } from '@vben/icons';
 import { getUrlValue } from '@vben/utils';
-
-import { Button, Card, Image, message } from 'ant-design-vue';
-
-import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { message } from 'ant-design-vue';
 import { socialAuthRedirect } from '#/api/core/auth';
-import {
-  getBindSocialUserList,
-  socialBind,
-  socialUnbind,
-} from '#/api/system/social/user';
-import { $t } from '#/locales';
+import { getBindSocialUserList, socialBind, socialUnbind } from '#/api/system/social/user';
 
-const emit = defineEmits<{
-  (e: 'update:activeName', v: string): void;
-}>();
-
+const emit = defineEmits<{ 'update:activeName': [string] }>();
 const route = useRoute();
-/** 宸茬粡缁戝畾鐨勫钩鍙� */
+const router = useRouter();
 const bindList = ref<SystemSocialUserApi.SocialUser[]>([]);
-const allBindList = computed<any[]>(() => {
-  return Object.values(SystemUserSocialTypeEnum).map((social) => {
-    const socialUser = bindList.value.find((item) => item.type === social.type);
-    return {
-      ...social,
-      socialUser,
-    };
-  });
-});
-
-function useGridColumns(): VxeTableGridOptions['columns'] {
-  return [
-    {
-      field: 'type',
-      title: '缁戝畾骞冲彴',
-      minWidth: 100,
-      cellRender: {
-        name: 'CellDict',
-        props: { type: DICT_TYPE.SYSTEM_SOCIAL_TYPE },
-      },
-    },
-    {
-      field: 'openid',
-      title: '鏍囪瘑',
-      minWidth: 180,
-    },
-    {
-      field: 'nickname',
-      title: '鏄电О',
-      minWidth: 180,
-    },
-    {
-      field: 'operation',
-      title: '鎿嶄綔',
-      minWidth: 80,
-      align: 'center',
-      fixed: 'right',
-      slots: {
-        default: ({ row }: { row: SystemSocialUserApi.SocialUser }) => {
-          return (
-            <Button onClick={() => onUnbind(row)} type="link">
-              瑙g粦
-            </Button>
-          );
-        },
-      },
-    },
-  ];
+const loading = ref(true);
+const failed = ref(false);
+const busyType = ref<number>();
+const callbackFailed = ref(false);
+const allBindList = computed(() => Object.values(SystemUserSocialTypeEnum).map(platform => ({
+  ...platform, user: bindList.value.find(item => item.type === platform.type),
+})));
+async function load() {
+  loading.value = true; failed.value = false;
+  try { bindList.value = await getBindSocialUserList(); }
+  catch { failed.value = true; }
+  finally { loading.value = false; }
 }
-
-const [Grid, gridApi] = useVbenVxeGrid({
-  gridOptions: {
-    columns: useGridColumns(),
-    minHeight: 0,
-    keepSource: true,
-    proxyConfig: {
-      ajax: {
-        query: async () => {
-          bindList.value = await getBindSocialUserList();
-          return bindList.value;
-        },
-      },
-    },
-    rowConfig: {
-      keyField: 'id',
-    },
-    pagerConfig: {
-      enabled: false,
-    },
-    toolbarConfig: {
-      enabled: false,
-    },
-  },
-});
-
-/** 瑙g粦璐﹀彿 */
-function onUnbind(row: SystemSocialUserApi.SocialUser) {
-  confirm(`纭畾瑙g粦[${getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, row.type)}]骞冲彴鐨刐${row.openid}]璐﹀彿鍚楋紵`).then(async () => {
-    await socialUnbind({ type: row.type, openid: row.openid });
-    // 鎻愮ず鎴愬姛
-    message.success($t('ui.actionMessage.operationSuccess'));
-    await gridApi.reload();
-  });
-}
-
-/** 缁戝畾璐﹀彿锛堣烦杞巿鏉冮〉闈級 */
-async function onBind(bind: any) {
-  const type = bind.type;
-  if (type <= 0) {
-    return;
-  }
+async function unbind(user: SystemSocialUserApi.SocialUser, title: string) {
+  try { await confirm('瑙i櫎涓�' + title + '鐨勭粦瀹氬悗锛屽皢鏃犳硶閫氳繃璇ヨ处鍙风櫥褰曘�傜‘瀹氳В闄ょ粦瀹氬悧锛�'); }
+  catch { return; }
+  busyType.value = user.type;
   try {
-    // 璁$畻 redirectUri
-    // tricky: type 闇�瑕佸厛 encode 涓�娆★紝鍚﹀垯閽夐拤鍥炶皟浼氫涪澶便�傞厤鍚� getUrlValue() 浣跨敤
-    // hash 璺敱锛堢敓浜ф瀯寤猴級涓嬪繀椤诲甫 # 鍓嶇紑锛屽惁鍒欏洖璺冲悗 vue-router 瑙f瀽鍒� /锛岃繘涓嶄簡缁戝畾娴佺▼
+    await socialUnbind({ type: user.type, openid: user.openid });
+    message.success('宸茶В闄ょ粦瀹�');
+    await load();
+  } finally { busyType.value = undefined; }
+}
+async function bind(type: number) {
+  busyType.value = type;
+  try {
     const hashPrefix = import.meta.env.VITE_ROUTER_HISTORY === 'hash' ? '/#' : '';
-    const redirectUri = `${location.origin}${hashPrefix}/profile?${encodeURIComponent(`type=${type}`)}`;
-
-    // 杩涜璺宠浆
+    const redirectUri = location.origin + hashPrefix + '/profile?' + encodeURIComponent('type=' + type);
     window.location.href = await socialAuthRedirect(type, redirectUri);
-  } catch (error) {
-    console.error('绀句氦缁戝畾澶勭悊澶辫触:', error);
-  }
+  } finally { busyType.value = undefined; }
 }
-
-/** 鐩戝惉璺敱鍙樺寲锛屽鐞嗙ぞ浜ょ粦瀹氬洖璋� */
-async function bindSocial() {
-  // 绀句氦缁戝畾
-  const type = Number(getUrlValue('type'));
-  const code = route.query.code as string;
-  const state = route.query.state as string;
-  if (!code) {
-    return;
+onMounted(async () => {
+  const code = route.query.code;
+  if (typeof code === 'string' && code) {
+    emit('update:activeName', 'userSocial');
+    const type = Number(getUrlValue('type'));
+    try {
+      await socialBind({ type, code, state: String(route.query.state ?? '') });
+      message.success('缁戝畾鎴愬姛');
+      const query: Record<string, any> = { ...route.query, section: 'userSocial' };
+      delete query.code; delete query.state; delete query.type;
+      await router.replace({ query });
+    } catch { callbackFailed.value = true; }
   }
-  await socialBind({ type, code, state });
-  // 鎻愮ず鎴愬姛
-  message.success('缁戝畾鎴愬姛');
-  emit('update:activeName', 'userSocial');
-  await gridApi.reload();
-  // 娓呯悊 URL 鍙傛暟锛岄伩鍏嶅埛鏂伴噸澶嶈Е鍙�
-  window.history.replaceState({}, '', location.pathname);
-}
-
-/** 鍒濆鍖� */
-onMounted(() => {
-  bindSocial();
+  await load();
 });
 </script>
 
 <template>
-  <div class="flex flex-col">
-    <Grid />
-
-    <div class="pb-3">
-      <div
-        class="grid grid-cols-1 gap-2 px-2 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3 2xl:grid-cols-3"
-      >
-        <Card v-for="item in allBindList" :key="item.type" class="!mb-2">
-          <div class="flex w-full items-center gap-4">
-            <Image
-              :src="item.img"
-              :width="40"
-              :height="40"
-              :alt="item.title"
-              :preview="false"
-            />
-            <div class="flex flex-1 items-center justify-between">
-              <div class="flex flex-col">
-                <h4 class="mb-1 text-sm text-black/85 dark:text-white/85">
-                  {{ getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, item.type) }}
-                </h4>
-                <span class="text-black/45 dark:text-white/45">
-                  <template v-if="item.socialUser">
-                    {{ item.socialUser?.nickname || item.socialUser?.openid }}
-                  </template>
-                  <template v-else>
-                    缁戝畾
-                    {{ getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, item.type) }}
-                    璐﹀彿
-                  </template>
-                </span>
-              </div>
-              <Button
-                :disabled="!!item.socialUser"
-                size="small"
-                type="link"
-                @click="onBind(item)"
-              >
-                {{ item.socialUser ? '宸茬粦瀹�' : '缁戝畾' }}
-              </Button>
-            </div>
-          </div>
-        </Card>
-      </div>
+  <div class="social-settings">
+    <p v-if="callbackFailed" class="social-error" role="alert">璐﹀彿缁戝畾鏈畬鎴愶紝璇烽噸鏂板彂璧风粦瀹氥��</p>
+    <div v-if="loading" class="social-state" role="status"><IconifyIcon icon="lucide:loader-circle" />姝e湪鍔犺浇缁戝畾淇℃伅鈥�</div>
+    <div v-else-if="failed" class="social-state" role="status"><span>鏆傛椂鏃犳硶鑾峰彇缁戝畾淇℃伅</span><button type="button" @click="load">閲嶆柊鍔犺浇</button></div>
+    <div v-else class="social-platforms">
+      <article v-for="item in allBindList" :key="item.type" class="social-platform">
+        <span class="social-platform-icon" :class="item.source"><IconifyIcon :icon="item.source === 'dingtalk' ? 'ant-design:dingtalk-outlined' : 'ant-design:wechat-outlined'" /></span>
+        <div class="social-platform-copy"><h3>{{ item.title }}<span :class="{ bound: item.user }">{{ item.user ? '宸茬粦瀹�' : '鏈粦瀹�' }}</span></h3><p>{{ item.user ? (item.user.nickname || '宸插叧鑱旀骞冲彴璐﹀彿') : '缁戝畾鍚庡彲浣跨敤' + item.title + '璐﹀彿鐧诲綍' }}</p></div>
+        <button type="button" :disabled="busyType !== undefined" :class="{ unbind: item.user }" @click="item.user ? unbind(item.user, item.title) : bind(item.type)">{{ busyType === item.type ? '澶勭悊涓��' : item.user ? '瑙i櫎缁戝畾' : '绔嬪嵆缁戝畾' }}</button>
+      </article>
     </div>
+    <div class="social-note"><IconifyIcon icon="lucide:info" /><p>缁戝畾鏃跺皢鍓嶅線瀵瑰簲骞冲彴鎺堟潈銆傝В闄ょ粦瀹氫笉褰卞搷褰撳墠璐﹀彿浣跨敤鐢ㄦ埛鍚嶅拰瀵嗙爜鐧诲綍銆�</p></div>
   </div>
 </template>
+<style scoped>
+.social-platforms { display: grid; gap: 14px; }.social-platform { display: flex; min-width: 0; align-items: center; gap: 14px; padding: 20px; border: 1px solid #dfeaf4; border-radius: 6px; }.social-platform-icon { display: grid; width: 48px; height: 48px; flex: none; place-items: center; border-radius: 10px; color: #2076bf; background: #edf5fd; font-size: 28px; }.social-platform-icon.wechat_enterprise { color: #19895a; background: #edf8f2; }.social-platform-copy { min-width: 0; flex: 1; }.social-platform-copy h3 { display: flex; flex-wrap: wrap; align-items: center; gap: 9px; margin: 0 0 7px; font-size: 15px; font-weight: 650; }.social-platform-copy h3 span { padding: 2px 7px; border-radius: 3px; color: #657a8c; background: #f0f4f7; font-size: 10px; font-weight: 400; }.social-platform-copy h3 span.bound { color: #207348; background: #edf8f1; }.social-platform-copy p { margin: 0; color: var(--portal-muted); font-size: 12px; line-height: 1.7; overflow-wrap: anywhere; }.social-platform button,.social-state button { min-height: 34px; flex: none; padding: 6px 13px; border: 1px solid #b7242d; border-radius: 4px; color: #b7242d; background: transparent; font-size: 12px; cursor: pointer; }.social-platform button.unbind { border-color: #d5e0eb; color: #4e6b85; }.social-platform button:disabled { opacity: .5; cursor: wait; }.social-note { display: flex; align-items: flex-start; gap: 9px; margin-top: 22px; color: var(--portal-muted); font-size: 12px; line-height: 1.8; }.social-note svg { width: 16px; height: 16px; flex: none; margin-top: 3px; }.social-note p { margin: 0; }.social-state { display: flex; min-height: 180px; align-items: center; justify-content: center; gap: 14px; color: var(--portal-muted); font-size: 13px; }.social-state svg { width: 20px; }.social-error { padding: 12px; color: #a3222b; background: #fff2f1; font-size: 12px; }button:focus-visible { outline: 2px solid #2c70b2; outline-offset: 3px; }
+@media(max-width:520px) { .social-platform { flex-wrap: wrap; gap: 12px; padding: 16px; }.social-platform-copy { flex-basis: calc(100% - 60px); }.social-platform button { margin-left: 60px; }.social-state { flex-direction: column; } }
+</style>
+

--
Gitblit v1.9.3