Ver código fonte

添加鱼苗好友管理功能,包括API接口、视图组件和导航菜单更新,优化用户体验和界面一致性。

wuyi 4 meses atrás
pai
commit
e5d31761e1

+ 6 - 0
src/enums/index.js

@@ -13,3 +13,9 @@ export const ConfigType = {
   time_range: '时间范围',
   range: '范围'
 }
+
+export const ResultEnum = {
+  success: '成功',
+  tagged: '已标记',
+  noTag: '未标记'
+}

+ 5 - 0
src/router/index.js

@@ -44,6 +44,11 @@ const router = createRouter({
           path: 'fish',
           name: 'fish',
           component: () => import('@/views/FishView.vue')
+        },
+        {
+          path: 'fish-friends',
+          name: 'fish-friends',
+          component: () => import('@/views/FishFriendsView.vue')
         }
       ]
     },

+ 58 - 0
src/services/api.js

@@ -193,9 +193,67 @@ export const getFishByOwner = async (ownerId) => {
   return response.data
 }
 
+// ... existing code ...
+
 export const getFishByResult = async (result) => {
   const response = await api.get('/fish/by-result', {
     params: { result }
   })
   return response.data
 }
+
+// FishFriends API
+export const listFishFriends = async (page, size, id, fishId, tgName, tgUsername, tgRemarkName, tgPhone, remark, createdAt) => {
+  const response = await api.get('/fish-friends', {
+    params: {
+      page,
+      size,
+      id,
+      fishId,
+      tgName,
+      tgUsername,
+      tgRemarkName,
+      tgPhone,
+      remark,
+      createdAt
+    }
+  })
+  return response.data
+}
+
+export const getFishFriendsById = async (id) => {
+  const response = await api.get(`/fish-friends/${id}`)
+  return response.data
+}
+
+export const updateFishFriends = async (fishFriendsData) => {
+  const response = await api.post('/fish-friends/update', fishFriendsData)
+  return response.data
+}
+
+export const deleteFishFriends = async (id) => {
+  const response = await api.post('/fish-friends/delete', { id })
+  return response.data
+}
+
+export const getFishFriendsByFishId = async (fishId) => {
+  const response = await api.get('/fish-friends/by-fish-id', {
+    params: { fishId }
+  })
+  return response.data
+}
+
+export const getFishFriendsByTgUsername = async (tgUsername) => {
+  const response = await api.get('/fish-friends/by-tg-username', {
+    params: { tgUsername }
+  })
+  return response.data
+}
+
+export const exportFishFriends = async (fishId) => {
+  const response = await api.get('/fish-friends/export', {
+    params: { fishId },
+    responseType: 'blob'
+  })
+  return response.data
+}

+ 583 - 0
src/views/FishFriendsView.vue

