|
|
@@ -34,6 +34,14 @@
|
|
|
size="small"
|
|
|
severity="success"
|
|
|
/>
|
|
|
+ <Button
|
|
|
+ v-if="isAdmin"
|
|
|
+ icon="pi pi-building"
|
|
|
+ @click="openGenerateTeamLinkDialog"
|
|
|
+ label="生成团队链接"
|
|
|
+ size="small"
|
|
|
+ severity="info"
|
|
|
+ />
|
|
|
<Button
|
|
|
v-if="isAdmin || isTeam"
|
|
|
icon="pi pi-plus"
|
|
|
@@ -61,10 +69,18 @@
|
|
|
<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" />
|
|
|
+ <img
|
|
|
+ v-else-if="getLinkQRCode(link.id)"
|
|
|
+ :src="getLinkQRCode(link.id)"
|
|
|
+ :alt="link.name + '二维码'"
|
|
|
+ class="link-image-card qr-code-clickable"
|
|
|
+ @click="openQRCodeDialog(link)"
|
|
|
+ title="点击查看大图并保存"
|
|
|
+ />
|
|
|
<div v-else class="no-image-placeholder">
|
|
|
<i class="pi pi-image text-gray-400"></i>
|
|
|
</div>
|
|
|
@@ -127,10 +143,18 @@
|
|
|
<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" />
|
|
|
+ <img
|
|
|
+ v-else-if="getLinkQRCode(link.id)"
|
|
|
+ :src="getLinkQRCode(link.id)"
|
|
|
+ :alt="link.name + '二维码'"
|
|
|
+ class="link-image-card qr-code-clickable"
|
|
|
+ @click="openQRCodeDialog(link)"
|
|
|
+ title="点击查看大图并保存"
|
|
|
+ />
|
|
|
<div v-else class="no-image-placeholder">
|
|
|
<i class="pi pi-image text-gray-400"></i>
|
|
|
</div>
|
|
|
@@ -193,10 +217,18 @@
|
|
|
<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" />
|
|
|
+ <img
|
|
|
+ v-else-if="getLinkQRCode(link.id)"
|
|
|
+ :src="getLinkQRCode(link.id)"
|
|
|
+ :alt="link.name + '二维码'"
|
|
|
+ class="link-image-card qr-code-clickable"
|
|
|
+ @click="openQRCodeDialog(link)"
|
|
|
+ title="点击查看大图并保存"
|
|
|
+ />
|
|
|
<div v-else class="no-image-placeholder">
|
|
|
<i class="pi pi-image text-gray-400"></i>
|
|
|
</div>
|
|
|
@@ -422,6 +454,105 @@
|
|
|
</div>
|
|
|
</template>
|
|
|
</Dialog>
|
|
|
+
|
|
|
+ <!-- 生成团队链接弹窗 -->
|
|
|
+ <Dialog
|
|
|
+ v-model:visible="generateTeamLinkDialog"
|
|
|
+ :modal="true"
|
|
|
+ header="生成团队链接"
|
|
|
+ :style="{ width: '600px' }"
|
|
|
+ position="center"
|
|
|
+ >
|
|
|
+ <div class="p-fluid">
|
|
|
+ <div class="field">
|
|
|
+ <label for="generate-team-link-teamId" class="font-medium text-sm mb-2 block">选择团队</label>
|
|
|
+ <Select
|
|
|
+ id="generate-team-link-teamId"
|
|
|
+ v-model="generateTeamLinkForm.teamId"
|
|
|
+ :options="teamSelectOptions"
|
|
|
+ optionLabel="label"
|
|
|
+ optionValue="value"
|
|
|
+ placeholder="选择团队"
|
|
|
+ class="w-full"
|
|
|
+ showClear
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="generateTeamLinkData.generalLink" class="field mt-4">
|
|
|
+ <label class="font-medium text-sm mb-2 block">通用链接</label>
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <InputText :value="generateTeamLinkData.generalLink" readonly class="flex-1" />
|
|
|
+ <Button
|
|
|
+ icon="pi pi-copy"
|
|
|
+ size="small"
|
|
|
+ @click="copyToClipboard(generateTeamLinkData.generalLink)"
|
|
|
+ title="复制通用链接"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="generateTeamLinkData.browserLink" class="field mt-4">
|
|
|
+ <label class="font-medium text-sm mb-2 block">浏览器链接</label>
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <InputText :value="generateTeamLinkData.browserLink" readonly class="flex-1" />
|
|
|
+ <Button
|
|
|
+ icon="pi pi-copy"
|
|
|
+ size="small"
|
|
|
+ @click="copyToClipboard(generateTeamLinkData.browserLink)"
|
|
|
+ title="复制浏览器链接"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <div class="flex justify-end gap-3">
|
|
|
+ <Button label="关闭" severity="secondary" @click="generateTeamLinkDialog = false" />
|
|
|
+ <Button
|
|
|
+ label="生成链接"
|
|
|
+ severity="success"
|
|
|
+ @click="handleGenerateTeamLink"
|
|
|
+ :loading="generateTeamLinkLoading"
|
|
|
+ :disabled="!generateTeamLinkForm.teamId"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </Dialog>
|
|
|
+
|
|
|
+ <!-- 二维码预览弹窗 -->
|
|
|
+ <Dialog
|
|
|
+ v-model:visible="qrCodeDialog"
|
|
|
+ :modal="true"
|
|
|
+ :header="qrCodeLinkName + ' - 二维码'"
|
|
|
+ :style="{ width: '500px' }"
|
|
|
+ position="center"
|
|
|
+ >
|
|
|
+ <div class="flex flex-col items-center gap-4">
|
|
|
+ <div v-if="qrCodeImage" class="qr-code-preview-container">
|
|
|
+ <img :src="qrCodeImage" alt="二维码" class="qr-code-preview-image" />
|
|
|
+ </div>
|
|
|
+ <p v-else class="text-gray-500">暂无二维码图片</p>
|
|
|
+ <div v-if="qrCodeLinkUrl" class="w-full">
|
|
|
+ <label class="font-medium text-sm mb-2 block text-gray-700">链接地址</label>
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <InputText :value="qrCodeLinkUrl" readonly class="flex-1 text-sm" />
|
|
|
+ <Button
|
|
|
+ icon="pi pi-copy"
|
|
|
+ size="small"
|
|
|
+ @click="copyToClipboard(qrCodeLinkUrl)"
|
|
|
+ title="复制链接"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <div class="flex justify-end gap-3">
|
|
|
+ <Button label="保存二维码" icon="pi pi-download" severity="success" @click="saveQRCode" />
|
|
|
+ <Button label="关闭" severity="secondary" @click="qrCodeDialog = false" />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </Dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -435,10 +566,11 @@ 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, listMembers, getPromotionLink } from '@/services/api'
|
|
|
+import { listLinks, createLink, updateLink, deleteLink, uploadImage, listMembers, getPromotionLink, generateFirstLevelAgentLink } from '@/services/api'
|
|
|
import { LinkType } from '@/enums'
|
|
|
import { useTeamStore } from '@/stores/team'
|
|
|
import { useUserStore } from '@/stores/user'
|
|
|
+import QRCode from 'qrcode'
|
|
|
|
|
|
const toast = useToast()
|
|
|
const confirm = useConfirm()
|
|
|
@@ -462,6 +594,9 @@ const tableData = ref({
|
|
|
// 过滤后的链接数据
|
|
|
const filteredLinks = ref([])
|
|
|
|
|
|
+// 二维码缓存
|
|
|
+const qrCodeCache = ref(new Map())
|
|
|
+
|
|
|
// 加载状态
|
|
|
const loading = ref(false)
|
|
|
|
|
|
@@ -512,6 +647,17 @@ const generatePromoData = ref({
|
|
|
promotionLink: null
|
|
|
})
|
|
|
|
|
|
+// 生成团队链接相关
|
|
|
+const generateTeamLinkDialog = ref(false)
|
|
|
+const generateTeamLinkLoading = ref(false)
|
|
|
+const generateTeamLinkForm = ref({
|
|
|
+ teamId: null
|
|
|
+})
|
|
|
+const generateTeamLinkData = ref({
|
|
|
+ generalLink: null,
|
|
|
+ browserLink: null
|
|
|
+})
|
|
|
+
|
|
|
// 搜索表单
|
|
|
const searchForm = ref({
|
|
|
type: null,
|
|
|
@@ -579,6 +725,8 @@ const fetchData = async () => {
|
|
|
)
|
|
|
tableData.value = response
|
|
|
applyFilters()
|
|
|
+ // 为所有链接生成二维码
|
|
|
+ await generateQRCodesForLinks(response.content || [])
|
|
|
} catch {
|
|
|
toast.add({
|
|
|
severity: 'error',
|
|
|
@@ -606,6 +754,8 @@ const applyFilters = () => {
|
|
|
}
|
|
|
|
|
|
filteredLinks.value = links
|
|
|
+ // 为没有图片的链接生成二维码
|
|
|
+ generateQRCodesForLinks(links)
|
|
|
}
|
|
|
|
|
|
// 按类型获取链接
|
|
|
@@ -745,6 +895,108 @@ const handleImageError = (event) => {
|
|
|
event.target.style.display = 'none'
|
|
|
}
|
|
|
|
|
|
+// 生成二维码
|
|
|
+const generateQRCode = async (linkUrl) => {
|
|
|
+ if (!linkUrl) return null
|
|
|
+
|
|
|
+ // 检查缓存
|
|
|
+ if (qrCodeCache.value.has(linkUrl)) {
|
|
|
+ return qrCodeCache.value.get(linkUrl)
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const qrCodeDataUrl = await QRCode.toDataURL(linkUrl, {
|
|
|
+ width: 200,
|
|
|
+ margin: 1,
|
|
|
+ color: {
|
|
|
+ dark: '#000000',
|
|
|
+ light: '#FFFFFF'
|
|
|
+ }
|
|
|
+ })
|
|
|
+ qrCodeCache.value.set(linkUrl, qrCodeDataUrl)
|
|
|
+ return qrCodeDataUrl
|
|
|
+ } catch (error) {
|
|
|
+ console.error('生成二维码失败:', error)
|
|
|
+ return null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 二维码数据存储(按链接ID)- 使用对象以支持响应式
|
|
|
+const linkQRCodes = ref({})
|
|
|
+
|
|
|
+// 二维码预览相关
|
|
|
+const qrCodeDialog = ref(false)
|
|
|
+const qrCodeImage = ref('')
|
|
|
+const qrCodeLinkName = ref('')
|
|
|
+const qrCodeLinkUrl = ref('')
|
|
|
+
|
|
|
+// 为链接生成二维码(如果还没有)
|
|
|
+const ensureQRCode = async (link) => {
|
|
|
+ if (!link || !link.link || link.image) return
|
|
|
+
|
|
|
+ const linkId = link.id
|
|
|
+ if (linkQRCodes.value[linkId]) return
|
|
|
+
|
|
|
+ const qrCode = await generateQRCode(link.link)
|
|
|
+ if (qrCode) {
|
|
|
+ linkQRCodes.value[linkId] = qrCode
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 批量生成二维码
|
|
|
+const generateQRCodesForLinks = async (links) => {
|
|
|
+ const promises = links
|
|
|
+ .filter(link => link && link.link && !link.image)
|
|
|
+ .map(link => ensureQRCode(link))
|
|
|
+ await Promise.all(promises)
|
|
|
+}
|
|
|
+
|
|
|
+// 获取链接的二维码
|
|
|
+const getLinkQRCode = (linkId) => {
|
|
|
+ return linkQRCodes.value[linkId] || null
|
|
|
+}
|
|
|
+
|
|
|
+// 打开二维码预览弹窗
|
|
|
+const openQRCodeDialog = (link) => {
|
|
|
+ const qrCode = getLinkQRCode(link.id)
|
|
|
+ if (qrCode) {
|
|
|
+ qrCodeImage.value = qrCode
|
|
|
+ qrCodeLinkName.value = link.name || '推广链接'
|
|
|
+ qrCodeLinkUrl.value = link.link || ''
|
|
|
+ qrCodeDialog.value = true
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 保存二维码
|
|
|
+const saveQRCode = () => {
|
|
|
+ if (!qrCodeImage.value) return
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 创建一个临时的 a 标签来下载图片
|
|
|
+ const link = document.createElement('a')
|
|
|
+ link.href = qrCodeImage.value
|
|
|
+ link.download = `${qrCodeLinkName.value || '二维码'}_${Date.now()}.png`
|
|
|
+ document.body.appendChild(link)
|
|
|
+ link.click()
|
|
|
+ document.body.removeChild(link)
|
|
|
+
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: '二维码已保存',
|
|
|
+ life: 2000
|
|
|
+ })
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存二维码失败:', error)
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: '保存二维码失败',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 打开新增弹窗
|
|
|
const openAddDialog = () => {
|
|
|
isEdit.value = false
|
|
|
@@ -1099,6 +1351,58 @@ const handleGenerateLink = async () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// 打开生成团队链接弹窗
|
|
|
+const openGenerateTeamLinkDialog = () => {
|
|
|
+ generateTeamLinkForm.value = {
|
|
|
+ teamId: null
|
|
|
+ }
|
|
|
+ generateTeamLinkData.value = {
|
|
|
+ generalLink: null,
|
|
|
+ browserLink: null
|
|
|
+ }
|
|
|
+ generateTeamLinkDialog.value = true
|
|
|
+}
|
|
|
+
|
|
|
+// 生成团队链接
|
|
|
+const handleGenerateTeamLink = async () => {
|
|
|
+ if (!generateTeamLinkForm.value.teamId) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'warn',
|
|
|
+ summary: '提示',
|
|
|
+ detail: '请选择团队',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ generateTeamLinkLoading.value = true
|
|
|
+ try {
|
|
|
+ const response = await generateFirstLevelAgentLink(generateTeamLinkForm.value.teamId)
|
|
|
+ generateTeamLinkData.value = {
|
|
|
+ generalLink: response.generalLink || null,
|
|
|
+ browserLink: response.browserLink || null
|
|
|
+ }
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: response.message || '团队链接生成成功',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ // 生成成功后刷新链接列表
|
|
|
+ fetchData()
|
|
|
+ } catch (error) {
|
|
|
+ const errorMessage = error?.message || error?.detail || '生成团队链接失败'
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '错误',
|
|
|
+ detail: errorMessage,
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ generateTeamLinkLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// 初始化
|
|
|
onMounted(async () => {
|
|
|
if (isAdmin.value) {
|
|
|
@@ -1195,4 +1499,31 @@ onMounted(async () => {
|
|
|
background-color: #d1d5db;
|
|
|
transform: scale(0.98);
|
|
|
}
|
|
|
+
|
|
|
+.qr-code-clickable {
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.qr-code-clickable:hover {
|
|
|
+ transform: scale(1.05);
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+.qr-code-preview-container {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ padding: 20px;
|
|
|
+ background-color: #f9fafb;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+}
|
|
|
+
|
|
|
+.qr-code-preview-image {
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 400px;
|
|
|
+ width: auto;
|
|
|
+ height: auto;
|
|
|
+}
|
|
|
</style>
|