|
|
@@ -0,0 +1,804 @@
|
|
|
+<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-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"
|
|
|
+ />
|
|
|
+ <InputText
|
|
|
+ v-model="searchForm.phone"
|
|
|
+ placeholder="手机号"
|
|
|
+ size="small"
|
|
|
+ class="w-32"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ />
|
|
|
+ <InputText
|
|
|
+ v-model="searchForm.ownerName"
|
|
|
+ placeholder="所有者"
|
|
|
+ size="small"
|
|
|
+ class="w-32"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ />
|
|
|
+ <Dropdown
|
|
|
+ v-model="searchForm.result"
|
|
|
+ :options="resultOptions"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ placeholder="操作结果"
|
|
|
+ size="small"
|
|
|
+ class="w-32"
|
|
|
+ :showClear="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" />
|
|
|
+ <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: 120px; max-width: 200px">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <span
|
|
|
+ class="font-medium name-text copyable-text"
|
|
|
+ :title="slotProps.data.name"
|
|
|
+ @click="copyToClipboard(slotProps.data.name)"
|
|
|
+ >
|
|
|
+ {{ slotProps.data.name }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+
|
|
|
+ <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)"
|
|
|
+ >
|
|
|
+ {{ slotProps.data.username }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+
|
|
|
+ <Column field="phone" header="手机号" style="width: 120px">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <span class="copyable-text" :title="slotProps.data.phone" @click="copyToClipboard(slotProps.data.phone)">
|
|
|
+ {{ slotProps.data.phone }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+
|
|
|
+ <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 || '无')"
|
|
|
+ >
|
|
|
+ {{ slotProps.data.password || '无' }}
|
|
|
+ </span>
|
|
|
+ </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 || '-'">
|
|
|
+ {{ slotProps.data.remark || '-' }}
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+
|
|
|
+ <Column field="ownerName" header="所有者" 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 || '未分配')"
|
|
|
+ >
|
|
|
+ {{ slotProps.data.ownerName || '未分配' }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+
|
|
|
+ <Column field="loginTime" header="登录时间" style="width: 120px">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <span class="text-sm time-text" :class="getTimeColorClass(slotProps.data.loginTime)">
|
|
|
+ {{ formatRelativeTime(slotProps.data.loginTime) }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+
|
|
|
+ <Column field="createdAt" header="中鱼时间" style="width: 120px">
|
|
|
+ <template #body="slotProps">
|
|
|
+ <span class="text-sm time-text" :class="getTimeColorClass(slotProps.data.createdAt)">
|
|
|
+ {{ formatRelativeTime(slotProps.data.createdAt) }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+
|
|
|
+ <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)"
|
|
|
+ >
|
|
|
+ {{ slotProps.data.ip }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </Column>
|
|
|
+
|
|
|
+ <Column header="操作" style="width: 200px" align-frozen="right" frozen>
|
|
|
+ <template #body="slotProps">
|
|
|
+ <div class="flex 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="编辑鱼儿信息"
|
|
|
+ :style="{ width: '700px' }"
|
|
|
+ position="center"
|
|
|
+ >
|
|
|
+ <div class="p-fluid">
|
|
|
+ <div class="grid grid-cols-3 gap-4">
|
|
|
+ <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">
|
|
|
+ <label for="edit-username" class="font-medium text-sm mb-2 block">用户名</label>
|
|
|
+ <InputText id="edit-username" v-model="editForm.username" class="w-full" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="field">
|
|
|
+ <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 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" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="field">
|
|
|
+ <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 class="field">
|
|
|
+ <label for="edit-ownerName" class="font-medium text-sm mb-2 block">所有者</label>
|
|
|
+ <Dropdown
|
|
|
+ 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
|
|
|
+ />
|
|
|
+ </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"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <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"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <Textarea
|
|
|
+ id="edit-session"
|
|
|
+ v-model="editForm.session"
|
|
|
+ rows="3"
|
|
|
+ class="w-full"
|
|
|
+ placeholder="请输入Session信息..."
|
|
|
+ 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 { listFish, deleteFish, updateFish } 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 Dropdown from 'primevue/dropdown'
|
|
|
+import InputText from 'primevue/inputtext'
|
|
|
+import Textarea from 'primevue/textarea'
|
|
|
+import { useConfirm } from 'primevue/useconfirm'
|
|
|
+import { useToast } from 'primevue/usetoast'
|
|
|
+import { onMounted, ref, inject } from 'vue'
|
|
|
+import { useOwnerStore } from '@/stores/owner'
|
|
|
+
|
|
|
+const toast = useToast()
|
|
|
+const confirm = useConfirm()
|
|
|
+const ownerStore = useOwnerStore()
|
|
|
+
|
|
|
+const isAdmin = inject('isAdmin')
|
|
|
+
|
|
|
+// 表格数据
|
|
|
+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,
|
|
|
+ name: null,
|
|
|
+ username: null,
|
|
|
+ phone: null,
|
|
|
+ password: null,
|
|
|
+ ip: null,
|
|
|
+ remark: null,
|
|
|
+ ownerName: null,
|
|
|
+ ownerId: null,
|
|
|
+ token: null,
|
|
|
+ session: null
|
|
|
+})
|
|
|
+
|
|
|
+// 搜索表单
|
|
|
+const searchForm = ref({
|
|
|
+ id: null,
|
|
|
+ name: null,
|
|
|
+ username: null,
|
|
|
+ phone: null,
|
|
|
+ ownerName: null,
|
|
|
+ result: null,
|
|
|
+ createdAt: null,
|
|
|
+ loginTime: null
|
|
|
+})
|
|
|
+
|
|
|
+// 状态选项
|
|
|
+const resultOptions = [
|
|
|
+ { label: '全部', value: null },
|
|
|
+ { label: '未获取客户码', value: '未获取客户码' },
|
|
|
+ { label: '已获取客户码', value: '已获取客户码' }
|
|
|
+]
|
|
|
+
|
|
|
+// 获取数据
|
|
|
+const fetchData = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const response = await listFish(
|
|
|
+ tableData.value.metadata.page,
|
|
|
+ tableData.value.metadata.size,
|
|
|
+ searchForm.value.id || undefined,
|
|
|
+ searchForm.value.name || undefined,
|
|
|
+ searchForm.value.phone || undefined,
|
|
|
+ searchForm.value.result || undefined,
|
|
|
+ searchForm.value.ownerName || undefined,
|
|
|
+ searchForm.value.createdAt ? formatDateForAPI(searchForm.value.createdAt) : undefined,
|
|
|
+ searchForm.value.loginTime ? formatDateForAPI(searchForm.value.loginTime) : undefined
|
|
|
+ )
|
|
|
+ tableData.value = response
|
|
|
+ } catch (error) {
|
|
|
+ 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,
|
|
|
+ username: null,
|
|
|
+ phone: null,
|
|
|
+ ownerName: null,
|
|
|
+ result: null,
|
|
|
+ createdAt: null,
|
|
|
+ loginTime: null
|
|
|
+ }
|
|
|
+ tableData.value.metadata.page = 0
|
|
|
+ fetchData()
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化相对时间
|
|
|
+const formatRelativeTime = (date) => {
|
|
|
+ if (!date) return '-'
|
|
|
+
|
|
|
+ const now = new Date()
|
|
|
+ const targetDate = new Date(date)
|
|
|
+ const diffInMs = now - targetDate
|
|
|
+
|
|
|
+ if (diffInMs < 0) return '刚刚'
|
|
|
+
|
|
|
+ const diffInSeconds = Math.floor(diffInMs / 1000)
|
|
|
+ const diffInMinutes = Math.floor(diffInSeconds / 60)
|
|
|
+ const diffInHours = Math.floor(diffInMinutes / 60)
|
|
|
+ const diffInDays = Math.floor(diffInHours / 24)
|
|
|
+
|
|
|
+ if (diffInDays > 0) {
|
|
|
+ const remainingHours = diffInHours % 24
|
|
|
+ return `${diffInDays}天${remainingHours}小时前`
|
|
|
+ } else if (diffInHours > 0) {
|
|
|
+ const remainingMinutes = diffInMinutes % 60
|
|
|
+ return `${diffInHours}小时${remainingMinutes}分钟前`
|
|
|
+ } else if (diffInMinutes > 0) {
|
|
|
+ return `${diffInMinutes}分钟前`
|
|
|
+ } else {
|
|
|
+ return '刚刚'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 获取时间颜色类
|
|
|
+const getTimeColorClass = (date) => {
|
|
|
+ if (!date) return ''
|
|
|
+
|
|
|
+ const now = new Date()
|
|
|
+ const targetDate = new Date(date)
|
|
|
+ const diffInMs = now - targetDate
|
|
|
+
|
|
|
+ if (diffInMs < 0) return 'time-just-now'
|
|
|
+
|
|
|
+ const diffInSeconds = Math.floor(diffInMs / 1000)
|
|
|
+ const diffInMinutes = Math.floor(diffInSeconds / 60)
|
|
|
+ const diffInHours = Math.floor(diffInMinutes / 60)
|
|
|
+ const diffInDays = Math.floor(diffInHours / 24)
|
|
|
+
|
|
|
+ if (diffInDays > 0) {
|
|
|
+ return 'time-old' // 大于1天
|
|
|
+ } else if (diffInHours > 0) {
|
|
|
+ return 'time-medium' // 大于1小时少于1天
|
|
|
+ } else {
|
|
|
+ return 'time-recent' // 少于1小时
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 格式化日期用于API调用
|
|
|
+const formatDateForAPI = (date) => {
|
|
|
+ if (!date) return undefined
|
|
|
+ return useDateFormat(new Date(date), 'YYYY-MM-DD').value
|
|
|
+}
|
|
|
+
|
|
|
+// 确认删除
|
|
|
+const confirmDelete = (fish) => {
|
|
|
+ confirm.require({
|
|
|
+ message: `确定要删除鱼儿 "${fish.name}" 吗?`,
|
|
|
+ header: '确认删除',
|
|
|
+ icon: 'pi pi-exclamation-triangle',
|
|
|
+ accept: () => deleteFishRecord(fish.id)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 删除鱼儿
|
|
|
+const deleteFishRecord = async (id) => {
|
|
|
+ try {
|
|
|
+ await deleteFish(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 = (fish) => {
|
|
|
+ let ownerId = null
|
|
|
+ if (fish.ownerName) {
|
|
|
+ const owner = ownerStore.owners.find((owner) => owner.name === fish.ownerName)
|
|
|
+ if (owner) {
|
|
|
+ ownerId = owner.value
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ editForm.value = {
|
|
|
+ id: fish.id,
|
|
|
+ name: fish.name || null,
|
|
|
+ username: fish.username || null,
|
|
|
+ phone: fish.phone || null,
|
|
|
+ password: fish.password || null,
|
|
|
+ ip: fish.ip || null,
|
|
|
+ remark: fish.remark || null,
|
|
|
+ ownerName: fish.ownerName || null,
|
|
|
+ ownerId: ownerId,
|
|
|
+ token: fish.token || null,
|
|
|
+ session: fish.session || null
|
|
|
+ }
|
|
|
+ editDialog.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 保存编辑
|
|
|
+const saveEdit = async () => {
|
|
|
+ editLoading.value = true
|
|
|
+ try {
|
|
|
+ await updateFish(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
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理所有者变化
|
|
|
+const handleOwnerChange = (event) => {
|
|
|
+ const selectedUserId = event.value
|
|
|
+ if (selectedUserId) {
|
|
|
+ const selectedOwner = ownerStore.owners.find((owner) => owner.value === selectedUserId)
|
|
|
+ if (selectedOwner) {
|
|
|
+ editForm.value.ownerId = selectedOwner.value
|
|
|
+ editForm.value.ownerName = selectedOwner.name
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ editForm.value.ownerId = null
|
|
|
+ editForm.value.ownerName = null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化
|
|
|
+onMounted(async () => {
|
|
|
+ if (isAdmin.value) {
|
|
|
+ await ownerStore.loadOwners()
|
|
|
+ }
|
|
|
+ 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-table {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.fish-table .p-datatable-wrapper {
|
|
|
+ overflow-x: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.fish-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;
|
|
|
+}
|
|
|
+
|
|
|
+.result-text {
|
|
|
+ width: 140px;
|
|
|
+ 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: 60px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.owner-text {
|
|
|
+ max-width: 150px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+
|
|
|
+.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;
|
|
|
+}
|
|
|
+
|
|
|
+.password-text {
|
|
|
+ color: #6b7280;
|
|
|
+ font-style: italic;
|
|
|
+ font-size: 0.875rem;
|
|
|
+ word-wrap: break-word;
|
|
|
+ word-break: break-all;
|
|
|
+ display: inline-block;
|
|
|
+ line-height: 1.2;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ background-color: #f3f4f6;
|
|
|
+ padding: 2px 6px;
|
|
|
+ border-radius: 4px;
|
|
|
+ width: fit-content;
|
|
|
+ max-width: 200px;
|
|
|
+}
|
|
|
+
|
|
|
+.password-text.has-password {
|
|
|
+ color: #2563eb;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.ip-text {
|
|
|
+ max-width: 120px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+
|
|
|
+.time-recent {
|
|
|
+ color: #059669;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.time-medium {
|
|
|
+ color: #ee5808;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.time-old {
|
|
|
+ color: #6b7280;
|
|
|
+ font-weight: 400;
|
|
|
+}
|
|
|
+
|
|
|
+.time-just-now {
|
|
|
+ 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>
|