@@ -0,0 +1,583 @@
+<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="fish-friends-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.fishId"
+            placeholder="鱼苗ID"
+            size="small"
+            class="w-32"
+            @keyup.enter="handleSearch"
+          />
+          <InputText
+            v-model="searchForm.tgName"
+            placeholder="好友名"
+            size="small"
+            class="w-32"
+            @keyup.enter="handleSearch"
+          />
+          <InputText
+            v-model="searchForm.tgUsername"
+            placeholder="好友昵称"
+            size="small"
+            class="w-32"
+            @keyup.enter="handleSearch"
+          />
+          <InputText
+            v-model="searchForm.tgRemarkName"
+            placeholder="好友名称备注"
+            size="small"
+            class="w-32"
+            @keyup.enter="handleSearch"
+          />
+          <InputText
+            v-model="searchForm.tgPhone"
+            placeholder="好友手机号"
+            size="small"
+            class="w-32"
+            @keyup.enter="handleSearch"
+          />
+          <DatePicker
+            v-model="searchForm.createdAt"
+            placeholder="创建日期"
+            size="small"
+            class="w-40"
+            dateFormat="yy-mm-dd"
+            showIcon
+          />
+          <Button icon="pi pi-search" @click="handleSearch" label="搜索" size="small" severity="secondary" />
+          <Button icon="pi pi-refresh" @click="handleRefresh" label="刷新" size="small" />
+          <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="fishId" header="鱼苗ID" style="width: 120px" frozen>
+        <template #body="slotProps">
+          <span
+            class="font-mono text-sm copyable-text"
+            :title="slotProps.data.fishId"
+            @click="copyToClipboard(slotProps.data.fishId)"
+          >
+            {{ slotProps.data.fishId }}
+          </span>
+        </template>
+      </Column>
+
+      <Column field="tgName" header="好友名" style="min-width: 120px; max-width: 200px">
+        <template #body="slotProps">
+          <span
+            class="font-medium name-text copyable-text"
+            :title="slotProps.data.tgName"
+            @click="copyToClipboard(slotProps.data.tgName)"
+          >
+            {{ slotProps.data.tgName }}
+          </span>
+        </template>
+      </Column>
+
+      <Column field="tgUsername" header="好友昵称" style="min-width: 120px; max-width: 200px">
+        <template #body="slotProps">
+          <span
+            class="username-text copyable-text"
+            :title="slotProps.data.tgUsername"
+            @click="copyToClipboard(slotProps.data.tgUsername)"
+          >
+            {{ slotProps.data.tgUsername }}
+          </span>
+        </template>
+      </Column>
+
+      <Column field="tgRemarkName" header="好友名称备注" style="min-width: 120px; max-width: 200px">
+        <template #body="slotProps">
+          <span
+            class="remark-name-text copyable-text"
+            :title="slotProps.data.tgRemarkName"
+            @click="copyToClipboard(slotProps.data.tgRemarkName)"
+          >
+            {{ slotProps.data.tgRemarkName }}
+          </span>
+        </template>
+      </Column>
+
+      <Column field="tgPhone" header="好友手机号" style="width: 140px">
+        <template #body="slotProps">
+          <span
+            class="phone-text copyable-text"
+            :title="slotProps.data.tgPhone"
+            @click="copyToClipboard(slotProps.data.tgPhone)"
+          >
+            {{ slotProps.data.tgPhone }}
+          </span>
+        </template>
+      </Column>
+
+      <Column field="remark" header="备注" style="width: 120px">
+        <template #body="slotProps">
+          <div class="text-sm remark-text remark-content" :title="slotProps.data.remark || '-'">
+            {{ slotProps.data.remark || '-' }}
+          </div>
+        </template>
+      </Column>
+
+      <Column field="createdAt" header="创建时间" style="width: 120px">
+        <template #body="slotProps">
+          <span class="text-sm time-text">
+            {{ formatDateTime(slotProps.data.createdAt) }}
+          </span>
+        </template>
+      </Column>
+
+      <Column
+        header="操作"
+        style="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="编辑 FishFriends 信息"
+      :style="{ width: '800px' }"
+      position="center"
+    >
+      <div class="p-fluid">
+        <div class="grid grid-cols-3 gap-4">
+          <div class="field">
+            <label for="edit-fishId" class="font-medium text-sm mb-2 block">鱼苗ID</label>
+            <InputText id="edit-fishId" v-model="editForm.fishId" class="w-full" />
+          </div>
+
+          <div class="field">
+            <label for="edit-tgName" class="font-medium text-sm mb-2 block">好友名</label>
+            <InputText id="edit-tgName" v-model="editForm.tgName" class="w-full" />
+          </div>
+
+          <div class="field">
+            <label for="edit-tgUsername" class="font-medium text-sm mb-2 block">好友昵称</label>
+            <InputText id="edit-tgUsername" v-model="editForm.tgUsername" class="w-full" />
+          </div>
+
+          <div lass="field">
+            <label for="edit-tgRemarkName" class="font-medium text-sm mb-2 block">好友名称备注</label>
+            <InputText id="edit-tgRemarkName" v-model="editForm.tgRemarkName" class="w-full" />
+          </div>
+
+          <div class="field">
+            <label for="edit-tgPhone" class="font-medium text-sm mb-2 block">好友手机号</label>
+            <InputText id="edit-tgPhone" v-model="editForm.tgPhone" class="w-full" />
+          </div>
+        </div>
+
+        <div class="field mt-4">
+          <label for="edit-remark" class="font-medium text-sm mb-2 block">备注</label>
+          <Textarea
+            id="edit-remark"
+            v-model="editForm.remark"
+            rows="3"
+            class="w-full"
+            placeholder="请输入备注信息..."
+            autoResize
+          />
+        </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 { listFishFriends, deleteFishFriends, updateFishFriends } from '@/services/api'
+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 Dialog from 'primevue/dialog'
+import InputText from 'primevue/inputtext'
+import Textarea from 'primevue/textarea'
+import { useConfirm } from 'primevue/useconfirm'
+import { useToast } from 'primevue/usetoast'
+import { onMounted, ref } from 'vue'
+
+const toast = useToast()
+const confirm = useConfirm()
+
+// 表格数据
+const tableData = ref({
+  content: [],
+  metadata: {
+    page: 0,
+    size: 20,
+    total: 0
+  }
+})
+
+// 加载状态
+const loading = ref(false)
+
+// 编辑相关
+const editDialog = ref(false)
+const editLoading = ref(false)
+const editForm = ref({
+  id: null,
+  fishId: null,
+  tgName: null,
+  tgUsername: null,
+  tgRemarkName: null,
+  tgPhone: null,
+  remark: null
+})
+
+// 搜索表单
+const searchForm = ref({
+  id: null,
+  fishId: null,
+  tgName: null,
+  tgUsername: null,
+  tgRemarkName: null,
+  tgPhone: null,
+  remark: null,
+  createdAt: null
+})
+
+// 获取数据
+const fetchData = async () => {
+  loading.value = true
+  try {
+    const response = await listFishFriends(
+      tableData.value.metadata.page,
+      tableData.value.metadata.size,
+      searchForm.value.id || undefined,
+      searchForm.value.fishId || undefined,
+      searchForm.value.tgName || undefined,
+      searchForm.value.tgUsername || undefined,
+      searchForm.value.tgRemarkName || undefined,
+      searchForm.value.tgPhone || undefined,
+      searchForm.value.remark || undefined,
+      searchForm.value.createdAt ? formatDateForAPI(searchForm.value.createdAt) : undefined
+    )
+    tableData.value = response
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '获取 FishFriends 列表失败',
+      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,
+    fishId: null,
+    tgName: null,
+    tgUsername: null,
+    tgRemarkName: null,
+    tgPhone: null,
+    remark: null,
+    createdAt: null
+  }
+  tableData.value.metadata.page = 0
+  fetchData()
+}
+
+// 格式化日期用于API调用
+const formatDateForAPI = (date) => {
+  if (!date) return undefined
+  return useDateFormat(new Date(date), 'YYYY-MM-DD').value
+}
+
+// 格式化日期时间显示
+const formatDateTime = (date) => {
+  if (!date) return '-'
+  return useDateFormat(new Date(date), 'YYYY-MM-DD HH:mm:ss').value
+}
+
+// 确认删除
+const confirmDelete = (fishFriends) => {
+  confirm.require({
+    message: `确定要删除 FishFriends 记录 "${fishFriends.tgName || fishFriends.tgId}" 吗?`,
+    header: '确认删除',
+    icon: 'pi pi-exclamation-triangle',
+    accept: () => deleteFishFriendsRecord(fishFriends.id)
+  })
+}
+
+// 删除 FishFriends 记录
+const deleteFishFriendsRecord = async (id) => {
+  try {
+    await deleteFishFriends(id)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '删除成功',
+      life: 3000
+    })
+    fetchData()
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '删除失败',
+      life: 3000
+    })
+  }
+}
+
+// 复制到剪贴板
+const copyToClipboard = async (text) => {
+  try {
+    await navigator.clipboard.writeText(text)
+  } catch (error) {
+    // 降级方案:使用传统方法
+    const textArea = document.createElement('textarea')
+    textArea.value = text
+    document.body.appendChild(textArea)
+    textArea.select()
+    document.execCommand('copy')
+    document.body.removeChild(textArea)
+  }
+}
+
+// 打开编辑弹窗
+const openEditDialog = (fishFriends) => {
+  editForm.value = {
+    id: fishFriends.id,
+    fishId: fishFriends.fishId || null,
+    tgName: fishFriends.tgName || null,
+    tgUsername: fishFriends.tgUsername || null,
+    tgRemarkName: fishFriends.tgRemarkName || null,
+    tgPhone: fishFriends.tgPhone || null,
+    remark: fishFriends.remark || null
+  }
+  editDialog.value = true
+}
+
+// 保存编辑
+const saveEdit = async () => {
+  editLoading.value = true
+  try {
+    await updateFishFriends(editForm.value)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '更新成功',
+      life: 3000
+    })
+    editDialog.value = false
+    fetchData() // 刷新列表
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: '更新失败',
+      life: 3000
+    })
+  } finally {
+    editLoading.value = false
+  }
+}
+
+// 初始化
+onMounted(() => {
+  fetchData()
+})
+</script>
+
+<style scoped>
+.p-datatable-sm .p-datatable-tbody > tr > td {
+  padding: 0.5rem;
+}
+
+.p-datatable-sm .p-datatable-thead > tr > th {
+  padding: 0.5rem;
+}
+
+.fish-friends-table {
+  width: 100%;
+}
+
+.fish-friends-table .p-datatable-wrapper {
+  overflow-x: auto;
+}
+
+.fish-friends-table .p-datatable-thead th {
+  white-space: nowrap;
+  min-width: 100px;
+}
+
+.font-mono {
+  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+}
+
+.name-text {
+  max-width: 200px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  display: inline-block;
+}
+
+.username-text {
+  max-width: 200px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  display: inline-block;
+}
+
+.remark-name-text {
+  max-width: 200px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  display: inline-block;
+}
+
+.phone-text {
+  max-width: 200px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  display: inline-block;
+}
+
+.remark-content {
+  width: 120px;
+  word-wrap: break-word;
+  word-break: break-all;
+  white-space: normal;
+  line-height: 1.2;
+  max-height: none;
+  overflow: visible;
+}
+
+.time-text {
+  width: 140px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  display: inline-block;
+}
+
+.text-gray-600 {
+  color: #6b7280;
+}
+
+.remark-text {
+  color: #059669;
+  font-weight: 500;
+}
+
+.font-medium {
+  font-weight: 500;
+}
+
+.text-sm {
+  font-size: 0.875rem;
+}
+
+.ml-2 {
+  margin-left: 0.5rem;
+}
+
+.w-full {
+  width: 100%;
+}
+
+.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);
+}
+</style>

