|
|
@@ -0,0 +1,727 @@
|
|
|
+<template>
|
|
|
+ <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
|
|
|
+ <!-- 搜索和操作区域 -->
|
|
|
+ <div class="flex flex-wrap items-center gap-2 mb-6">
|
|
|
+ <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"
|
|
|
+ />
|
|
|
+ <Dropdown
|
|
|
+ v-model="searchForm.type"
|
|
|
+ :options="typeOptions"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ placeholder="链接类型"
|
|
|
+ size="small"
|
|
|
+ class="w-32"
|
|
|
+ :showClear="true"
|
|
|
+ />
|
|
|
+ <Button icon="pi pi-search" @click="handleSearch" label="搜索" size="small" severity="secondary" />
|
|
|
+ <Button icon="pi pi-refresh" @click="handleRefresh" label="刷新" size="small" />
|
|
|
+ <Button icon="pi pi-plus" @click="openAddDialog" label="新增链接" size="small" severity="success" />
|
|
|
+ <div class="flex-1"></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 卡片展示区域 -->
|
|
|
+ <div class="space-y-8">
|
|
|
+ <!-- 通用链接 -->
|
|
|
+ <div v-if="getLinksByType('general').length > 0">
|
|
|
+ <h3 class="text-lg font-semibold mb-4 text-gray-700">{{ LinkType.general }}</h3>
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
|
+ <div
|
|
|
+ v-for="link in getLinksByType('general')"
|
|
|
+ :key="link.id"
|
|
|
+ class="link-card bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 relative"
|
|
|
+ >
|
|
|
+ <!-- 链接名称 -->
|
|
|
+ <div class="p-4 pb-2">
|
|
|
+ <h4 class="font-medium text-gray-800 truncate" :title="link.name">{{ link.name }}</h4>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 图片 -->
|
|
|
+ <div class="px-4 py-2">
|
|
|
+ <div class="image-container-card">
|
|
|
+ <img v-if="link.image" :src="link.image" :alt="link.name" class="link-image-card" />
|
|
|
+ <div v-else class="no-image-placeholder">
|
|
|
+ <i class="pi pi-image text-gray-400"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 链接地址 -->
|
|
|
+ <div class="p-4 pt-2 pb-16">
|
|
|
+ <span class="link-url-text-card block" :title="link.link" @click="copyToClipboard(link.link)">
|
|
|
+ {{ link.link }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 操作按钮 - 固定在右下角 -->
|
|
|
+ <div class="absolute bottom-4 right-4 flex gap-2">
|
|
|
+ <Button
|
|
|
+ icon="pi pi-copy"
|
|
|
+ size="small"
|
|
|
+ text
|
|
|
+ rounded
|
|
|
+ @click="copyToClipboard(link.link)"
|
|
|
+ title="复制链接"
|
|
|
+ />
|
|
|
+ <Button icon="pi pi-pencil" size="small" text rounded @click="openEditDialog(link)" title="编辑" />
|
|
|
+ <Button
|
|
|
+ icon="pi pi-trash"
|
|
|
+ size="small"
|
|
|
+ text
|
|
|
+ rounded
|
|
|
+ severity="danger"
|
|
|
+ @click="confirmDelete(link)"
|
|
|
+ title="删除"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 分隔线 -->
|
|
|
+ <div
|
|
|
+ v-if="getLinksByType('general').length > 0 && getLinksByType('super').length > 0"
|
|
|
+ class="border-t border-gray-300 my-8"
|
|
|
+ ></div>
|
|
|
+
|
|
|
+ <!-- 超级链接 -->
|
|
|
+ <div v-if="getLinksByType('super').length > 0">
|
|
|
+ <h3 class="text-lg font-semibold mb-4 text-gray-700">{{ LinkType.super }}</h3>
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
|
+ <div
|
|
|
+ v-for="link in getLinksByType('super')"
|
|
|
+ :key="link.id"
|
|
|
+ class="link-card bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 relative"
|
|
|
+ >
|
|
|
+ <!-- 链接名称 -->
|
|
|
+ <div class="p-4 pb-2">
|
|
|
+ <h4 class="font-medium text-gray-800 truncate" :title="link.name">{{ link.name }}</h4>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 图片 -->
|
|
|
+ <div class="px-4 py-2">
|
|
|
+ <div class="image-container-card">
|
|
|
+ <img v-if="link.image" :src="link.image" :alt="link.name" class="link-image-card" />
|
|
|
+ <div v-else class="no-image-placeholder">
|
|
|
+ <i class="pi pi-image text-gray-400"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 链接地址 -->
|
|
|
+ <div class="p-4 pt-2 pb-16">
|
|
|
+ <span class="link-url-text-card block" :title="link.link" @click="copyToClipboard(link.link)">
|
|
|
+ {{ link.link }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 操作按钮 - 固定在右下角 -->
|
|
|
+ <div class="absolute bottom-4 right-4 flex gap-2">
|
|
|
+ <Button
|
|
|
+ icon="pi pi-copy"
|
|
|
+ size="small"
|
|
|
+ text
|
|
|
+ rounded
|
|
|
+ @click="copyToClipboard(link.link)"
|
|
|
+ title="复制链接"
|
|
|
+ />
|
|
|
+ <Button icon="pi pi-pencil" size="small" text rounded @click="openEditDialog(link)" title="编辑" />
|
|
|
+ <Button
|
|
|
+ icon="pi pi-trash"
|
|
|
+ size="small"
|
|
|
+ text
|
|
|
+ rounded
|
|
|
+ severity="danger"
|
|
|
+ @click="confirmDelete(link)"
|
|
|
+ title="删除"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 分隔线 -->
|
|
|
+ <div
|
|
|
+ v-if="getLinksByType('super').length > 0 && getLinksByType('browser').length > 0"
|
|
|
+ class="border-t border-gray-300 my-8"
|
|
|
+ ></div>
|
|
|
+
|
|
|
+ <!-- 浏览器链接 -->
|
|
|
+ <div v-if="getLinksByType('browser').length > 0">
|
|
|
+ <h3 class="text-lg font-semibold mb-4 text-gray-700">{{ LinkType.browser }}</h3>
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
|
+ <div
|
|
|
+ v-for="link in getLinksByType('browser')"
|
|
|
+ :key="link.id"
|
|
|
+ class="link-card bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-200 relative"
|
|
|
+ >
|
|
|
+ <!-- 链接名称 -->
|
|
|
+ <div class="p-4 pb-2">
|
|
|
+ <h4 class="font-medium text-gray-800 truncate" :title="link.name">{{ link.name }}</h4>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 图片 -->
|
|
|
+ <div class="px-4 py-2">
|
|
|
+ <div class="image-container-card">
|
|
|
+ <img v-if="link.image" :src="link.image" :alt="link.name" class="link-image-card" />
|
|
|
+ <div v-else class="no-image-placeholder">
|
|
|
+ <i class="pi pi-image text-gray-400"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 链接地址 -->
|
|
|
+ <div class="p-4 pt-2 pb-16">
|
|
|
+ <span class="link-url-text-card block" :title="link.link" @click="copyToClipboard(link.link)">
|
|
|
+ {{ link.link }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 操作按钮 - 固定在右下角 -->
|
|
|
+ <div class="absolute bottom-4 right-4 flex gap-2">
|
|
|
+ <Button
|
|
|
+ icon="pi pi-copy"
|
|
|
+ size="small"
|
|
|
+ text
|
|
|
+ rounded
|
|
|
+ @click="copyToClipboard(link.link)"
|
|
|
+ title="复制链接"
|
|
|
+ />
|
|
|
+ <Button icon="pi pi-pencil" size="small" text rounded @click="openEditDialog(link)" title="编辑" />
|
|
|
+ <Button
|
|
|
+ icon="pi pi-trash"
|
|
|
+ size="small"
|
|
|
+ text
|
|
|
+ rounded
|
|
|
+ severity="danger"
|
|
|
+ @click="confirmDelete(link)"
|
|
|
+ title="删除"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 无数据提示 -->
|
|
|
+ <div v-if="filteredLinks.length === 0" class="text-center py-12">
|
|
|
+ <i class="pi pi-link text-6xl text-gray-300 mb-4"></i>
|
|
|
+ <p class="text-gray-500 text-lg">暂无推广链接</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 新增/编辑弹窗 -->
|
|
|
+ <Dialog
|
|
|
+ v-model:visible="editDialog"
|
|
|
+ :modal="true"
|
|
|
+ :header="isEdit ? '编辑推广链接' : '新增推广链接'"
|
|
|
+ :style="{ width: '600px' }"
|
|
|
+ position="center"
|
|
|
+ >
|
|
|
+ <div class="p-fluid">
|
|
|
+ <div class="field">
|
|
|
+ <label for="edit-name" class="font-medium text-sm mb-2 block">链接名称</label>
|
|
|
+ <InputText id="edit-name" v-model="editForm.name" class="w-full" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="field mt-4">
|
|
|
+ <label for="edit-type" class="font-medium text-sm mb-2 block">链接类型</label>
|
|
|
+ <Dropdown
|
|
|
+ id="edit-type"
|
|
|
+ v-model="editForm.type"
|
|
|
+ :options="typeOptions.filter((option) => option.value !== null)"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ placeholder="选择链接类型"
|
|
|
+ class="w-full"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="field mt-4">
|
|
|
+ <label for="edit-link" class="font-medium text-sm mb-2 block">链接地址</label>
|
|
|
+ <InputText id="edit-link" v-model="editForm.link" class="w-full" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="field mt-4">
|
|
|
+ <label class="font-medium text-sm mb-2 block">图片</label>
|
|
|
+ <div class="card flex flex-col items-center gap-6">
|
|
|
+ <FileUpload
|
|
|
+ v-if="!imagePreview"
|
|
|
+ mode="basic"
|
|
|
+ @select="onFileSelect"
|
|
|
+ customUpload
|
|
|
+ auto
|
|
|
+ severity="secondary"
|
|
|
+ class="p-button-outlined"
|
|
|
+ accept="image/*"
|
|
|
+ :maxFileSize="50000000"
|
|
|
+ chooseLabel="选择图片"
|
|
|
+ />
|
|
|
+ <div v-if="imagePreview" class="flex flex-col items-center gap-2">
|
|
|
+ <img
|
|
|
+ :src="imagePreview"
|
|
|
+ alt="Image"
|
|
|
+ class="shadow-md rounded-xl w-full sm:w-64"
|
|
|
+ style="filter: grayscale(100%)"
|
|
|
+ />
|
|
|
+ <Button icon="pi pi-times" size="small" rounded severity="danger" @click="removeImage" title="移除图片" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <div class="flex justify-end gap-3">
|
|
|
+ <Button label="取消" severity="secondary" @click="editDialog = false" />
|
|
|
+ <Button label="保存" severity="success" @click="saveEdit" :loading="editLoading" />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </Dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted } from 'vue'
|
|
|
+import { useDateFormat } from '@vueuse/core'
|
|
|
+import Button from 'primevue/button'
|
|
|
+import Column from 'primevue/column'
|
|
|
+import DataTable from 'primevue/datatable'
|
|
|
+import Dialog from 'primevue/dialog'
|
|
|
+import Dropdown from 'primevue/dropdown'
|
|
|
+import InputText from 'primevue/inputtext'
|
|
|
+import FileUpload from 'primevue/fileupload'
|
|
|
+import { useConfirm } from 'primevue/useconfirm'
|
|
|
+import { useToast } from 'primevue/usetoast'
|
|
|
+import { usePrimeVue } from 'primevue/config'
|
|
|
+import { listLinks, createLink, updateLink, deleteLink, uploadImage } from '@/services/api'
|
|
|
+import { LinkType } from '@/enums'
|
|
|
+
|
|
|
+const toast = useToast()
|
|
|
+const confirm = useConfirm()
|
|
|
+const $primevue = usePrimeVue()
|
|
|
+
|
|
|
+// 表格数据
|
|
|
+const tableData = ref({
|
|
|
+ content: [],
|
|
|
+ metadata: {
|
|
|
+ page: 0,
|
|
|
+ size: 1000,
|
|
|
+ total: 0
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 过滤后的链接数据
|
|
|
+const filteredLinks = ref([])
|
|
|
+
|
|
|
+// 加载状态
|
|
|
+const loading = ref(false)
|
|
|
+
|
|
|
+// 编辑相关
|
|
|
+const editDialog = ref(false)
|
|
|
+const editLoading = ref(false)
|
|
|
+const uploading = ref(false)
|
|
|
+const isEdit = ref(false)
|
|
|
+
|
|
|
+// 图片预览相关
|
|
|
+const imagePreview = ref(null)
|
|
|
+const imageFile = ref(null)
|
|
|
+const editForm = ref({
|
|
|
+ id: null,
|
|
|
+ name: null,
|
|
|
+ type: null,
|
|
|
+ link: null,
|
|
|
+ image: null
|
|
|
+})
|
|
|
+
|
|
|
+// 搜索表单
|
|
|
+const searchForm = ref({
|
|
|
+ id: null,
|
|
|
+ name: null,
|
|
|
+ type: null
|
|
|
+})
|
|
|
+
|
|
|
+// 链接类型选项
|
|
|
+const typeOptions = [
|
|
|
+ { label: '全部', value: null },
|
|
|
+ { label: LinkType.general, value: 'general' },
|
|
|
+ { label: LinkType.super, value: 'super' },
|
|
|
+ { label: LinkType.browser, value: 'browser' }
|
|
|
+]
|
|
|
+
|
|
|
+// 获取链接类型文本
|
|
|
+const getLinkTypeText = (type) => {
|
|
|
+ return LinkType[type] || type
|
|
|
+}
|
|
|
+
|
|
|
+// 获取链接类型样式类
|
|
|
+const getLinkTypeClass = (type) => {
|
|
|
+ const classMap = {
|
|
|
+ general: 'link-type-general',
|
|
|
+ super: 'link-type-super',
|
|
|
+ browser: 'link-type-browser'
|
|
|
+ }
|
|
|
+ return classMap[type] || ''
|
|
|
+}
|
|
|
+
|
|
|
+// 获取数据
|
|
|
+const fetchData = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const response = await listLinks(
|
|
|
+ tableData.value.metadata.page,
|
|
|
+ tableData.value.metadata.size,
|
|
|
+ searchForm.value.name || undefined,
|
|
|
+ searchForm.value.type || undefined
|
|
|
+ )
|
|
|
+ tableData.value = response
|
|
|
+ applyFilters()
|
|
|
+ } catch {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: '获取推广链接失败',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 应用搜索过滤
|
|
|
+const applyFilters = () => {
|
|
|
+ let links = [...tableData.value.content]
|
|
|
+
|
|
|
+ // ID 过滤
|
|
|
+ if (searchForm.value.id) {
|
|
|
+ links = links.filter((link) => link.id.toString().includes(searchForm.value.id))
|
|
|
+ }
|
|
|
+
|
|
|
+ // 名称过滤
|
|
|
+ if (searchForm.value.name) {
|
|
|
+ links = links.filter((link) => link.name && link.name.toLowerCase().includes(searchForm.value.name.toLowerCase()))
|
|
|
+ }
|
|
|
+
|
|
|
+ // 类型过滤
|
|
|
+ if (searchForm.value.type) {
|
|
|
+ links = links.filter((link) => link.type === searchForm.value.type)
|
|
|
+ }
|
|
|
+
|
|
|
+ filteredLinks.value = links
|
|
|
+}
|
|
|
+
|
|
|
+// 按类型获取链接
|
|
|
+const getLinksByType = (type) => {
|
|
|
+ return filteredLinks.value.filter((link) => link.type === type)
|
|
|
+}
|
|
|
+
|
|
|
+// 搜索处理
|
|
|
+const handleSearch = () => {
|
|
|
+ applyFilters()
|
|
|
+}
|
|
|
+
|
|
|
+// 刷新处理
|
|
|
+const handleRefresh = () => {
|
|
|
+ searchForm.value = {
|
|
|
+ id: null,
|
|
|
+ name: null,
|
|
|
+ type: null
|
|
|
+ }
|
|
|
+ fetchData()
|
|
|
+}
|
|
|
+
|
|
|
+// 确认删除
|
|
|
+const confirmDelete = (link) => {
|
|
|
+ confirm.require({
|
|
|
+ message: `确定要删除推广链接 "${link.name}" 吗?`,
|
|
|
+ header: '确认删除',
|
|
|
+ icon: 'pi pi-exclamation-triangle',
|
|
|
+ accept: () => deleteLinkRecord(link.id)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+// 删除推广链接
|
|
|
+const deleteLinkRecord = async (id) => {
|
|
|
+ try {
|
|
|
+ await deleteLink(id)
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: '删除成功',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ fetchData()
|
|
|
+ } catch {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: '删除失败',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 文件选择处理
|
|
|
+const onFileSelect = (event) => {
|
|
|
+ const file = event.files[0]
|
|
|
+ if (!file) return
|
|
|
+
|
|
|
+ // 检查文件类型
|
|
|
+ if (!file.type.startsWith('image/')) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: '请选择图片文件',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查文件大小 (50MB)
|
|
|
+ if (file.size > 50000000) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: '图片文件大小不能超过50MB',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const reader = new FileReader()
|
|
|
+ reader.onload = (e) => {
|
|
|
+ imagePreview.value = e.target.result
|
|
|
+ imageFile.value = file
|
|
|
+ }
|
|
|
+ reader.readAsDataURL(file)
|
|
|
+}
|
|
|
+
|
|
|
+// 移除图片
|
|
|
+const removeImage = () => {
|
|
|
+ imagePreview.value = null
|
|
|
+ imageFile.value = null
|
|
|
+}
|
|
|
+
|
|
|
+// 复制到剪贴板
|
|
|
+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 formatSize = (bytes) => {
|
|
|
+ const k = 1024
|
|
|
+ const dm = 3
|
|
|
+ const sizes = $primevue.config.locale.fileSizeTypes
|
|
|
+
|
|
|
+ if (bytes === 0) {
|
|
|
+ return `0 ${sizes[0]}`
|
|
|
+ }
|
|
|
+
|
|
|
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
|
+ const formattedSize = parseFloat((bytes / Math.pow(k, i)).toFixed(dm))
|
|
|
+
|
|
|
+ return `${formattedSize} ${sizes[i]}`
|
|
|
+}
|
|
|
+
|
|
|
+// 处理图片加载错误
|
|
|
+const handleImageError = (event) => {
|
|
|
+ event.target.style.display = 'none'
|
|
|
+}
|
|
|
+
|
|
|
+// 打开新增弹窗
|
|
|
+const openAddDialog = () => {
|
|
|
+ isEdit.value = false
|
|
|
+ editForm.value = {
|
|
|
+ id: null,
|
|
|
+ name: null,
|
|
|
+ type: null,
|
|
|
+ link: null,
|
|
|
+ image: null
|
|
|
+ }
|
|
|
+ // 重置图片预览状态
|
|
|
+ imagePreview.value = null
|
|
|
+ imageFile.value = null
|
|
|
+ editDialog.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 打开编辑弹窗
|
|
|
+const openEditDialog = (link) => {
|
|
|
+ isEdit.value = true
|
|
|
+ editForm.value = {
|
|
|
+ id: link.id,
|
|
|
+ name: link.name || null,
|
|
|
+ type: link.type || null,
|
|
|
+ link: link.link || null,
|
|
|
+ image: link.image || null
|
|
|
+ }
|
|
|
+ // 编辑时显示原有图片
|
|
|
+ imagePreview.value = link.image || null
|
|
|
+ imageFile.value = null
|
|
|
+ editDialog.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 保存编辑
|
|
|
+const saveEdit = async () => {
|
|
|
+ editLoading.value = true
|
|
|
+ try {
|
|
|
+ // 准备保存的数据
|
|
|
+ const saveData = { ...editForm.value }
|
|
|
+
|
|
|
+ // 处理图片逻辑
|
|
|
+ if (imageFile.value) {
|
|
|
+ // 有新选择的图片文件,先上传
|
|
|
+ uploading.value = true
|
|
|
+ try {
|
|
|
+ const result = await uploadImage(imageFile.value)
|
|
|
+ saveData.image = result.data.url
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: `${result.message || '图片上传成功'} (${formatSize(result.data.size)})`,
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ console.error('图片上传失败', error)
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: '图片上传失败: ' + (error.message || error),
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ return
|
|
|
+ } finally {
|
|
|
+ uploading.value = false
|
|
|
+ }
|
|
|
+ } else if (imagePreview.value === null) {
|
|
|
+ // 如果图片被移除,设置为null
|
|
|
+ saveData.image = null
|
|
|
+ } else if (isEdit.value && imagePreview.value && !imageFile.value) {
|
|
|
+ // 编辑时:如果显示原有图片但没有选择新文件,说明图片未修改,删除image参数
|
|
|
+ delete saveData.image
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isEdit.value) {
|
|
|
+ await updateLink(editForm.value.id, saveData)
|
|
|
+ } else {
|
|
|
+ await createLink(saveData)
|
|
|
+ }
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: isEdit.value ? '更新成功' : '创建成功',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ editDialog.value = false
|
|
|
+ fetchData()
|
|
|
+ } catch {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: isEdit.value ? '更新失败' : '创建失败',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ editLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化
|
|
|
+onMounted(() => {
|
|
|
+ fetchData()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.link-card {
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.link-card:hover {
|
|
|
+ border-color: #3b82f6;
|
|
|
+ transform: translateY(-2px);
|
|
|
+}
|
|
|
+
|
|
|
+.image-container-card {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 120px;
|
|
|
+ background-color: #f9fafb;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+}
|
|
|
+
|
|
|
+.link-image-card {
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 100%;
|
|
|
+ object-fit: contain;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.no-image-placeholder {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ height: 100%;
|
|
|
+ font-size: 2rem;
|
|
|
+}
|
|
|
+
|
|
|
+.link-url-text-card {
|
|
|
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
|
+ font-size: 0.875rem;
|
|
|
+ color: #6b7280;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: color 0.2s ease;
|
|
|
+ word-break: break-all;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+
|
|
|
+.link-url-text-card:hover {
|
|
|
+ color: #3b82f6;
|
|
|
+}
|
|
|
+
|
|
|
+.preview-image {
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 200px;
|
|
|
+ object-fit: contain;
|
|
|
+ border-radius: 4px;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+}
|
|
|
+
|
|
|
+.font-medium {
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.text-sm {
|
|
|
+ font-size: 0.875rem;
|
|
|
+}
|
|
|
+
|
|
|
+.copyable-text {
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ user-select: none;
|
|
|
+}
|
|
|
+
|
|
|
+.copyable-text:hover {
|
|
|
+ background-color: #e5e7eb;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.copyable-text:active {
|
|
|
+ background-color: #d1d5db;
|
|
|
+ transform: scale(0.98);
|
|
|
+}
|
|
|
+</style>
|