|
|
@@ -115,6 +115,40 @@
|
|
|
<!-- Breadcrumb placeholder -->
|
|
|
</div>
|
|
|
<div class="user-actions">
|
|
|
+ <el-popover
|
|
|
+ placement="bottom"
|
|
|
+ :width="260"
|
|
|
+ trigger="hover"
|
|
|
+ @show="handleDownloadPopoverShow"
|
|
|
+ >
|
|
|
+ <template #reference>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ link
|
|
|
+ class="header-download-link"
|
|
|
+ :disabled="downloadLinksLoading || !clientDownloadHref"
|
|
|
+ @click="openClientDownload"
|
|
|
+ >
|
|
|
+ 下载客户端
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ <div class="download-popover">
|
|
|
+ <div v-if="downloadQrDataUrl" class="download-qr-wrap">
|
|
|
+ <img :src="downloadQrDataUrl" alt="客户端下载二维码" class="download-qr-img" />
|
|
|
+ </div>
|
|
|
+ <p v-else class="download-qr-empty">暂无可用下载链接</p>
|
|
|
+ <div class="download-popover-actions">
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ :disabled="!downloadQrDataUrl || copyDownloadQrLoading"
|
|
|
+ :loading="copyDownloadQrLoading"
|
|
|
+ @click="copyDownloadQrImage"
|
|
|
+ >
|
|
|
+ 复制图片
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-popover>
|
|
|
<el-dropdown trigger="click" @command="handleCommand">
|
|
|
<span class="el-dropdown-link">
|
|
|
<span v-if="user" class="username">{{ user.mobile }}</span>
|
|
|
@@ -198,12 +232,95 @@ import { Grid, List, User, ArrowDown, Connection, Monitor, Document, Download, R
|
|
|
import { ElMessage, FormInstance, FormRules } from 'element-plus'
|
|
|
import QRCode from 'qrcode'
|
|
|
import api from '../utils/request'
|
|
|
+import { getDownloadLinks, type DownloadLinksPublic } from '../api/public'
|
|
|
import { useMessageStore } from '../store/message'
|
|
|
|
|
|
const router = useRouter()
|
|
|
const authStore = useAuthStore()
|
|
|
const messageStore = useMessageStore()
|
|
|
const user = computed(() => authStore.user)
|
|
|
+const downloadLinks = ref<DownloadLinksPublic | null>(null)
|
|
|
+const downloadLinksLoading = ref(false)
|
|
|
+const downloadQrDataUrl = ref('')
|
|
|
+const copyDownloadQrLoading = ref(false)
|
|
|
+
|
|
|
+const isMobileUa = () =>
|
|
|
+ /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
|
|
+
|
|
|
+const clientDownloadHref = computed(() => {
|
|
|
+ const links = downloadLinks.value
|
|
|
+ if (!links) return ''
|
|
|
+ if (isMobileUa()) {
|
|
|
+ return links.mobile || links.pc || ''
|
|
|
+ }
|
|
|
+ return links.pc || links.mobile || ''
|
|
|
+})
|
|
|
+
|
|
|
+const qrDownloadHref = computed(() => {
|
|
|
+ const links = downloadLinks.value
|
|
|
+ if (!links) return ''
|
|
|
+ // 扫码场景默认给移动端(安卓)下载地址
|
|
|
+ return links.mobile || links.pc || ''
|
|
|
+})
|
|
|
+
|
|
|
+const ensureDownloadLinksLoaded = async () => {
|
|
|
+ if (downloadLinks.value || downloadLinksLoading.value) return
|
|
|
+ downloadLinksLoading.value = true
|
|
|
+ try {
|
|
|
+ const res = await getDownloadLinks()
|
|
|
+ downloadLinks.value = res.data
|
|
|
+ } catch {
|
|
|
+ downloadLinks.value = null
|
|
|
+ } finally {
|
|
|
+ downloadLinksLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const ensureDownloadQrGenerated = async () => {
|
|
|
+ if (!qrDownloadHref.value) {
|
|
|
+ downloadQrDataUrl.value = ''
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (downloadQrDataUrl.value) return
|
|
|
+ downloadQrDataUrl.value = await QRCode.toDataURL(qrDownloadHref.value, {
|
|
|
+ width: 220,
|
|
|
+ margin: 2,
|
|
|
+ errorCorrectionLevel: 'M'
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const handleDownloadPopoverShow = async () => {
|
|
|
+ await ensureDownloadLinksLoaded()
|
|
|
+ await ensureDownloadQrGenerated()
|
|
|
+}
|
|
|
+
|
|
|
+const openClientDownload = async () => {
|
|
|
+ await ensureDownloadLinksLoaded()
|
|
|
+ if (!clientDownloadHref.value) {
|
|
|
+ ElMessage.warning('暂无可用下载链接')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ window.open(clientDownloadHref.value, '_blank', 'noopener,noreferrer')
|
|
|
+}
|
|
|
+
|
|
|
+const copyDownloadQrImage = async () => {
|
|
|
+ if (!downloadQrDataUrl.value) return
|
|
|
+ if (!window.isSecureContext || !('clipboard' in navigator) || !('ClipboardItem' in window)) {
|
|
|
+ ElMessage.warning('当前环境不支持复制图片,请手动保存二维码')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ copyDownloadQrLoading.value = true
|
|
|
+ try {
|
|
|
+ const qrBlob = await (await fetch(downloadQrDataUrl.value)).blob()
|
|
|
+ await navigator.clipboard.write([new ClipboardItem({ 'image/png': qrBlob })])
|
|
|
+ ElMessage.success('二维码图片已复制')
|
|
|
+ } catch {
|
|
|
+ ElMessage.error('复制失败,请检查浏览器权限')
|
|
|
+ } finally {
|
|
|
+ copyDownloadQrLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
// Logout & Menu Command
|
|
|
const handleCommand = (command: string) => {
|
|
|
@@ -329,6 +446,7 @@ onMounted(() => {
|
|
|
}
|
|
|
messageStore.initWebSocket()
|
|
|
messageStore.fetchUnreadCount()
|
|
|
+ ensureDownloadLinksLoaded()
|
|
|
})
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
@@ -430,6 +548,10 @@ onUnmounted(() => {
|
|
|
.user-actions {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+.header-download-link {
|
|
|
+ font-size: 14px;
|
|
|
}
|
|
|
.el-dropdown-link {
|
|
|
cursor: pointer;
|
|
|
@@ -471,6 +593,30 @@ onUnmounted(() => {
|
|
|
font-size: 13px;
|
|
|
color: #909399;
|
|
|
}
|
|
|
+.download-popover {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+.download-qr-wrap {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+.download-qr-img {
|
|
|
+ width: 220px;
|
|
|
+ height: 220px;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+.download-qr-empty {
|
|
|
+ margin: 0;
|
|
|
+ text-align: center;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+.download-popover-actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
.el-main {
|
|
|
background-color: #f0f2f5;
|
|
|
padding: 20px;
|