+ 183 - 25
src/views/FishView.vue

@@ -126,14 +126,6 @@
         </template>
       </Column>
 
-      <Column field="result" header="状态" style="width: 140px">
-        <template #body="slotProps">
-          <span class="result-text" :title="slotProps.data.result">
-            {{ slotProps.data.result }}
-          </span>
-        </template>
-      </Column>
-
       <Column field="remark" header="客户备注" style="width: 120px">
         <template #body="slotProps">
           <div class="text-sm remark-text remark-content" :title="slotProps.data.remark || '-'">
@@ -142,6 +134,18 @@
         </template>
       </Column>
 
+      <Column field="result" header="操作结果" style="width: 100px">
+        <template #body="slotProps">
+          <span
+            class="result-text"
+            :class="getResultText(slotProps.data.result).class"
+            :title="getResultText(slotProps.data.result).text"
+          >
+            {{ getResultText(slotProps.data.result).text }}
+          </span>
+        </template>
+      </Column>
+
       <Column field="ownerName" header="所有者" style="min-width: 100px; max-width: 150px">
         <template #body="slotProps">
           <span
@@ -154,7 +158,7 @@
         </template>
       </Column>
 
-      <Column field="loginTime" header="登录时间" style="width: 120px">
+      <Column field="loginTime" header="登录时间" style="width: 100px">
         <template #body="slotProps">
           <span class="text-sm time-text" :class="getTimeColorClass(slotProps.data.loginTime)">
             {{ formatRelativeTime(slotProps.data.loginTime) }}
