ys пре 2 недеља
родитељ
комит
7e0277a576

+ 13 - 1
yushu-uivue3/src/layout/components/Settings/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <a-drawer v-model:open="showSettings" title="系统设置" :maskClosable="true" placement="right" :width="300">
+  <a-drawer v-model:open="showSettings" title="系统设置" :maskClosable="true" placement="right" :width="300" :zIndex="1001">
     <div class="setting-drawer-title">
       <h3 class="drawer-title">主题风格设置</h3>
     </div>
@@ -306,4 +306,16 @@ defineExpose({
   margin-top: 16px;
   text-align: right;
 }
+</style>
+
+<style lang="scss">
+/* 修复抽屉蒙版层级问题 - 全局样式 */
+.ant-drawer {
+  .ant-drawer-mask {
+    z-index: 1001 !important;
+  }
+  .ant-drawer-content-wrapper {
+    z-index: 1002 !important;
+  }
+}
 </style>

+ 1 - 1
yushu-uivue3/src/settings.js

@@ -57,7 +57,7 @@ export default {
   /**
    * 是否显示水印
    */
-  watermarkVisible: false,
+  watermarkVisible: true,
 
   /**
    * 水印文本

+ 74 - 7
yushu-uivue3/src/views/message/chat-styles.scss

@@ -29,6 +29,21 @@
       font-weight: 600;
       color: #1f2937;
       gap: 8px;
+      
+      .anticon {
+        font-size: 20px;
+        color: #1890ff;
+      }
+    }
+    
+    .header-actions {
+      .ant-btn {
+        color: #666;
+        
+        &:hover {
+          color: #1890ff;
+        }
+      }
     }
   }
 
@@ -36,6 +51,10 @@
     padding: 16px 20px;
     background: #fafbfc;
     border-bottom: 1px solid #f0f2f5;
+    
+    .search-input {
+      border-radius: 20px;
+    }
   }
 
   .conversation-list {
@@ -56,13 +75,13 @@
       }
 
       &.active {
-        background: #ecf5ff;
-        border-left-color: #409eff;
+        background: #e6f7ff;
+        border-left-color: #1890ff;
       }
 
       &.unread {
         background: #fefefe;
-        border-left-color: #ff4757;
+        border-left-color: #ff4d4f;
 
         .conversation-name {
           font-weight: 600;
@@ -71,6 +90,10 @@
 
       .conversation-avatar {
         margin-right: 12px;
+        
+        .ant-avatar {
+          background: #1890ff;
+        }
       }
 
       .conversation-content {
@@ -94,6 +117,8 @@
           .conversation-time {
             font-size: 12px;
             color: #9ca3af;
+            flex-shrink: 0;
+            margin-left: 8px;
           }
         }
 
@@ -110,6 +135,10 @@
             white-space: nowrap;
             max-width: 180px;
           }
+          
+          .unread-count {
+            flex-shrink: 0;
+          }
         }
       }
     }
@@ -120,6 +149,11 @@
       align-items: center;
       padding: 60px 20px;
       color: #9ca3af;
+      
+      .anticon {
+        color: #d9d9d9;
+        margin-bottom: 12px;
+      }
     }
   }
 }
@@ -158,6 +192,14 @@
           }
         }
       }
+      
+      .header-actions {
+        .ant-btn {
+          .anticon {
+            margin-right: 4px;
+          }
+        }
+      }
     }
 
     .messages-container {
@@ -180,8 +222,8 @@
           }
 
           &.is-unread {
-            background: #ecf5ff;
-            border-left: 3px solid #409eff;
+            background: #e6f7ff;
+            border-left: 3px solid #1890ff;
           }
 
           .card-title {
@@ -206,6 +248,11 @@
           text-align: center;
           padding: 60px;
           color: #909399;
+          
+          .anticon {
+            color: #d9d9d9;
+            margin-bottom: 12px;
+          }
         }
       }
 
@@ -270,10 +317,10 @@
                   }
 
                   &.bubble-mine {
-                    background: #409eff;
+                    background: #1890ff;
                     color: #ffffff;
                     border-top-right-radius: 2px;
-                    box-shadow: 0 2px 6px rgba(64, 158, 255, 0.25);
+                    box-shadow: 0 2px 6px rgba(24, 144, 255, 0.25);
                   }
                 }
 
@@ -299,6 +346,11 @@
           justify-content: center;
           height: 300px;
           color: #9ca3af;
+          
+          .anticon {
+            color: #d9d9d9;
+            margin-bottom: 12px;
+          }
         }
       }
     }
@@ -308,6 +360,11 @@
       padding: 16px 24px;
 
       .input-container {
+        .ant-input {
+          resize: none;
+          border-radius: 8px;
+        }
+        
         .input-footer {
           display: flex;
           justify-content: space-between;
@@ -331,6 +388,10 @@
     justify-content: center;
     background: #f5f7fa;
     color: #606266;
+    
+    .anticon {
+      color: #d9d9d9;
+    }
 
     h2 {
       margin: 24px 0 12px;
@@ -362,9 +423,15 @@
     padding: 10px 16px;
     cursor: pointer;
     font-size: 14px;
+    color: #333;
 
     &:hover {
       background: #f5f7fa;
+      color: #1890ff;
+    }
+    
+    .anticon {
+      font-size: 14px;
     }
   }
 }

+ 162 - 134
yushu-uivue3/src/views/message/index.vue

@@ -4,24 +4,25 @@
     <div class="chat-sidebar">
       <div class="sidebar-header">
         <div class="header-title">
-          <el-icon><ChatDotRound /></el-icon>
+          <MessageOutlined />
           <span>消息</span>
         </div>
         <div class="header-actions">
-          <el-tooltip content="新建会话" placement="bottom">
-            <el-button link icon="Plus" @click="showNewMessageDialog = true" />
-          </el-tooltip>
+          <a-tooltip title="新建会话" placement="bottom">
+            <a-button type="text" @click="showNewMessageDialog = true"><PlusOutlined /></a-button>
+          </a-tooltip>
         </div>
       </div>
 
       <div class="search-container">
-        <el-input
-          v-model="searchKeyword"
+        <a-input
+          v-model:value="searchKeyword"
           placeholder="搜索联系人、消息"
-          prefix-icon="Search"
-          clearable
+          allow-clear
           class="search-input"
-        />
+        >
+          <template #prefix><SearchOutlined /></template>
+        </a-input>
       </div>
 
       <div class="conversation-list">
@@ -37,12 +38,12 @@
           @contextmenu.prevent="showContextMenu($event, conv)"
         >
           <div class="conversation-avatar">
-            <el-avatar v-if="conv.conversationId === 'sys_notification'" :size="48" style="background-color: #409eff;">
-              <el-icon :size="24"><Promotion /></el-icon>
-            </el-avatar>
-            <el-avatar v-else :size="48" :src="conv.avatar">
+            <a-avatar v-if="conv.conversationId === 'sys_notification'" :size="48" style="background-color: #1890ff;">
+              <template #icon><NotificationOutlined /></template>
+            </a-avatar>
+            <a-avatar v-else :size="48" :src="conv.avatar">
               {{ (conv.otherUserName || conv.title || '未知').charAt(0) }}
-            </el-avatar>
+            </a-avatar>
           </div>
           
           <div class="conversation-content">
@@ -52,13 +53,13 @@
             </div>
             <div class="conversation-preview">
               <div class="last-message">{{ conv.lastMessage || '暂无消息' }}</div>
-              <el-badge v-if="conv.unreadCount > 0" :value="conv.unreadCount > 99 ? '99+' : conv.unreadCount" class="unread-count" />
+              <a-badge v-if="conv.unreadCount > 0" :count="conv.unreadCount" :overflow-count="99" class="unread-count" />
             </div>
           </div>
         </div>
 
         <div v-if="displayConversations.length === 0" class="empty-conversations">
-          <el-icon :size="48"><ChatLineRound /></el-icon>
+          <MessageOutlined style="font-size: 48px;" />
           <p>暂无会话</p>
         </div>
       </div>
@@ -69,29 +70,27 @@
       <div v-if="currentConversation" class="chat-window">
         <div class="chat-header">
           <div class="chat-user-info">
-            <el-avatar v-if="currentConversation.conversationId === 'sys_notification'" :size="40" style="background-color: #409eff;">
-              <el-icon :size="20"><Promotion /></el-icon>
-            </el-avatar>
-            <el-avatar v-else :size="40" :src="currentConversation.avatar">
+            <a-avatar v-if="currentConversation.conversationId === 'sys_notification'" :size="40" style="background-color: #1890ff;">
+              <template #icon><NotificationOutlined /></template>
+            </a-avatar>
+            <a-avatar v-else :size="40" :src="currentConversation.avatar">
               {{ (currentConversation.otherUserName || '未知').charAt(0) }}
-            </el-avatar>
+            </a-avatar>
             <div class="user-details">
               <div class="user-name">{{ currentConversation.otherUserName || currentConversation.title || '未知用户' }}</div>
             </div>
           </div>
           <div class="header-actions">
-            <el-button 
+            <a-button 
               v-if="currentConversation.conversationId === 'sys_notification'"
-              link
-              icon="CircleCheck"
+              type="link"
               @click="markAllNotificationsAsRead"
-            >全部已读</el-button>
-            <el-button 
+            ><CheckOutlined />全部已读</a-button>
+            <a-button 
               v-else
-              link
-              icon="Edit"
+              type="link"
               @click="editRemark"
-            >备注</el-button>
+            ><EditOutlined />备注</a-button>
           </div>
         </div>
 
@@ -99,7 +98,7 @@
           <!-- 系统通知 -->
           <div v-if="currentConversation.conversationId === 'sys_notification'" class="notification-container">
             <div v-if="systemNotifications.length === 0" class="empty-notifications">
-              <el-icon :size="48"><Bell /></el-icon>
+              <BellOutlined style="font-size: 48px;" />
               <p>暂无系统通知</p>
             </div>
             <div 
@@ -130,9 +129,9 @@
               >
                 <div class="message-item">
                   <div v-if="msg.senderId !== userId" class="message-avatar">
-                    <el-avatar :size="36" :src="msg.senderAvatar">
+                    <a-avatar :size="36" :src="msg.senderAvatar">
                       {{ (msg.senderName || '未知').charAt(0) }}
-                    </el-avatar>
+                    </a-avatar>
                   </div>
                   
                   <div class="message-content">
@@ -145,16 +144,16 @@
                   </div>
                   
                   <div v-if="msg.senderId === userId" class="message-avatar">
-                    <el-avatar :size="36" :src="userAvatar">
+                    <a-avatar :size="36" :src="userAvatar">
                       {{ (userName || '我').charAt(0) }}
-                    </el-avatar>
+                    </a-avatar>
                   </div>
                 </div>
               </div>
             </div>
 
             <div v-if="messages.length === 0" class="empty-messages">
-              <el-icon :size="48"><ChatDotRound /></el-icon>
+              <MessageOutlined style="font-size: 48px;" />
               <p>还没有消息</p>
             </div>
           </div>
@@ -163,23 +162,22 @@
         <!-- 输入区域 -->
         <div v-if="currentConversation.conversationId !== 'sys_notification'" class="input-area">
           <div class="input-container">
-            <el-input
-              v-model="messageContent"
-              type="textarea"
+            <a-textarea
+              v-model:value="messageContent"
               :rows="3"
               placeholder="输入消息... (Enter发送, Ctrl+Enter换行)"
               :maxlength="1000"
-              resize="none"
+              :auto-size="false"
               @keydown="handleKeyDown"
             />
             <div class="input-footer">
               <span class="char-count">{{ messageContent.length }}/1000</span>
-              <el-button 
+              <a-button 
                 type="primary" 
                 :disabled="!messageContent.trim() || sending"
                 :loading="sending"
                 @click="sendMessage"
-              >发送</el-button>
+              >发送</a-button>
             </div>
           </div>
         </div>
@@ -187,58 +185,59 @@
 
       <!-- 欢迎界面 -->
       <div v-else class="welcome-screen">
-        <el-icon :size="64"><ChatDotRound /></el-icon>
+        <MessageOutlined style="font-size: 64px;" />
         <h2>欢迎使用消息中心</h2>
         <p>选择一个会话开始聊天</p>
-        <el-button type="primary" icon="Plus" @click="showNewMessageDialog = true">开始新对话</el-button>
+        <a-button type="primary" @click="showNewMessageDialog = true"><PlusOutlined />开始新对话</a-button>
       </div>
     </div>
 
     <!-- 新建会话对话框 -->
-    <el-dialog title="新建会话" v-model="showNewMessageDialog" width="400px">
-      <el-form :model="newConvForm" :rules="newConvRules" ref="newConvFormRef" label-width="80px">
-        <el-form-item label="选择用户" prop="userId">
-          <el-select
-            v-model="newConvForm.userId"
-            filterable
-            remote
-            reserve-keyword
-            placeholder="请输入昵称/手机号搜索"
-            :remote-method="searchUsers"
-            :loading="searchLoading"
+    <a-modal title="新建会话" v-model:open="showNewMessageDialog" width="400px" :footer="null" :destroyOnClose="true">
+      <a-form :model="newConvForm" ref="newConvFormRef" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
+        <a-form-item label="选择用户" name="userId" :rules="[{ required: true, message: '请选择用户' }]">
+          <a-select
+            v-model:value="newConvForm.userId"
+            show-search
+            placeholder="请输入昵称搜索用户"
+            :filter-option="false"
+            :options="userOptions"
+            :field-names="{ label: 'label', value: 'value' }"
+            @search="handleUserSearch"
             style="width: 100%"
-            clearable
+            allow-clear
+            :loading="searchLoading"
           >
-            <el-option
-              v-for="user in userOptions"
-              :key="user.userId"
-              :label="user.nickName + ' (' + (user.phonenumber || '无') + ')'"
-              :value="user.userId"
-            />
-          </el-select>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="showNewMessageDialog = false">取消</el-button>
-        <el-button type="primary" @click="createNewConversation" :loading="sending">创建</el-button>
-      </template>
-    </el-dialog>
+            <template #notFoundContent>
+              <div style="text-align: center; padding: 8px;">
+                <a-spin v-if="searchLoading" size="small" />
+                <span v-else style="color: #999;">{{ userSearchText ? '未找到用户' : '请输入关键词搜索' }}</span>
+              </div>
+            </template>
+          </a-select>
+        </a-form-item>
+      </a-form>
+      <div style="text-align: right; margin-top: 16px;">
+        <a-button @click="showNewMessageDialog = false">取消</a-button>
+        <a-button type="primary" style="margin-left: 8px;" @click="createNewConversation" :loading="sending">创建</a-button>
+      </div>
+    </a-modal>
 
     <!-- 设置备注对话框 -->
-    <el-dialog title="设置备注" v-model="showRemarkDialog" width="400px">
-      <el-form :model="remarkForm" label-width="80px">
-        <el-form-item label="用户名">
-          <el-input :model-value="currentConversation?.otherUserName || ''" disabled />
-        </el-form-item>
-        <el-form-item label="备注名">
-          <el-input v-model="remarkForm.remark" placeholder="请输入备注名" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="showRemarkDialog = false">取消</el-button>
-        <el-button type="primary" @click="saveRemark">保存</el-button>
-      </template>
-    </el-dialog>
+    <a-modal title="设置备注" v-model:open="showRemarkDialog" width="400px" :footer="null">
+      <a-form :model="remarkForm" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
+        <a-form-item label="用户名">
+          <a-input :value="currentConversation?.otherUserName || ''" disabled />
+        </a-form-item>
+        <a-form-item label="备注名">
+          <a-input v-model:value="remarkForm.remark" placeholder="请输入备注名" />
+        </a-form-item>
+      </a-form>
+      <div style="text-align: right; margin-top: 16px;">
+        <a-button @click="showRemarkDialog = false">取消</a-button>
+        <a-button type="primary" style="margin-left: 8px;" @click="saveRemark">保存</a-button>
+      </div>
+    </a-modal>
 
     <!-- 右键菜单 -->
     <div 
@@ -247,7 +246,7 @@
       :style="{ top: contextMenu.top + 'px', left: contextMenu.left + 'px' }"
     >
       <div class="context-menu-item" @click="deleteConversationItem(contextMenu.conversation)">
-        <el-icon><Delete /></el-icon>
+        <DeleteOutlined />
         <span>删除会话</span>
       </div>
     </div>
@@ -255,7 +254,11 @@
 </template>
 
 <script setup name="MessageCenter">
-import { MessageOutlined, MessageOutlined as ChatLineRound, GiftOutlined, BellOutlined, DeleteOutlined } from '@ant-design/icons-vue'
+import { 
+  MessageOutlined, PlusOutlined, SearchOutlined, NotificationOutlined, 
+  BellOutlined, DeleteOutlined, EditOutlined, CheckOutlined 
+} from '@ant-design/icons-vue'
+import { Modal, message } from 'ant-design-vue'
 import { getConversationList, getConversationMessages, sendMessage as sendMessageAPI, setConversationRemark, createPrivateConversation, markConversationAsRead as markReadAPI, deleteConversation } from '@/api/system/message'
 import { listUser } from '@/api/system/user'
 import { getAllNotifications, markNotificationAsRead } from '@/api/system/notification'
@@ -278,14 +281,17 @@ const messagesContainerRef = ref(null)
 const showNewMessageDialog = ref(false)
 const showRemarkDialog = ref(false)
 const searchLoading = ref(false)
+const userSearchText = ref('')
 
 // 表单
 const newConvForm = ref({ userId: null })
-const newConvRules = { userId: [{ required: true, message: '请选择用户', trigger: 'change' }] }
 const userOptions = ref([])
 const remarkForm = ref({ remark: '' })
 const newConvFormRef = ref(null)
 
+// 搜索防抖定时器
+let searchTimer = null
+
 // 右键菜单
 const contextMenu = reactive({
   visible: false,
@@ -363,7 +369,7 @@ async function loadConversations() {
       remarkName: null
     })
   } catch (error) {
-    proxy.$modal.msgError('加载会话列表失败')
+    message.error('加载会话列表失败')
   }
 }
 
@@ -431,7 +437,7 @@ async function sendMessage() {
     await loadConversations()
     scrollToBottom()
   } catch (error) {
-    proxy.$modal.msgError('发送失败')
+    message.error('发送失败')
   } finally {
     sending.value = false
   }
@@ -479,66 +485,88 @@ async function handleNotificationClick(notif) {
   }
 }
 
-/** 搜索用户 */
-async function searchUsers(query) {
-  if (!query) {
+/** 搜索用户(带防抖) */
+function handleUserSearch(value) {
+  userSearchText.value = value
+  
+  // 清除之前的定时器
+  if (searchTimer) {
+    clearTimeout(searchTimer)
+  }
+  
+  if (!value || !value.trim()) {
     userOptions.value = []
+    searchLoading.value = false
     return
   }
+  
   searchLoading.value = true
-  try {
-    const response = await listUser({ nickName: query, pageNum: 1, pageSize: 20 })
-    userOptions.value = (response.rows || [])
-      .filter(user => user.userId !== userId.value)
-      .map(user => ({
-        userId: user.userId,
-        nickName: user.nickName,
-        phonenumber: user.phonenumber
-      }))
-  } finally {
-    searchLoading.value = false
-  }
-}
-
-/** 创建新会话 */
-async function createNewConversation() {
-  newConvFormRef.value.validate(async (valid) => {
-    if (!valid) return
-    
-    sending.value = true
+  
+  // 防抖 300ms
+  searchTimer = setTimeout(async () => {
     try {
-      await createPrivateConversation(newConvForm.value.userId)
-      proxy.$modal.msgSuccess('会话创建成功')
-      showNewMessageDialog.value = false
-      newConvForm.value.userId = null
-      userOptions.value = []
-      loadConversations()
+      const response = await listUser({ nickName: value.trim(), pageNum: 1, pageSize: 20 })
+      userOptions.value = (response.rows || [])
+        .filter(user => user.userId !== userId.value)
+        .map(user => ({
+          value: user.userId,
+          label: `${user.nickName} (${user.phonenumber || '无手机号'})`
+        }))
     } catch (error) {
-      proxy.$modal.msgError('创建失败')
+      console.error('搜索用户失败:', error)
+      userOptions.value = []
     } finally {
-      sending.value = false
+      searchLoading.value = false
     }
-  })
+  }, 300)
 }
 
-/** 删除会话 */
-async function deleteConversationItem(conversation) {
-  contextMenu.visible = false
+/** 创建新会话 */
+async function createNewConversation() {
+  if (!newConvForm.value.userId) {
+    message.warning('请选择用户')
+    return
+  }
+  
+  sending.value = true
   try {
-    await proxy.$modal.confirm('确定删除该会话吗?')
-    await deleteConversation(conversation.conversationId)
-    proxy.$modal.msgSuccess('删除成功')
-    
-    if (currentConversation.value?.conversationId === conversation.conversationId) {
-      currentConversation.value = null
-      messages.value = []
-    }
+    await createPrivateConversation(newConvForm.value.userId)
+    message.success('会话创建成功')
+    showNewMessageDialog.value = false
+    newConvForm.value.userId = null
+    userOptions.value = []
+    userSearchText.value = ''
     loadConversations()
   } catch (error) {
-    if (error !== 'cancel') proxy.$modal.msgError('删除失败')
+    message.error('创建失败')
+  } finally {
+    sending.value = false
   }
 }
 
+/** 删除会话 */
+async function deleteConversationItem(conversation) {
+  contextMenu.visible = false
+  Modal.confirm({
+    title: '提示',
+    content: '确定删除该会话吗?',
+    onOk: async () => {
+      try {
+        await deleteConversation(conversation.conversationId)
+        message.success('删除成功')
+        
+        if (currentConversation.value?.conversationId === conversation.conversationId) {
+          currentConversation.value = null
+          messages.value = []
+        }
+        loadConversations()
+      } catch (error) {
+        message.error('删除失败')
+      }
+    }
+  })
+}
+
 /** 编辑备注 */
 function editRemark() {
   remarkForm.value.remark = currentConversation.value?.remarkName || ''
@@ -551,9 +579,9 @@ async function saveRemark() {
     await setConversationRemark(currentConversation.value.conversationId, remarkForm.value.remark)
     currentConversation.value.remarkName = remarkForm.value.remark
     showRemarkDialog.value = false
-    proxy.$modal.msgSuccess('保存成功')
+    message.success('保存成功')
   } catch (error) {
-    proxy.$modal.msgError('保存失败')
+    message.error('保存失败')
   }
 }
 

+ 278 - 219
yushu-uivue3/src/views/system/ai/service/index.vue

@@ -3,53 +3,40 @@
     <!-- 搜索区域 -->
     <a-form :model="queryParams" ref="queryRef" layout="inline" v-show="showSearch" class="search-form">
       <a-form-item label="服务名称" name="serviceName">
-        <a-input v-model:value="queryParams.serviceName" placeholder="请输入服务名称" allowClear @pressEnter="handleQuery" />
+        <a-input v-model:value="queryParams.serviceName" placeholder="请输入服务名称" allowClear style="width: 240px" @pressEnter="handleQuery" />
       </a-form-item>
       <a-form-item label="厂商" name="provider">
-        <a-select v-model:value="queryParams.provider" placeholder="请选择厂商" allowClear style="width: 200px">
+        <a-select v-model:value="queryParams.provider" placeholder="请选择厂商" allowClear style="width: 240px">
           <a-select-option v-for="item in providerOptions" :key="item.value" :value="item.value">{{ item.label }}</a-select-option>
         </a-select>
       </a-form-item>
       <a-form-item label="状态" name="enabled">
-        <a-select v-model:value="queryParams.enabled" placeholder="请选择状态" allowClear style="width: 200px">
+        <a-select v-model:value="queryParams.enabled" placeholder="请选择状态" allowClear style="width: 240px">
           <a-select-option value="1">启用</a-select-option>
           <a-select-option value="0">停用</a-select-option>
         </a-select>
       </a-form-item>
       <a-form-item>
-        <a-button type="primary" @click="handleQuery">
-          <template #icon><SearchOutlined /></template>
-          搜索
-        </a-button>
-        <a-button style="margin-left: 8px" @click="resetQuery">
-          <template #icon><ReloadOutlined /></template>
-          重置
-        </a-button>
+        <a-button type="primary" @click="handleQuery"><SearchOutlined />搜索</a-button>
+        <a-button style="margin-left: 8px" @click="resetQuery"><ReloadOutlined />重置</a-button>
       </a-form-item>
     </a-form>
 
     <!-- 操作按钮 -->
-    <a-row :gutter="10" class="mb8">
-      <a-col :span="1.5">
-        <a-button type="primary" @click="handleAdd" v-hasPermi="['ai:service:add']">
-          <template #icon><PlusOutlined /></template>
-          新增
-        </a-button>
-      </a-col>
-      <a-col :span="1.5">
-        <a-button type="primary" :disabled="single" @click="handleUpdate" v-hasPermi="['ai:service:edit']">
-          <template #icon><EditOutlined /></template>
-          修改
-        </a-button>
-      </a-col>
-      <a-col :span="1.5">
-        <a-button danger :disabled="multiple" @click="handleDelete" v-hasPermi="['ai:service:remove']">
-          <template #icon><DeleteOutlined /></template>
-          删除
-        </a-button>
-      </a-col>
-      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
-    </a-row>
+    <div class="button-toolbar">
+      <a-row :gutter="10">
+        <a-col :span="1.5">
+          <a-button type="primary" @click="handleAdd" v-hasPermi="['ai:service:add']"><PlusOutlined />新增</a-button>
+        </a-col>
+        <a-col :span="1.5">
+          <a-button type="primary" :disabled="single" @click="handleUpdate" v-hasPermi="['ai:service:edit']" style="background-color: #52c41a; border-color: #52c41a"><EditOutlined />修改</a-button>
+        </a-col>
+        <a-col :span="1.5">
+          <a-button danger :disabled="multiple" @click="handleDelete" v-hasPermi="['ai:service:remove']"><DeleteOutlined />删除</a-button>
+        </a-col>
+        <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
+      </a-row>
+    </div>
 
     <!-- 数据表格 -->
     <a-table 
@@ -118,195 +105,133 @@
     <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
 
     <!-- 新增/修改对话框 -->
-    <a-modal :title="title" v-model:open="open" width="750px" :maskClosable="false" :destroyOnClose="true">
-      <a-form ref="serviceRef" :model="form" :rules="rules" :label-col="{ span: 6 }">
+    <a-modal :title="title" v-model:open="open" width="720px" :maskClosable="false" :destroyOnClose="true">
+      <a-form ref="serviceRef" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
         <a-divider orientation="left">基本信息</a-divider>
         
-        <a-row :gutter="16">
+        <a-row :gutter="24">
           <a-col :span="12">
             <a-form-item label="服务名称" name="serviceName">
-              <a-input v-model:value="form.serviceName" placeholder="显示名称,如:GPT-4o" />
+              <a-input v-model:value="form.serviceName" placeholder="如:GPT-4o" />
             </a-form-item>
           </a-col>
           <a-col :span="12">
             <a-form-item label="服务代码" name="serviceCode">
-              <a-input v-model:value="form.serviceCode" placeholder="唯一标识,如:openai-gpt4o" :disabled="form.serviceId != null" />
+              <a-input v-model:value="form.serviceCode" placeholder="如:openai-gpt4o" :disabled="form.serviceId != null" />
             </a-form-item>
           </a-col>
         </a-row>
-
-        <a-row :gutter="16">
+        
+        <a-row :gutter="24">
           <a-col :span="12">
             <a-form-item label="厂商" name="provider">
-              <a-select v-model:value="form.provider" placeholder="请选择厂商" @change="handleProviderChange" style="width: 100%">
+              <a-select v-model:value="form.provider" placeholder="请选择厂商" @change="handleProviderChange">
                 <a-select-option v-for="item in providerOptions" :key="item.value" :value="item.value">
-                  <span>{{ item.label }}</span>
-                  <span class="provider-url">{{ item.url }}</span>
+                  {{ item.label }}
                 </a-select-option>
               </a-select>
             </a-form-item>
           </a-col>
           <a-col :span="12">
             <a-form-item label="模型代码" name="modelCode">
-              <a-input v-model:value="form.modelCode" placeholder="厂商模型标识,如:gpt-4o、glm-4" />
+              <a-input v-model:value="form.modelCode" placeholder="如:gpt-4o、glm-4" />
             </a-form-item>
           </a-col>
         </a-row>
 
-        <a-divider orientation="left">
-          <span>API配置</span>
-          <a-tooltip placement="top">
-            <template #title>
-              <div style="max-width: 280px;">
-                <p style="margin-bottom: 8px;"><b>如何获取 API Key?</b></p>
-                <p>• OpenAI: platform.openai.com</p>
-                <p>• 智谱AI: open.bigmodel.cn</p>
-                <p>• 阿里云: dashscope.console.aliyun.com</p>
-                <p>• 月之暗面: platform.moonshot.cn</p>
-                <p>• DeepSeek: platform.deepseek.com</p>
-              </div>
-            </template>
-            <QuestionCircleOutlined class="help-icon" />
-          </a-tooltip>
-        </a-divider>
-
-        <a-form-item label="API地址" name="apiUrl">
-          <a-input v-model:value="form.apiUrl" placeholder="选择厂商后自动填充,也可手动修改" />
-        </a-form-item>
+        <a-divider orientation="left">API配置</a-divider>
 
-        <a-row :gutter="16">
+        <a-row :gutter="24">
+          <a-col :span="24">
+            <a-form-item label="API地址" name="apiUrl" :label-col="{ span: 3 }" :wrapper-col="{ span: 20 }">
+              <a-input v-model:value="form.apiUrl" placeholder="选择厂商后自动填充,也可手动修改" />
+            </a-form-item>
+          </a-col>
+        </a-row>
+        
+        <a-row :gutter="24">
           <a-col :span="12">
             <a-form-item label="API密钥" name="apiKey">
-              <div class="key-input-wrapper">
-                <a-input v-model:value="form.apiKey" placeholder="从厂商平台获取的 API Key" />
-                <a-button 
-                  v-if="form.serviceId && isKeyMasked" 
-                  class="view-key-btn"
-                  type="link" 
-                  @click="handleViewKey"
-                  v-hasPermi="['ai:service:viewkey']"
-                >
-                  <template #icon><EyeOutlined /></template>
-                  查看原文
-                </a-button>
-              </div>
+              <a-input-password v-model:value="form.apiKey" placeholder="API Key" />
             </a-form-item>
           </a-col>
           <a-col :span="12">
             <a-form-item label="API Secret" name="apiSecret">
-              <a-input v-model:value="form.apiSecret" placeholder="部分厂商需要(如百度)" />
+              <a-input-password v-model:value="form.apiSecret" placeholder="部分厂商需要" />
+            </a-form-item>
+          </a-col>
+        </a-row>
+        
+        <a-row v-if="form.serviceId && isKeyMasked">
+          <a-col :span="24">
+            <a-form-item :wrapper-col="{ offset: 3 }">
+              <a-button type="link" size="small" @click="handleViewKey" v-hasPermi="['ai:service:viewkey']" style="padding: 0;">
+                <EyeOutlined /> 查看完整密钥
+              </a-button>
             </a-form-item>
           </a-col>
         </a-row>
 
-        <a-divider orientation="left">
-          <span>模型参数</span>
-          <a-tooltip placement="top">
-            <template #title>
-              <div style="max-width: 280px;">
-                <p><b>温度</b>: 控制随机性,0=确定,0.7=推荐,1.5+=创意</p>
-                <p style="margin-top: 6px;"><b>Top P</b>: 核采样,0.95推荐</p>
-                <p style="margin-top: 6px;"><b>最大Token</b>: 回复长度上限,1Token≈0.75中文字</p>
-              </div>
-            </template>
-            <QuestionCircleOutlined class="help-icon" />
-          </a-tooltip>
-        </a-divider>
+        <a-divider orientation="left">模型参数</a-divider>
 
-        <a-row :gutter="16">
+        <a-row :gutter="24">
           <a-col :span="8">
-            <a-form-item label="温度" name="temperature">
+            <a-form-item label="温度" name="temperature" :label-col="{ span: 8 }" :wrapper-col="{ span: 14 }">
               <a-input-number v-model:value="form.temperature" :min="0" :max="2" :step="0.1" :precision="2" style="width: 100%" />
             </a-form-item>
           </a-col>
           <a-col :span="8">
-            <a-form-item label="Top P" name="topP">
+            <a-form-item label="Top P" name="topP" :label-col="{ span: 8 }" :wrapper-col="{ span: 14 }">
               <a-input-number v-model:value="form.topP" :min="0" :max="1" :step="0.05" :precision="2" style="width: 100%" />
             </a-form-item>
           </a-col>
           <a-col :span="8">
-            <a-form-item label="最大Token" name="maxTokens">
+            <a-form-item label="最大Token" name="maxTokens" :label-col="{ span: 10 }" :wrapper-col="{ span: 14 }">
               <a-input-number v-model:value="form.maxTokens" :min="1" :max="128000" :step="1024" style="width: 100%" />
             </a-form-item>
           </a-col>
         </a-row>
-
-        <a-form-item label="系统提示词" name="systemPrompt">
-          <a-textarea v-model:value="form.systemPrompt" :rows="3" placeholder="设定AI角色,如:你是一个专业、友好的AI助手" />
-        </a-form-item>
-
-        <a-divider orientation="left">
-          <span>支持的能力</span>
-          <a-tooltip placement="top">
-            <template #title>
-              <div style="max-width: 320px;">
-                <p style="margin-bottom: 8px;">勾选此服务支持的能力,对话时会显示对应按钮</p>
-                <p><b>流式输出</b>: 逐字显示回复,体验更好</p>
-                <p><b>视觉理解</b>: 可上传图片分析(GPT-4o/GLM-4V)</p>
-                <p><b>工具调用</b>: Function Calling能力</p>
-                <p><b>深度思考</b>: AI先推理再回答(智谱/DeepSeek)</p>
-                <p><b>联网搜索</b>: 获取最新信息(智谱/Kimi)</p>
-                <p><b>文件解析</b>: 上传文件分析(Kimi/通义)</p>
-              </div>
-            </template>
-            <QuestionCircleOutlined class="help-icon" />
-          </a-tooltip>
-        </a-divider>
-
-        <a-row :gutter="16">
-          <a-col :span="8">
-            <a-form-item label="流式输出">
-              <a-switch v-model:checked="form.supportsStream" checkedValue="1" unCheckedValue="0" />
-            </a-form-item>
-          </a-col>
-          <a-col :span="8">
-            <a-form-item label="视觉理解">
-              <a-switch v-model:checked="form.supportsVision" checkedValue="1" unCheckedValue="0" />
-            </a-form-item>
-          </a-col>
-          <a-col :span="8">
-            <a-form-item label="工具调用">
-              <a-switch v-model:checked="form.supportsTools" checkedValue="1" unCheckedValue="0" />
+        
+        <a-row :gutter="24">
+          <a-col :span="24">
+            <a-form-item label="系统提示词" name="systemPrompt" :label-col="{ span: 3 }" :wrapper-col="{ span: 20 }">
+              <a-textarea v-model:value="form.systemPrompt" :rows="2" placeholder="设定AI角色,如:你是一个专业的AI助手" />
             </a-form-item>
           </a-col>
         </a-row>
 
-        <a-row :gutter="16">
-          <a-col :span="8">
-            <a-form-item label="深度思考">
-              <a-switch v-model:checked="form.supportsThinking" checkedValue="1" unCheckedValue="0" />
-            </a-form-item>
-          </a-col>
-          <a-col :span="8">
-            <a-form-item label="联网搜索">
-              <a-switch v-model:checked="form.supportsSearch" checkedValue="1" unCheckedValue="0" />
-            </a-form-item>
-          </a-col>
-          <a-col :span="8">
-            <a-form-item label="文件解析">
-              <a-switch v-model:checked="form.supportsFiles" checkedValue="1" unCheckedValue="0" />
+        <a-divider orientation="left">支持能力</a-divider>
+
+        <a-row :gutter="24">
+          <a-col :span="24">
+            <a-form-item label="能力配置" :label-col="{ span: 3 }" :wrapper-col="{ span: 20 }">
+              <a-checkbox-group v-model:value="capabilitiesChecked" :options="capabilityOptions" />
             </a-form-item>
           </a-col>
         </a-row>
 
         <a-divider orientation="left">状态设置</a-divider>
 
-        <a-row :gutter="16">
+        <a-row :gutter="24">
           <a-col :span="8">
-            <a-form-item label="启用状态">
+            <a-form-item label="启用状态" :label-col="{ span: 10 }" :wrapper-col="{ span: 14 }">
               <a-switch v-model:checked="form.enabled" checkedValue="1" unCheckedValue="0" />
             </a-form-item>
           </a-col>
           <a-col :span="8">
-            <a-form-item label="排序" name="sortOrder">
+            <a-form-item label="排序" name="sortOrder" :label-col="{ span: 8 }" :wrapper-col="{ span: 14 }">
               <a-input-number v-model:value="form.sortOrder" :min="0" style="width: 100%" />
             </a-form-item>
           </a-col>
         </a-row>
-
-        <a-form-item label="备注" name="remark">
-          <a-input v-model:value="form.remark" placeholder="备注信息" />
-        </a-form-item>
+        
+        <a-row :gutter="24">
+          <a-col :span="24">
+            <a-form-item label="备注" name="remark" :label-col="{ span: 3 }" :wrapper-col="{ span: 20 }">
+              <a-input v-model:value="form.remark" placeholder="备注信息" />
+            </a-form-item>
+          </a-col>
+        </a-row>
       </a-form>
       
       <template #footer>
@@ -320,6 +245,7 @@
 <script setup name="AiService">
 import { SearchOutlined, ReloadOutlined, PlusOutlined, EditOutlined, DeleteOutlined, StarOutlined } from '@ant-design/icons-vue'
 import { VideoCameraOutlined, PictureOutlined, SettingOutlined, QuestionCircleOutlined, ThunderboltOutlined, FileOutlined, EyeOutlined } from '@ant-design/icons-vue'
+import { Modal, message } from 'ant-design-vue'
 import { listService, getService, addService, updateService, delService, setDefaultService, viewApiKey } from '@/api/system/ai/service'
 
 const { proxy } = getCurrentInstance()
@@ -336,13 +262,13 @@ const title = ref('')
 const open = ref(false)
 
 const columns = [
-  { title: '服务名称', key: 'serviceName', dataIndex: 'serviceName', minWidth: 140 },
-  { title: '服务代码', dataIndex: 'serviceCode', key: 'serviceCode', width: 150 },
-  { title: '厂商', key: 'providerName', width: 100, align: 'center' },
-  { title: '模型', dataIndex: 'modelCode', key: 'modelCode', width: 150 },
-  { title: '支持能力', key: 'capabilities', width: 180, align: 'center' },
+  { title: '服务名称', key: 'serviceName', dataIndex: 'serviceName', width: 120, ellipsis: true },
+  { title: '服务代码', dataIndex: 'serviceCode', key: 'serviceCode', width: 140, ellipsis: true },
+  { title: '厂商', key: 'providerName', width: 90, align: 'center', ellipsis: true },
+  { title: '模型', dataIndex: 'modelCode', key: 'modelCode', width: 140, ellipsis: true },
+  { title: '支持能力', key: 'capabilities', width: 220, align: 'center' },
   { title: '状态', key: 'enabled', width: 80, align: 'center' },
-  { title: '操作', key: 'action', width: 150, align: 'center', fixed: 'right' }
+  { title: '操作', key: 'action', width: 120, align: 'center', fixed: 'right' }
 ]
 
 const providerOptions = ref([
@@ -381,6 +307,38 @@ const isKeyMasked = computed(() => {
   return form.value.apiKey && form.value.apiKey.includes('****')
 })
 
+// 能力配置选项
+const capabilityOptions = [
+  { label: '流式输出', value: 'supportsStream' },
+  { label: '视觉理解', value: 'supportsVision' },
+  { label: '工具调用', value: 'supportsTools' },
+  { label: '深度思考', value: 'supportsThinking' },
+  { label: '联网搜索', value: 'supportsSearch' },
+  { label: '文件解析', value: 'supportsFiles' }
+]
+
+// 能力配置双向绑定
+const capabilitiesChecked = computed({
+  get() {
+    const checked = []
+    if (form.value.supportsStream === '1') checked.push('supportsStream')
+    if (form.value.supportsVision === '1') checked.push('supportsVision')
+    if (form.value.supportsTools === '1') checked.push('supportsTools')
+    if (form.value.supportsThinking === '1') checked.push('supportsThinking')
+    if (form.value.supportsSearch === '1') checked.push('supportsSearch')
+    if (form.value.supportsFiles === '1') checked.push('supportsFiles')
+    return checked
+  },
+  set(val) {
+    form.value.supportsStream = val.includes('supportsStream') ? '1' : '0'
+    form.value.supportsVision = val.includes('supportsVision') ? '1' : '0'
+    form.value.supportsTools = val.includes('supportsTools') ? '1' : '0'
+    form.value.supportsThinking = val.includes('supportsThinking') ? '1' : '0'
+    form.value.supportsSearch = val.includes('supportsSearch') ? '1' : '0'
+    form.value.supportsFiles = val.includes('supportsFiles') ? '1' : '0'
+  }
+})
+
 const rules = {
   serviceName: [{ required: true, message: '请输入服务名称', trigger: 'blur' }],
   serviceCode: [{ required: true, message: '请输入服务代码', trigger: 'blur' }],
@@ -461,15 +419,19 @@ function handleUpdate(row) {
 
 function handleViewKey() {
   if (!form.value.serviceId) return
-  proxy.$modal.confirm('查看完整API Key是敏感操作,确认继续?').then(() => {
-    viewApiKey(form.value.serviceId).then(response => {
-      if (response.data) {
-        form.value.apiKey = response.data.apiKey || ''
-        form.value.apiSecret = response.data.apiSecret || ''
-        proxy.$modal.msgSuccess('已显示完整密钥')
-      }
-    })
-  }).catch(() => {})
+  Modal.confirm({
+    title: '提示',
+    content: '查看完整API Key是敏感操作,确认继续?',
+    onOk() {
+      return viewApiKey(form.value.serviceId).then(response => {
+        if (response.data) {
+          form.value.apiKey = response.data.apiKey || ''
+          form.value.apiSecret = response.data.apiSecret || ''
+          message.success('已显示完整密钥')
+        }
+      })
+    }
+  })
 }
 
 function handleProviderChange(val) {
@@ -486,13 +448,13 @@ function submitForm() {
   proxy.$refs['serviceRef'].validate().then(() => {
     if (form.value.serviceId) {
       updateService(form.value).then(() => {
-        proxy.$modal.msgSuccess('修改成功')
+        message.success('修改成功')
         open.value = false
         getList()
       })
     } else {
       addService(form.value).then(() => {
-        proxy.$modal.msgSuccess('新增成功')
+        message.success('新增成功')
         open.value = false
         getList()
       })
@@ -507,32 +469,45 @@ function cancel() {
 
 function handleDelete(row) {
   const serviceIds = row.serviceId ? [row.serviceId] : ids.value
-  proxy.$modal.confirm('确认删除选中的AI服务吗?').then(() => {
-    return delService(serviceIds)
-  }).then(() => {
-    getList()
-    proxy.$modal.msgSuccess('删除成功')
-  }).catch(() => {})
+  Modal.confirm({
+    title: '提示',
+    content: '确认删除选中的AI服务吗?',
+    onOk() {
+      return delService(serviceIds).then(() => {
+        getList()
+        message.success('删除成功')
+      })
+    }
+  })
 }
 
 function handleStatusChange(row) {
   const text = row.enabled === '1' ? '启用' : '停用'
-  proxy.$modal.confirm(`确认${text}「${row.serviceName}」吗?`).then(() => {
-    return updateService({ serviceId: row.serviceId, enabled: row.enabled })
-  }).then(() => {
-    proxy.$modal.msgSuccess(`${text}成功`)
-  }).catch(() => {
-    row.enabled = row.enabled === '1' ? '0' : '1'
+  Modal.confirm({
+    title: '提示',
+    content: `确认${text}「${row.serviceName}」吗?`,
+    onOk() {
+      return updateService({ serviceId: row.serviceId, enabled: row.enabled }).then(() => {
+        message.success(`${text}成功`)
+      })
+    },
+    onCancel() {
+      row.enabled = row.enabled === '1' ? '0' : '1'
+    }
   })
 }
 
 function handleSetDefault(row) {
-  proxy.$modal.confirm(`确认将「${row.serviceName}」设为默认服务吗?`).then(() => {
-    return setDefaultService(row.serviceId)
-  }).then(() => {
-    getList()
-    proxy.$modal.msgSuccess('设置成功')
-  }).catch(() => {})
+  Modal.confirm({
+    title: '提示',
+    content: `确认将「${row.serviceName}」设为默认服务吗?`,
+    onOk() {
+      return setDefaultService(row.serviceId).then(() => {
+        getList()
+        message.success('设置成功')
+      })
+    }
+  })
 }
 
 function getProviderTagColor(provider) {
@@ -552,18 +527,106 @@ function getProviderTagColor(provider) {
 getList()
 </script>
 
-<style scoped>
-.search-form {
+<style lang="scss" scoped>
+// 搜索表单布局
+.search-form.ant-form-inline {
+  background: #fff;
+  padding: 0;
+  border-radius: 4px;
+  
+  .ant-form-item {
+    margin-right: 16px;
+    margin-bottom: 16px;
+    display: inline-flex;
+    flex-wrap: nowrap;
+    align-items: center;
+    
+    .ant-form-item-label {
+      padding-right: 8px;
+      flex-shrink: 0;
+      
+      > label {
+        white-space: nowrap;
+        height: 32px;
+        line-height: 32px;
+        margin-bottom: 0;
+      }
+    }
+    
+    .ant-form-item-control {
+      flex: 0 0 auto;
+      display: inline-block;
+    }
+    
+    .ant-form-item-control-input {
+      min-height: 32px;
+      display: flex;
+      align-items: center;
+    }
+    
+    &:last-child {
+      margin-right: 0;
+    }
+  }
+}
+
+// 按钮工具栏
+.button-toolbar {
   background: #fff;
-  padding: 20px 20px 0;
+  padding: 0;
   border-radius: 4px;
   margin-bottom: 16px;
+  
+  .ant-row {
+    margin: 0;
+    
+    .ant-col {
+      .ant-btn {
+        margin-right: 8px;
+        
+        &:last-child {
+          margin-right: 0;
+        }
+      }
+    }
+  }
+}
+
+// 表格容器
+.ant-table-wrapper {
+  background: #fff;
+  padding: 0;
+  border-radius: 4px;
+}
+
+// 表格样式
+:deep(.ant-table) {
+  .ant-table-thead > tr > th {
+    background-color: #fafafa;
+    font-weight: 600;
+    color: #262626;
+    white-space: nowrap;
+  }
+  
+  .ant-table-tbody > tr > td {
+    padding: 12px 16px;
+    white-space: nowrap;
+  }
+}
+
+// 按钮图标间距
+.ant-btn {
+  .anticon + span,
+  span + .anticon {
+    margin-left: 4px;
+  }
 }
 
 .service-name {
   display: flex;
   align-items: center;
   gap: 8px;
+  white-space: nowrap;
 }
 
 .default-tag {
@@ -574,35 +637,31 @@ getList()
   display: flex;
   align-items: center;
   justify-content: center;
-  gap: 6px;
-  flex-wrap: wrap;
-}
-
-.help-icon {
-  color: #999;
-  font-size: 14px;
-  margin-left: 4px;
-  cursor: help;
-  vertical-align: middle;
-  transition: color 0.2s;
+  gap: 4px;
+  flex-wrap: nowrap;
 }
 
-.help-icon:hover {
-  color: #1890ff;
-}
-
-.provider-url {
-  float: right;
-  color: #999;
-  font-size: 12px;
-}
-
-.key-input-wrapper {
-  width: 100%;
-}
-
-.view-key-btn {
-  margin-top: 4px;
-  font-size: 12px;
+// 弹窗表单样式
+:deep(.ant-modal) {
+  .ant-divider {
+    margin: 16px 0 12px;
+    
+    .ant-divider-inner-text {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+    }
+  }
+  
+  .ant-form-item {
+    margin-bottom: 16px;
+  }
+  
+  .ant-checkbox-group {
+    .ant-checkbox-wrapper {
+      margin-right: 24px;
+      margin-bottom: 8px;
+    }
+  }
 }
 </style>

+ 55 - 96
yushu-uivue3/src/views/system/user/profile/index.vue

@@ -1,57 +1,50 @@
 <template>
    <div class="app-container">
-      <a-row :gutter="20">
-         <a-col :span="6" :xs="24">
-            <a-card class="box-card" :bordered="false">
+      <div class="profile-wrapper">
+         <div class="profile-left">
+            <a-card :bordered="false">
                <template #title>
-                 <div class="card-header">
-                   <span>个人信息</span>
-                 </div>
+                  <span>个人信息</span>
                </template>
-               <div class="profile-box">
+               <div>
                   <div class="text-center">
                      <userAvatar />
-                     <div class="user-name">{{ state.user.nickName || state.user.userName }}</div>
-                     <div class="user-role">{{ state.roleGroup }}</div>
                   </div>
                   <ul class="list-group list-group-striped">
                      <li class="list-group-item">
-                        <svg-icon icon-class="user" />
-                        <span class="list-label">用户名称</span>
+                        <svg-icon icon-class="user" />用户名称
                         <div class="pull-right">{{ state.user.userName }}</div>
                      </li>
                      <li class="list-group-item">
-                        <svg-icon icon-class="phone" />
-                        <span class="list-label">手机号码</span>
+                        <svg-icon icon-class="phone" />手机号码
                         <div class="pull-right">{{ state.user.phonenumber }}</div>
                      </li>
                      <li class="list-group-item">
-                        <svg-icon icon-class="email" />
-                        <span class="list-label">用户邮箱</span>
+                        <svg-icon icon-class="email" />用户邮箱
                         <div class="pull-right">{{ state.user.email }}</div>
                      </li>
                      <li class="list-group-item">
-                        <svg-icon icon-class="tree" />
-                        <span class="list-label">所属部门</span>
+                        <svg-icon icon-class="tree" />所属部门
                         <div class="pull-right" v-if="state.user.dept">{{ state.user.dept.deptName }} / {{ state.postGroup }}</div>
                      </li>
                      <li class="list-group-item">
-                        <svg-icon icon-class="date" />
-                        <span class="list-label">创建日期</span>
+                        <svg-icon icon-class="peoples" />所属角色
+                        <div class="pull-right">{{ state.roleGroup }}</div>
+                     </li>
+                     <li class="list-group-item">
+                        <svg-icon icon-class="date" />创建日期
                         <div class="pull-right">{{ state.user.createTime }}</div>
                      </li>
                   </ul>
                </div>
             </a-card>
-         </a-col>
-         <a-col :span="18" :xs="24">
-            <a-card class="box-card" :bordered="false">
+         </div>
+         <div class="profile-right">
+            <a-card :bordered="false">
                <template #title>
-                 <div class="card-header">
-                   <span>基本资料</span>
-                 </div>
+                  <span>基本资料</span>
                </template>
-               <a-tabs v-model:activeKey="selectedTab" class="custom-tabs">
+               <a-tabs v-model:activeKey="selectedTab">
                   <a-tab-pane key="userinfo" tab="基本资料">
                      <userInfo :user="state.user" />
                   </a-tab-pane>
@@ -60,8 +53,8 @@
                   </a-tab-pane>
                </a-tabs>
             </a-card>
-         </a-col>
-      </a-row>
+         </div>
+      </div>
    </div>
 </template>
 
@@ -97,90 +90,56 @@ onMounted(() => {
 </script>
 
 <style lang="scss" scoped>
-.app-container {
-  padding: 20px;
-  background-color: #f5f7fa;
-  min-height: calc(100vh - 84px);
-}
-
-.box-card {
-  border: none;
-  border-radius: 8px;
-  background: #fff;
-  margin-bottom: 20px;
-  box-shadow: 0 1px 4px rgba(0,21,41,.08);
-}
-
-.card-header {
-  font-size: 16px;
-  font-weight: 600;
-  color: #333;
-  line-height: 1.5;
-}
-
-.profile-box {
-  padding: 10px 0;
+.profile-wrapper {
+  display: flex;
+  gap: 20px;
   
-  .user-name {
-    font-size: 20px;
-    font-weight: 600;
-    margin-top: 15px;
-    color: #303133;
+  .profile-left {
+    width: 300px;
+    flex-shrink: 0;
   }
   
-  .user-role {
-    font-size: 13px;
-    color: #909399;
-    margin-top: 5px;
-    margin-bottom: 20px;
+  .profile-right {
+    flex: 1;
+  }
+}
+
+@media (max-width: 768px) {
+  .profile-wrapper {
+    flex-direction: column;
+    
+    .profile-left {
+      width: 100%;
+    }
   }
 }
 
+.list-group-striped > .list-group-item {
+  border-left: 0;
+  border-right: 0;
+  border-radius: 0;
+  padding-left: 0;
+  padding-right: 0;
+}
+
 .list-group {
   padding-left: 0;
   list-style: none;
-  border-top: 1px solid #ebeef5;
 }
 
 .list-group-item {
-  border-bottom: 1px solid #ebeef5;
-  padding: 14px 0;
-  font-size: 14px;
-  color: #606266;
-  display: flex;
-  align-items: center;
+  border-bottom: 1px solid #e7eaec;
+  border-top: 1px solid #e7eaec;
+  margin-bottom: -1px;
+  padding: 11px 0;
+  font-size: 13px;
   
-  .svg-icon {
-    margin-right: 10px;
-    font-size: 16px;
-    color: #909399;
-  }
-  
-  .list-label {
-    flex: 1;
+  svg {
+    margin-right: 5px;
   }
   
   .pull-right {
-    color: #303133;
-    font-weight: 500;
-  }
-}
-
-.custom-tabs {
-  :deep(.ant-tabs-nav::before) {
-    border-bottom: none;
-  }
-  
-  :deep(.ant-tabs-tab) {
-    font-size: 15px;
-    color: #606266;
-    
-    &.ant-tabs-tab-active {
-      .ant-tabs-tab-btn {
-        color: var(--ant-primary-color);
-        font-weight: 600;
-      }
-    }
+    float: right;
   }
 }
 

+ 3 - 3
yushu-uivue3/src/views/system/user/profile/resetPwd.vue

@@ -1,5 +1,5 @@
 <template>
-   <a-form ref="pwdRef" :model="user" :rules="rules" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
+   <a-form ref="pwdRef" :model="user" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }">
       <a-form-item label="旧密码" name="oldPassword">
          <a-input-password v-model:value="user.oldPassword" placeholder="请输入旧密码" />
       </a-form-item>
@@ -9,9 +9,9 @@
       <a-form-item label="确认密码" name="confirmPassword">
          <a-input-password v-model:value="user.confirmPassword" placeholder="请确认新密码" />
       </a-form-item>
-      <a-form-item :wrapper-col="{ offset: 4 }">
+      <a-form-item :wrapper-col="{ offset: 6 }">
         <a-button type="primary" @click="submit">保存</a-button>
-        <a-button danger style="margin-left: 8px" @click="close">关闭</a-button>
+        <a-button type="danger" style="margin-left: 10px" @click="close">关闭</a-button>
       </a-form-item>
    </a-form>
 </template>

+ 32 - 55
yushu-uivue3/src/views/system/user/profile/userAvatar.vue

@@ -1,10 +1,7 @@
 <template>
   <div class="user-info-head" @click="editCropper()">
     <img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
-    <div class="mask">
-      <CameraOutlined />
-    </div>
-    <a-modal :title="title" v-model:open="open" width="800px" @afterClose="closeDialog" class="avatar-dialog" :footer="null">
+    <a-modal :title="title" v-model:open="open" width="800px" @afterClose="closeDialog" :footer="null">
       <a-row :gutter="20">
         <a-col :xs="24" :md="12" :style="{ height: '350px' }">
           <vue-cropper
@@ -21,11 +18,8 @@
           />
         </a-col>
         <a-col :xs="24" :md="12" :style="{ height: '350px' }">
-          <div class="avatar-preview-wrapper">
-            <div class="avatar-upload-preview">
-              <img :src="options.previews.url" :style="options.previews.img" />
-            </div>
-            <div class="preview-text">预览</div>
+          <div class="avatar-upload-preview">
+            <img :src="options.previews.url" :style="options.previews.img" />
           </div>
         </a-col>
       </a-row>
@@ -68,7 +62,7 @@ import "vue-cropper/dist/index.css"
 import { VueCropper } from "vue-cropper"
 import { uploadAvatar } from "@/api/system/user"
 import useUserStore from "@/store/modules/user"
-import { CameraOutlined, UploadOutlined, PlusOutlined, MinusOutlined, UndoOutlined, RedoOutlined } from '@ant-design/icons-vue'
+import { UploadOutlined, PlusOutlined, MinusOutlined, UndoOutlined, RedoOutlined } from '@ant-design/icons-vue'
 import { message } from 'ant-design-vue'
 
 const userStore = useUserStore()
@@ -163,61 +157,44 @@ function closeDialog() {
   position: relative;
   display: inline-block;
   height: 120px;
-  width: 120px;
-  border-radius: 50%;
-  overflow: hidden;
+}
+
+.user-info-head:hover:after {
+  content: '+';
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  color: #eee;
+  background: rgba(0, 0, 0, 0.5);
+  font-size: 24px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
   cursor: pointer;
-  border: 1px solid #dcdfe6;
-  
-  img {
-    width: 100%;
-    height: 100%;
-    object-fit: cover;
-  }
-  
-  .mask {
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    background: rgba(0, 0, 0, 0.4);
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    color: #fff;
-    font-size: 28px;
-    opacity: 0;
-    transition: opacity 0.3s;
-  }
-  
-  &:hover {
-    .mask {
-      opacity: 1;
-    }
-  }
+  line-height: 110px;
+  border-radius: 50%;
 }
 
-.avatar-preview-wrapper {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  height: 100%;
+.img-circle {
+  border-radius: 50%;
+}
+
+.img-lg {
+  width: 120px;
+  height: 120px;
 }
 
 .avatar-upload-preview {
+  position: relative;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
   width: 200px;
   height: 200px;
   border-radius: 50%;
   box-shadow: 0 0 4px #ccc;
   overflow: hidden;
 }
-
-.preview-text {
-  text-align: center;
-  color: #909399;
-  font-size: 14px;
-  margin-top: 15px;
-}
 </style>

+ 4 - 15
yushu-uivue3/src/views/system/user/profile/userInfo.vue

@@ -1,5 +1,5 @@
 <template>
-   <a-form ref="userRef" :model="form" :rules="rules" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }" class="user-info-form">
+   <a-form ref="userRef" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }">
       <a-form-item label="用户昵称" name="nickName">
          <a-input v-model:value="form.nickName" :maxlength="30" />
       </a-form-item>
@@ -15,9 +15,9 @@
             <a-radio value="1">女</a-radio>
          </a-radio-group>
       </a-form-item>
-      <a-form-item class="form-actions" :wrapper-col="{ offset: 4 }">
+      <a-form-item :wrapper-col="{ offset: 6 }">
          <a-button type="primary" @click="submit">保存</a-button>
-         <a-button danger style="margin-left: 8px" @click="close">关闭</a-button>
+         <a-button type="danger" style="margin-left: 10px" @click="close">关闭</a-button>
       </a-form-item>
    </a-form>
 </template>
@@ -62,16 +62,5 @@ watch(() => props.user, user => {
   if (user) {
     form.value = { nickName: user.nickName, phonenumber: user.phonenumber, email: user.email, sex: user.sex }
   }
-},{ immediate: true })
+}, { immediate: true })
 </script>
-
-<style lang="scss" scoped>
-.user-info-form {
-  max-width: 600px;
-  padding-top: 10px;
-  
-  .form-actions {
-    margin-top: 30px;
-  }
-}
-</style>