Просмотр исходного кода

新增团队成员管理视图,支持成员列表展示、搜索、编辑和删除功能,完善团队相关的API接口,调整路由以支持成员信息子项。

wuyi 4 месяцев назад
Родитель
Сommit
3f8e238e2f

+ 16 - 0
src/main.js

@@ -28,4 +28,20 @@ app.provide(
     })
 )
 
+app.provide(
+    'isTeam',
+    computed(() => {
+        if (!(useUserStore().userInfo && useUserStore().userInfo.role)) return false
+        return useUserStore().userInfo.role === 'team'
+    })
+)
+
+app.provide(
+    'isUser',
+    computed(() => {
+        if (!(useUserStore().userInfo && useUserStore().userInfo.role)) return false
+        return useUserStore().userInfo.role === 'user'
+    })
+)
+
 app.mount('#app')

+ 14 - 0
src/router/index.js

@@ -3,6 +3,7 @@ import MainView from '@/views/MainView.vue'
 import DashboardView from '@/views/DashboardView.vue'
 import LoginView from '@/views/LoginView.vue'
 import { useUserStore } from '@/stores/user'
+import { useTeamStore } from '@/stores/team'
 
 const router = createRouter({
   history: createWebHistory('/admin/'),
@@ -54,6 +55,11 @@ const router = createRouter({
               path: 'info',
               name: 'team-info',
               component: () => import('@/views/TeamView.vue')
+            },
+            {
+              path: 'members',
+              name: 'team-members',
+              component: () => import('@/views/TeamMembersView.vue')
             }
           ]
         },
@@ -79,11 +85,19 @@ const router = createRouter({
 
 router.beforeEach(async (to, from, next) => {
   const userStore = useUserStore()
+  const teamStore = useTeamStore()
+
   if (to.meta.requiresAuth) {
     if (userStore.token && userStore.userInfo.id) {
+      if (userStore.userInfo.role === 'admin') {
+        await teamStore.loadTeams()
+      }
       next()
     } else if (userStore.token) {
       await userStore.sync()
+      if (userStore.userInfo.role === 'admin') {
+        await teamStore.loadTeams()
+      }
       next()
     } else {
       next('/login')

+ 49 - 0
src/services/api.js

@@ -315,6 +315,55 @@ export const getTeamStatistics = async () => {
   return response.data
 }
 
+// 获取所有团队
+export const getAllTeams = async () => {
+  const response = await api.get('/teams/all')
+  return response.data
+}
+
+
+// ==================== 团队成员管理相关API ====================
+
+// 获取团队成员列表
+export const listMembers = async (page = 0, size = 20, name, teamId, userId) => {
+  const params = new URLSearchParams({ page, size })
+  if (name) params.append('name', name)
+  if (teamId) params.append('teamId', teamId)
+  if (userId) params.append('userId', userId)
+  const response = await api.get(`/members?${params}`)
+  return response.data
+}
+
+// 创建团队成员
+export const createMember = async (memberData) => {
+  const response = await api.post('/members', memberData)
+  return response.data
+}
+
+// 更新团队成员
+export const updateMember = async (id, memberData) => {
+  const response = await api.put(`/members/${id}`, memberData)
+  return response.data
+}
+
+// 删除团队成员
+export const deleteMember = async (id) => {
+  const response = await api.delete(`/members/${id}`)
+  return response.data
+}
+
+// 更新团队成员收入
+export const updateMemberRevenue = async (id, revenueData) => {
+  const response = await api.put(`/members/${id}/revenue`, revenueData)
+  return response.data
+}
+
+// 获取团队成员统计数据
+export const getMemberStatistics = async () => {
+  const response = await api.get('/members/statistics/summary')
+  return response.data
+}
+
 // ==================== 推广链接相关API ====================
 
 // 创建推广链接

+ 0 - 12
src/stores/counter.js

@@ -1,12 +0,0 @@
-import { ref, computed } from 'vue'
-import { defineStore } from 'pinia'
-
-export const useCounterStore = defineStore('counter', () => {
-  const count = ref(0)
-  const doubleCount = computed(() => count.value * 2)
-  function increment() {
-    count.value++
-  }
-
-  return { count, doubleCount, increment }
-})

+ 0 - 46
src/stores/owner.js

@@ -1,46 +0,0 @@
-import { defineStore } from 'pinia'
-import { allUsersApi } from '@/services/api'
-import { ref } from 'vue'
-import { useUserStore } from '@/stores/user'
-
-export const useOwnerStore = defineStore('owner', () => {
-  const owners = ref([])
-  const isLoading = ref(false)
-  const error = ref(null)
-
-  const loadOwners = async () => {
-    if (owners.value.length > 0) return
-    
-    const userStore = useUserStore()
-    if (userStore.userInfo && userStore.userInfo.role !== 'admin') return
-    
-    try {
-      isLoading.value = true
-      error.value = null
-      const data = await allUsersApi()
-      owners.value = [
-        { name: '全部用户', value: '' },
-        ...data.map(user => ({ name: user.name, value: user.id }))
-      ]
-    } catch (err) {
-      error.value = '加载用户列表失败'
-      console.error('加载用户列表失败:', err)
-    } finally {
-      isLoading.value = false
-    }
-  }
-
-  const reset = () => {
-    owners.value = []
-    isLoading.value = false
-    error.value = null
-  }
-
-  return {
-    owners,
-    isLoading,
-    error,
-    loadOwners,
-    reset
-  }
-})

+ 44 - 0
src/stores/team.js

@@ -0,0 +1,44 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import { getAllTeams } from '@/services/api'
+import { useUserStore } from '@/stores/user'
+
+export const useTeamStore = defineStore('team', () => {
+    const teams = ref([])
+    const isLoading = ref(false)
+    const error = ref(null)
+
+    const loadTeams = async () => {
+        const userStore = useUserStore()
+
+        if (!userStore.userInfo || userStore.userInfo.role !== 'admin') {
+            return
+        }
+        if (teams.value.length > 0) return
+
+        try {
+            isLoading.value = true
+            error.value = null
+            const data = await getAllTeams()
+            teams.value = data
+        } catch (err) {
+            console.error('加载团队列表失败:', err)
+        } finally {
+            isLoading.value = false
+        }
+    }
+
+    const reset = () => {
+        teams.value = []
+        isLoading.value = false
+        error.value = null
+    }
+
+    return {
+        teams,
+        isLoading,
+        error,
+        loadTeams,
+        reset
+    }
+})

+ 4 - 3
src/stores/user.js

@@ -2,12 +2,14 @@ import { defineStore } from 'pinia'
 import { ref } from 'vue'
 import { login as apiLogin, profile } from '@/services/api'
 import { useStorage } from '@vueuse/core'
-import { useOwnerStore } from '@/stores/owner'
+import { useTeamStore } from '@/stores/team'
 
 export const useUserStore = defineStore('user', () => {
   const token = useStorage('token', '')
   const userInfo = ref({})
 
+  const teamStore = useTeamStore()
+
   const setToken = (newToken) => {
     token.value = newToken
   }
@@ -31,8 +33,7 @@ export const useUserStore = defineStore('user', () => {
   const logout = () => {
     token.value = ''
     userInfo.value = {}
-    const ownerStore = useOwnerStore()
-    ownerStore.reset()
+    teamStore.reset()
   }
 
   return {

+ 3 - 3
src/views/FinanceView.vue

@@ -37,7 +37,7 @@
             class="w-32"
             @keyup.enter="handleSearch"
           />
-          <Dropdown
+          <Select
             v-model="searchForm.status"
             :options="statusOptions"
             optionLabel="label"
@@ -367,9 +367,9 @@ import { useDateFormat } from '@vueuse/core'
 import Button from 'primevue/button'
 import Column from 'primevue/column'
 import DataTable from 'primevue/datatable'
-import DatePicker from 'primevue/calendar'
+import DatePicker from 'primevue/datepicker'
 import Dialog from 'primevue/dialog'
-import Dropdown from 'primevue/dropdown'
+import Select from 'primevue/select'
 import InputText from 'primevue/inputtext'
 import InputNumber from 'primevue/inputnumber'
 import Textarea from 'primevue/textarea'

+ 8 - 8
src/views/IncomeView.vue

@@ -23,7 +23,7 @@
             class="w-32"
             @keyup.enter="handleSearch"
           />
-          <Dropdown
+          <Select
             v-model="searchForm.incomeType"
             :options="incomeTypeOptions"
             optionLabel="label"
@@ -33,7 +33,7 @@
             class="w-32"
             :showClear="true"
           />
-          <Dropdown
+          <Select
             v-model="searchForm.orderType"
             :options="orderTypeOptions"
             optionLabel="label"
@@ -43,7 +43,7 @@
             class="w-32"
             :showClear="true"
           />
-          <Dropdown
+          <Select
             v-model="searchForm.payChannel"
             :options="payChannelOptions"
             optionLabel="label"
@@ -272,7 +272,7 @@
         <div class="grid grid-cols-2 gap-4 mt-4">
           <div class="field">
             <label for="edit-incomeType" class="font-medium text-sm mb-2 block">收入类型</label>
-            <Dropdown
+            <Select
               id="edit-incomeType"
               v-model="editForm.incomeType"
               :options="incomeTypeOptions.filter((option) => option.value !== null)"
@@ -304,7 +304,7 @@
 
           <div class="field">
             <label for="edit-orderType" class="font-medium text-sm mb-2 block">订单类型</label>
-            <Dropdown
+            <Select
               id="edit-orderType"
               v-model="editForm.orderType"
               :options="orderTypeOptions.filter((option) => option.value !== null)"
@@ -324,7 +324,7 @@
 
           <div class="field">
             <label for="edit-payChannel" class="font-medium text-sm mb-2 block">支付渠道</label>
-            <Dropdown
+            <Select
               id="edit-payChannel"
               v-model="editForm.payChannel"
               :options="payChannelOptions.filter((option) => option.value !== null)"
@@ -365,9 +365,9 @@ import { useDateFormat } from '@vueuse/core'
 import Button from 'primevue/button'
 import Column from 'primevue/column'
 import DataTable from 'primevue/datatable'
-import DatePicker from 'primevue/calendar'
+import DatePicker from 'primevue/datepicker'
 import Dialog from 'primevue/dialog'
-import Dropdown from 'primevue/dropdown'
+import Select from 'primevue/select'
 import InputText from 'primevue/inputtext'
 import InputNumber from 'primevue/inputnumber'
 import { useConfirm } from 'primevue/useconfirm'

+ 3 - 3
src/views/LinkView.vue

@@ -10,7 +10,7 @@
         class="w-32"
         @keyup.enter="handleSearch"
       />
-      <Dropdown
+      <Select
         v-model="searchForm.type"
         :options="typeOptions"
         optionLabel="label"
@@ -231,7 +231,7 @@
 
         <div class="field mt-4">
           <label for="edit-type" class="font-medium text-sm mb-2 block">链接类型</label>
-          <Dropdown
+          <Select
             id="edit-type"
             v-model="editForm.type"
             :options="typeOptions.filter((option) => option.value !== null)"
@@ -292,7 +292,7 @@ import Button from 'primevue/button'
 import Column from 'primevue/column'
 import DataTable from 'primevue/datatable'
 import Dialog from 'primevue/dialog'
-import Dropdown from 'primevue/dropdown'
+import Select from 'primevue/select'
 import InputText from 'primevue/inputtext'
 import FileUpload from 'primevue/fileupload'
 import { useConfirm } from 'primevue/useconfirm'

+ 5 - 0
src/views/MainView.vue

@@ -40,6 +40,11 @@ const navItems = [
         label: '团队信息',
         icon: 'pi pi-fw pi-info-circle',
         name: 'team-info'
+      },
+      {
+        label: '成员信息',
+        icon: 'pi pi-fw pi-users',
+        name: 'team-members'
       }
     ]
   },

+ 558 - 0
src/views/TeamMembersView.vue

@@ -0,0 +1,558 @@
+<template>
+  <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
+    <DataTable
+      :value="tableData.content"
+      :paginator="true"
+      paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown JumpToPageInput"
+      currentPageReportTemplate="{totalRecords} 条记录 "
+      :rows="tableData.metadata.size"
+      :rowsPerPageOptions="[10, 20, 50, 100]"
+      :totalRecords="tableData.metadata.total"
+      @page="handlePageChange"
+      lazy
+      scrollable
+      class="members-table"
+    >
+      <template #header>
+        <div class="flex flex-wrap items-center gap-2">
+          <InputText v-model="searchForm.id" placeholder="ID" size="small" class="w-32" @keyup.enter="handleSearch" />
+          <InputText
+            v-model="searchForm.name"
+            placeholder="成员名称"
+            size="small"
+            class="w-32"
+            @keyup.enter="handleSearch"
+          />
+          <Select
+            v-model="searchForm.teamId"
+            :options="teamOptions"
+            optionLabel="label"
+            optionValue="value"
+            placeholder="选择团队"
+            size="small"
+            class="w-40"
+            showClear
+          />
+          <Button icon="pi pi-search" @click="handleSearch" label="搜索" size="small" severity="secondary" />
+          <Button icon="pi pi-refresh" @click="handleRefresh" label="刷新" size="small" />
+          <Button icon="pi pi-plus" @click="openAddDialog" label="新增成员" size="small" severity="success" />
+          <div class="flex-1"></div>
+        </div>
+      </template>
+
+      <Column field="id" header="ID" style="width: 80px" frozen>
+        <template #body="slotProps">
+          <span
+            class="font-mono text-sm copyable-text"
+            :title="slotProps.data.id"
+            @click="copyToClipboard(slotProps.data.id)"
+          >
+            {{ slotProps.data.id }}
+          </span>
+        </template>
+      </Column>
+
+      <Column field="name" header="成员名称" style="min-width: 150px; max-width: 200px">
+        <template #body="slotProps">
+          <span
+            class="font-medium member-name-text copyable-text"
+            :title="slotProps.data.name"
+            @click="copyToClipboard(slotProps.data.name)"
+          >
+            {{ slotProps.data.name }}
+          </span>
+        </template>
+      </Column>
+
+      <Column field="teamId" header="所属团队" style="min-width: 120px">
+        <template #body="slotProps">
+          <span class="team-name-text font-medium">
+            {{ getTeamName(slotProps.data.teamId) }}
+          </span>
+        </template>
+      </Column>
+
+      <Column field="totalRevenue" header="总收入" style="min-width: 120px">
+        <template #body="slotProps">
+          <span class="total-revenue-text font-semibold"> {{ formatAmount(slotProps.data.totalRevenue) }} </span>
+        </template>
+      </Column>
+
+      <Column field="todayRevenue" header="今日收入" style="min-width: 120px">
+        <template #body="slotProps">
+          <span class="today-revenue-text font-semibold"> {{ formatAmount(slotProps.data.todayRevenue) }} </span>
+        </template>
+      </Column>
+
+      <Column field="createdAt" header="创建时间" style="min-width: 150px">
+        <template #body="slotProps">
+          <span class="text-sm">
+            {{ formatDateTime(slotProps.data.createdAt) }}
+          </span>
+        </template>
+      </Column>
+
+      <Column
+        header="操作"
+        style="min-width: 200px; width: 200px"
+        align-frozen="right"
+        frozen
+        :pt="{
+          columnHeaderContent: {
+            class: 'justify-center'
+          }
+        }"
+      >
+        <template #body="slotProps">
+          <div class="flex justify-center gap-1">
+            <Button
+              icon="pi pi-pencil"
+              severity="info"
+              size="small"
+              text
+              rounded
+              aria-label="编辑"
+              @click="openEditDialog(slotProps.data)"
+            />
+            <Button
+              icon="pi pi-trash"
+              severity="danger"
+              size="small"
+              text
+              rounded
+              aria-label="删除"
+              @click="confirmDelete(slotProps.data)"
+            />
+          </div>
+        </template>
+      </Column>
+    </DataTable>
+
+    <!-- 新增/编辑弹窗 -->
+    <Dialog
+      v-model:visible="editDialog"
+      :modal="true"
+      :header="isEdit ? '编辑成员' : '新增成员'"
+      :style="{ width: '500px' }"
+      position="center"
+    >
+      <div class="p-fluid">
+        <div class="field">
+          <label for="edit-name" class="font-medium text-sm mb-2 block">成员名称</label>
+          <InputText id="edit-name" v-model="editForm.name" class="w-full" />
+        </div>
+
+        <div class="field mt-4">
+          <label for="edit-teamId" class="font-medium text-sm mb-2 block">选择团队</label>
+          <Select
+            id="edit-teamId"
+            v-model="editForm.teamId"
+            :options="teamSelectOptions"
+            optionLabel="label"
+            optionValue="value"
+            placeholder="请选择团队"
+            class="w-full"
+            showClear
+          />
+        </div>
+
+        <div v-if="!isEdit" class="field mt-4">
+          <label for="edit-password" class="font-medium text-sm mb-2 block">密码</label>
+          <Password
+            id="edit-password"
+            v-model="editForm.password"
+            :feedback="false"
+            toggleMask
+            class="w-full"
+            placeholder="请输入密码"
+            inputClass="w-full"
+          />
+        </div>
+
+        <div v-if="!isEdit" class="field mt-4">
+          <label for="edit-confirmPassword" class="font-medium text-sm mb-2 block">确认密码</label>
+          <Password
+            id="edit-confirmPassword"
+            v-model="editForm.confirmPassword"
+            :feedback="false"
+            toggleMask
+            class="w-full"
+            placeholder="请再次输入密码"
+            inputClass="w-full"
+          />
+        </div>
+      </div>
+
+      <template #footer>
+        <div class="flex justify-end gap-3">
+          <Button label="取消" severity="secondary" @click="editDialog = false" />
+          <Button label="保存" severity="success" @click="saveEdit" :loading="editLoading" />
+        </div>
+      </template>
+    </Dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from 'vue'
+import Button from 'primevue/button'
+import Column from 'primevue/column'
+import DataTable from 'primevue/datatable'
+import Dialog from 'primevue/dialog'
+import Select from 'primevue/select'
+import InputText from 'primevue/inputtext'
+import InputNumber from 'primevue/inputnumber'
+import Password from 'primevue/password'
+import { useConfirm } from 'primevue/useconfirm'
+import { useToast } from 'primevue/usetoast'
+import { listMembers, createMember, updateMember, deleteMember } from '@/services/api'
+import { useTeamStore } from '@/stores/team'
+
+const toast = useToast()
+const confirm = useConfirm()
+const teamStore = useTeamStore()
+
+// 团队选项(用于搜索)
+const teamOptions = computed(() => {
+  return [
+    { label: '全部团队', value: null },
+    ...teamStore.teams.map((team) => ({
+      label: team.name,
+      value: team.id
+    }))
+  ]
+})
+
+// 团队选项(用于新增/编辑,不包含全部团队)
+const teamSelectOptions = computed(() => {
+  return teamStore.teams.map((team) => ({
+    label: team.name,
+    value: team.id
+  }))
+})
+
+// 根据团队ID获取团队名称
+const getTeamName = (teamId) => {
+  if (!teamId) return '-'
+  const team = teamStore.teams.find((t) => t.id === teamId)
+  return team ? team.name : '-'
+}
+
+// 根据团队ID获取团队的userId
+const getTeamUserId = (teamId) => {
+  if (!teamId) return null
+  const team = teamStore.teams.find((t) => t.id === teamId)
+  return team ? team.userId : null
+}
+
+// 表格数据
+const tableData = ref({
+  content: [],
+  metadata: {
+    page: 0,
+    size: 20,
+    total: 0
+  }
+})
+
+// 加载状态
+const loading = ref(false)
+
+// 编辑相关
+const editDialog = ref(false)
+const editLoading = ref(false)
+const isEdit = ref(false)
+const editForm = ref({
+  id: null,
+  name: null,
+  teamId: null
+})
+
+// 搜索表单
+const searchForm = ref({
+  id: null,
+  name: null,
+  teamId: null
+})
+
+// 格式化金额
+const formatAmount = (amount) => {
+  if (!amount) return '0.00'
+  return Number(amount).toFixed(2)
+}
+
+// 格式化日期时间
+const formatDateTime = (dateString) => {
+  if (!dateString) return '-'
+
+  const date = new Date(dateString)
+  const year = date.getFullYear()
+  const month = String(date.getMonth() + 1).padStart(2, '0')
+  const day = String(date.getDate()).padStart(2, '0')
+  const hours = String(date.getHours()).padStart(2, '0')
+  const minutes = String(date.getMinutes()).padStart(2, '0')
+  const seconds = String(date.getSeconds()).padStart(2, '0')
+
+  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
+}
+
+// 获取数据
+const fetchData = async () => {
+  loading.value = true
+  try {
+    const response = await listMembers(
+      tableData.value.metadata.page,
+      tableData.value.metadata.size,
+      searchForm.value.name || undefined,
+      searchForm.value.teamId || undefined
+    )
+    tableData.value = response
+  } catch {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '获取成员列表失败',
+      life: 3000
+    })
+  } finally {
+    loading.value = false
+  }
+}
+
+// 分页处理
+const handlePageChange = (event) => {
+  tableData.value.metadata.page = event.page
+  tableData.value.metadata.size = event.rows
+  fetchData()
+}
+
+// 搜索处理
+const handleSearch = () => {
+  tableData.value.metadata.page = 0
+  fetchData()
+}
+
+// 刷新处理
+const handleRefresh = () => {
+  searchForm.value = {
+    id: null,
+    name: null,
+    teamId: null
+  }
+  tableData.value.metadata.page = 0
+  fetchData()
+}
+
+// 确认删除
+const confirmDelete = (member) => {
+  confirm.require({
+    message: `确定要删除成员 "${member.name}" 吗?`,
+    header: '确认删除',
+    icon: 'pi pi-exclamation-triangle',
+    accept: () => deleteMemberRecord(member.id)
+  })
+}
+
+// 删除成员
+const deleteMemberRecord = async (id) => {
+  try {
+    await deleteMember(id)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '删除成功',
+      life: 3000
+    })
+    fetchData()
+  } catch {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '删除失败',
+      life: 3000
+    })
+  }
+}
+
+// 复制到剪贴板
+const copyToClipboard = async (text) => {
+  try {
+    await navigator.clipboard.writeText(text)
+  } catch {
+    const textArea = document.createElement('textarea')
+    textArea.value = text
+    document.body.appendChild(textArea)
+    textArea.select()
+    document.execCommand('copy')
+    document.body.removeChild(textArea)
+  }
+}
+
+// 打开新增弹窗
+const openAddDialog = () => {
+  isEdit.value = false
+  editForm.value = {
+    id: null,
+    name: null,
+    teamId: null
+  }
+  editDialog.value = true
+}
+
+// 打开编辑弹窗
+const openEditDialog = (member) => {
+  isEdit.value = true
+  editForm.value = {
+    id: member.id,
+    name: member.name || null,
+    teamId: member.teamId || null
+  }
+  editDialog.value = true
+}
+
+// 保存编辑
+const saveEdit = async () => {
+  editLoading.value = true
+  try {
+    // 过滤掉空值参数
+    const formData = {}
+    if (editForm.value.name !== null && editForm.value.name !== '') {
+      formData.name = editForm.value.name
+    }
+    if (editForm.value.teamId !== null && editForm.value.teamId !== '') {
+      formData.teamId = editForm.value.teamId
+      // 使用选中团队的userId作为teamUserId
+      formData.teamUserId = getTeamUserId(editForm.value.teamId)
+    }
+
+    if (isEdit.value) {
+      await updateMember(editForm.value.id, formData)
+    } else {
+      await createMember(formData)
+    }
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: isEdit.value ? '更新成功' : '创建成功',
+      life: 3000
+    })
+    editDialog.value = false
+    fetchData()
+  } catch {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: isEdit.value ? '更新失败' : '创建失败',
+      life: 3000
+    })
+  } finally {
+    editLoading.value = false
+  }
+}
+
+// 初始化
+onMounted(() => {
+  fetchData()
+})
+</script>
+
+<style scoped>
+.p-datatable-sm .p-datatable-tbody > tr > td {
+  padding: 0.5rem;
+  vertical-align: top;
+}
+
+.p-datatable-sm .p-datatable-thead > tr > th {
+  padding: 0.5rem;
+}
+
+.members-table {
+  width: 100%;
+}
+
+.members-table .p-datatable-wrapper {
+  overflow-x: auto;
+}
+
+.members-table .p-datatable-thead th {
+  white-space: nowrap;
+  min-width: 100px;
+}
+
+.font-mono {
+  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+}
+
+.member-name-text {
+  word-wrap: break-word;
+  word-break: break-all;
+  white-space: normal;
+  line-height: 1.4;
+}
+
+.team-name-text {
+  color: #7c3aed;
+  font-weight: 500;
+}
+
+.team-id-text {
+  word-wrap: break-word;
+  word-break: break-all;
+  white-space: normal;
+  line-height: 1.4;
+}
+
+.user-id-text {
+  word-wrap: break-word;
+  word-break: break-all;
+  white-space: normal;
+  line-height: 1.4;
+}
+
+.total-revenue-text {
+  color: #2563eb;
+  font-weight: 600;
+}
+
+.today-revenue-text {
+  color: #059669;
+  font-weight: 600;
+}
+
+.font-medium {
+  font-weight: 500;
+}
+
+.text-sm {
+  font-size: 0.875rem;
+}
+
+.copyable-text {
+  cursor: pointer;
+  transition: all 0.2s ease;
+  user-select: none;
+}
+
+.copyable-text:hover {
+  background-color: #e5e7eb;
+  border-radius: 4px;
+}
+
+.copyable-text:active {
+  background-color: #d1d5db;
+  transform: scale(0.98);
+}
+
+/* Password组件样式修复 */
+.p-password {
+  width: 100%;
+}
+
+.p-password .p-inputtext {
+  width: 100%;
+}
+
+.p-password .p-password-input {
+  width: 100%;
+}
+</style>

+ 2 - 2
src/views/UserView.vue

@@ -8,7 +8,7 @@ import Button from 'primevue/button'
 import Column from 'primevue/column'
 import DataTable from 'primevue/datatable'
 import Dialog from 'primevue/dialog'
-import Dropdown from 'primevue/dropdown'
+import Select from 'primevue/select'
 import FloatLabel from 'primevue/floatlabel'
 import IconField from 'primevue/iconfield'
 import InputIcon from 'primevue/inputicon'
@@ -299,7 +299,7 @@ onMounted(() => {
 
         <div class="field mt-4">
           <FloatLabel variant="on">
-            <Dropdown
+            <Select
               id="role"
               name="role"
               v-model="userForm.role"