@@ -162,7 +166,7 @@
         </template>
       </Column>
 
-      <Column field="createdAt" header="中鱼时间" style="width: 120px">
+      <Column field="createdAt" header="中鱼时间" style="width: 100px">
         <template #body="slotProps">
           <span class="text-sm time-text" :class="getTimeColorClass(slotProps.data.createdAt)">
             {{ formatRelativeTime(slotProps.data.createdAt) }}
@@ -182,9 +186,41 @@
         </template>
       </Column>
 
-      <Column header="操作" style="width: 200px" align-frozen="right" frozen>
+      <Column
+        header="操作"
+        style="min-width: 220px; width: 220px"
+        align-frozen="right"
+        frozen
+        :pt="{
+          columnHeaderContent: {
+            class: 'justify-center'
+          }
+        }"
+      >
         <template #body="slotProps">
-          <div class="flex gap-1">
+          <div class="flex justify-center gap-1">
+            <Button
+              icon="pi pi-window-maximize"
+              label="一键登录"
+              severity="danger"
+              size="small"
+              text
+              rounded
+              class="quick-login-btn"
+              aria-label="一键登录"
+              @click="handleQuickLogin(slotProps.data)"
+            />
+            <Button
+              icon="pi pi-file-export"
+              label="导出好友列表"
+              severity="warn"
+              size="small"
+              text
+              rounded
+              class="export-friends-btn"
+              aria-label="导出好友列表"
+              @click="handleExportFriends(slotProps.data)"
+            />
             <Button
               icon="pi pi-pencil"
               severity="info"
@@ -212,7 +248,7 @@
     <Dialog
       v-model:visible="editDialog"
       :modal="true"
-      header="编辑鱼信息"
+      header="编辑鱼信息"
       :style="{ width: '700px' }"
       position="center"
     >
@@ -232,7 +268,9 @@
             <label for="edit-phone" class="font-medium text-sm mb-2 block">手机号</label>
             <InputText id="edit-phone" v-model="editForm.phone" class="w-full" />
           </div>
+        </div>
 
+        <div class="grid grid-cols-2 gap-4 mt-4">
           <div class="field">
             <label for="edit-password" class="font-medium text-sm mb-2 block">二级密码</label>
             <InputText id="edit-password" v-model="editForm.password" class="w-full" />
@@ -242,6 +280,21 @@
             <label for="edit-ip" class="font-medium text-sm mb-2 block">IP地址</label>
             <InputText id="edit-ip" v-model="editForm.ip" class="w-full" />
           </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4 mt-4">
+          <div class="field">
+            <label for="edit-result" class="font-medium text-sm mb-2 block">操作结果</label>
+            <Dropdown
+              id="edit-result"
+              v-model="editForm.result"
+              :options="resultOptions.filter((option) => option.value !== null)"
+              optionLabel="label"
+              optionValue="value"
+              placeholder="选择操作结果"
+              class="w-full"
+            />
+          </div>
 
           <div class="field">
             <label for="edit-ownerName" class="font-medium text-sm mb-2 block">所有者</label>
@@ -328,7 +381,7 @@
 </template>
 
 <script setup>
-import { listFish, deleteFish, updateFish } from '@/services/api'
+import { listFish, deleteFish, updateFish, exportFishFriends } from '@/services/api'
 import { useDateFormat } from '@vueuse/core'
 import Button from 'primevue/button'
 import Column from 'primevue/column'
@@ -342,6 +395,7 @@ import { useConfirm } from 'primevue/useconfirm'
 import { useToast } from 'primevue/usetoast'
 import { onMounted, ref, inject } from 'vue'
 import { useOwnerStore } from '@/stores/owner'
+import { ResultEnum } from '@/enums'
 
 const toast = useToast()
 const confirm = useConfirm()
@@ -376,7 +430,8 @@ const editForm = ref({
   ownerName: null,
   ownerId: null,
   token: null,
-  session: null
+  session: null,
+  result: null
 })
 
 // 搜索表单
@@ -394,10 +449,24 @@ const searchForm = ref({
 // 状态选项
 const resultOptions = [
   { label: '全部', value: null },
-  { label: '未获取客户码', value: '未获取客户码' },
-  { label: '已获取客户码', value: '已获取客户码' }
+  { label: ResultEnum.success, value: 'success' },
+  { label: ResultEnum.tagged, value: 'tagged' },
+  { label: ResultEnum.noTag, value: 'noTag' }
 ]
 
+// 获取结果中文显示和样式类
+const getResultText = (result) => {
+  if (!result) return { text: '-', class: '' }
+
+  const resultMap = {
+    success: { text: ResultEnum.success, class: 'result-success' },
+    tagged: { text: ResultEnum.tagged, class: 'result-tagged' },
+    noTag: { text: ResultEnum.noTag, class: 'result-no-tag' }
+  }
+
+  return resultMap[result] || { text: result, class: '' }
+}
+
 // 获取数据
 const fetchData = async () => {
   loading.value = true
@@ -418,7 +487,7 @@ const fetchData = async () => {
     toast.add({
       severity: 'error',
       summary: '错误',
-      detail: '获取鱼列表失败',
+      detail: '获取鱼列表失败',
       life: 3000
     })
   } finally {
@@ -516,14 +585,14 @@ const formatDateForAPI = (date) => {
 // 确认删除
 const confirmDelete = (fish) => {
   confirm.require({
-    message: `确定要删除鱼 "${fish.name}" 吗?`,
+    message: `确定要删除鱼 "${fish.name}" 吗?`,
     header: '确认删除',
     icon: 'pi pi-exclamation-triangle',
     accept: () => deleteFishRecord(fish.id)
   })
 }
 
-// 删除鱼
+// 删除鱼
 const deleteFishRecord = async (id) => {
   try {
     await deleteFish(id)
@@ -580,7 +649,8 @@ const openEditDialog = (fish) => {
     ownerName: fish.ownerName || null,
     ownerId: ownerId,
     token: fish.token || null,
-    session: fish.session || null
+    session: fish.session || null,
+    result: fish.result || null
   }
   editDialog.value = true
 }
@@ -625,6 +695,63 @@ const handleOwnerChange = (event) => {
   }
 }
 
+// 一键登录处理
+const handleQuickLogin = (fish) => {
+  toast.add({
+    severity: 'info',
+    summary: '提示',
+    detail: `正在处理 ${fish.name} 的一键登录...`,
+    life: 3000
+  })
+}
+
+// 导出好友列表处理
+const handleExportFriends = async (fish) => {
+  try {
+    toast.add({
+      severity: 'info',
+      summary: '提示',
+      detail: `正在导出好友列表...`,
+      life: 2000
+    })
+
+    const blob = await exportFishFriends(fish.id)
+    const url = window.URL.createObjectURL(blob)
+    const link = document.createElement('a')
+    link.href = url
+    link.download = `fish_friends_${fish.id}.xlsx`
+    document.body.appendChild(link)
+    link.click()
+    document.body.removeChild(link)
+    window.URL.revokeObjectURL(url)
+
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: `已导出 ${fish.name} 的好友列表`,
+      life: 3000
+    })
+  } catch (error) {
+    console.error('导出失败:', error)
+
+    let errorMessage = '导出好友列表失败'
+    if (error.message?.includes('ERR_CONNECTION_REFUSED') || error.code === 'ERR_NETWORK') {
+      errorMessage = '无法连接到服务器,请检查后端服务是否运行'
+    } else if (error.response?.status === 401) {
+      errorMessage = '认证失败,请重新登录'
+    } else if (error.response?.status) {
+      errorMessage = `服务器错误: ${error.response.status}`
+    }
+
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: errorMessage,
+      life: 5000
+    })
+  }
+}
+
 // 初始化
 onMounted(async () => {
   if (isAdmin.value) {
@@ -677,21 +804,36 @@ onMounted(async () => {
 }
 
 .result-text {
-  width: 140px;
+  width: 100px;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
   display: inline-block;
 }
 
+.result-success {
+  color: #146249;
+  font-weight: 600;
+}
+
+.result-tagged {
+  color: #7c3aed;
+  font-weight: 500;
+}
+
+.result-no-tag {
+  color: #6b7280;
+  font-weight: 400;
+}
+
 .remark-content {
   width: 120px;
   word-wrap: break-word;
   word-break: break-all;
   white-space: normal;
   line-height: 1.2;
-  max-height: 60px;
-  overflow-y: auto;
+  max-height: none;
+  overflow: visible;
 }
 
 .owner-text {
@@ -703,7 +845,7 @@ onMounted(async () => {
 }
 
 .time-text {
-  width: 140px;
+  width: 100px;
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
@@ -801,4 +943,20 @@ onMounted(async () => {
   background-color: #d1d5db;
   transform: scale(0.98);
 }
+
+.quick-login-btn {
+  height: 2rem;
+  padding: 0 0.75rem;
+  font-size: 0.875rem;
+  white-space: nowrap;
+  width: 80px;
+}
+
+.export-friends-btn {
+  height: 2rem;
+  padding: 0 0.75rem;
+  font-size: 0.875rem;
+  white-space: nowrap;
+  width: 110px;
+}
 </style>

+ 6 - 1
src/views/MainView.vue

@@ -47,9 +47,14 @@ const navItems = [
     icon: 'pi pi-fw pi-telegram',
     items: [
       {
-        label: '鱼列表',
+        label: '鱼列表',
         icon: 'pi pi-fw pi-list',
         name: 'fish'
+      },
+      {
+        label: '鱼苗好友列表',
+        icon: 'pi pi-fw pi-users',
+        name: 'fish-friends'
       }
     ]
   },