| | |
| | | return null; |
| | | }; |
| | | |
| | | const getAdaptiveWatermarkStyle = (width, height) => { |
| | | const w = Math.max(1, Number(width) || 1); |
| | | const h = Math.max(1, Number(height) || 1); |
| | | const shortSide = Math.min(w, h); |
| | | const longSide = Math.max(w, h); |
| | | // 分段适配:小图减小字号,大图保持当前视觉效果 |
| | | const isSmallImage = shortSide <= 720; |
| | | const fontSize = isSmallImage |
| | | ? Math.max( |
| | | 10, |
| | | Math.min(28, Math.floor(shortSide * 0.016 + longSide * 0.004)) |
| | | ) |
| | | : Math.max( |
| | | 14, |
| | | Math.min(56, Math.floor(shortSide * 0.022 + longSide * 0.006)) |
| | | ); |
| | | return { |
| | | fontSize, |
| | | padding: Math.max(isSmallImage ? 4 : 5, Math.floor(fontSize * 0.4)), |
| | | lineGap: Math.max(isSmallImage ? 1 : 2, Math.floor(fontSize * 0.16)), |
| | | edgeGap: 0, |
| | | }; |
| | | }; |
| | | |
| | | const addWatermarkByBrowserCanvas = (tempFilePath, text) => { |
| | | return new Promise((resolve, reject) => { |
| | | try { |
| | |
| | | ctx.drawImage(img, 0, 0, w, h); |
| | | |
| | | const lines = wmText.split(/\r?\n/).filter(Boolean); |
| | | const fontSize = Math.max(8, Math.floor(Math.min(w, h) * 0.014)); |
| | | const padding = Math.max(3, Math.floor(fontSize * 0.35)); |
| | | const lineGap = Math.max(1, Math.floor(fontSize * 0.1)); |
| | | const edgeGap = 0; |
| | | const { fontSize, padding, lineGap, edgeGap } = |
| | | getAdaptiveWatermarkStyle(w, h); |
| | | |
| | | ctx.font = `${fontSize}px sans-serif`; |
| | | const maxChars = Math.max(...lines.map(t => (t || "").length), 0); |
| | |
| | | ctx.drawImage(tempFilePath, 0, 0, w, h); |
| | | |
| | | const lines = wmText.split(/\r?\n/).filter(Boolean); |
| | | const fontSize = Math.max(8, Math.floor(Math.min(w, h) * 0.014)); |
| | | const padding = Math.max(3, Math.floor(fontSize * 0.35)); |
| | | const lineGap = Math.max(1, Math.floor(fontSize * 0.1)); |
| | | const edgeGap = 0; |
| | | const { fontSize, padding, lineGap, edgeGap } = |
| | | getAdaptiveWatermarkStyle(w, h); |
| | | |
| | | ctx.setFontSize(fontSize); |
| | | ctx.setFillStyle("rgba(0,0,0,0.2)"); |
| | |
| | | <text class="info-label">经纬度</text> |
| | | <text class="info-value">{{ form.latitude }}, {{ form.longitude }}</text> |
| | | </view> |
| | | <view class="info-item photo-item"> |
| | | <text class="info-label">拜访照片</text> |
| | | <view class="photo-wrap"> |
| | | <view v-if="visitImageList.length" |
| | | class="photo-list"> |
| | | <image v-for="(img, idx) in visitImageList" |
| | | :key="img.id || img.url || idx" |
| | | class="photo-img" |
| | | :src="img.url" |
| | | mode="aspectFill" |
| | | @click="previewVisitImage(idx)" /> |
| | | </view> |
| | | <text v-else |
| | | class="empty-text">-</text> |
| | | </view> |
| | | </view> |
| | | </view> |
| | | <!-- 备注信息 --> |
| | | <view class="section"> |
| | |
| | | |
| | | import { ref, onMounted } from "vue"; |
| | | import PageHeader from "@/components/PageHeader.vue"; |
| | | import { normalizeFileUrl } from "@/utils/filePreview"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | |
| | | const userStore = useUserStore(); |
| | |
| | | locationAddress: "", |
| | | remark: "", |
| | | }); |
| | | const visitImageList = ref([]); |
| | | |
| | | const isLikelyPathValue = value => { |
| | | const v = String(value || "").trim(); |
| | | if (!v) return false; |
| | | if (/^(https?:)?\/\//i.test(v)) return true; |
| | | if (/^(blob:|data:|wxfile:|file:|content:)/i.test(v)) return true; |
| | | if (v.includes("/") || v.includes("\\")) return true; |
| | | if (/\.[a-zA-Z0-9]{2,8}($|\?)/.test(v)) return true; |
| | | return false; |
| | | }; |
| | | |
| | | const buildVisitFilePreviewUrl = file => { |
| | | const candidates = [ |
| | | file?.link, |
| | | file?.url, |
| | | file?.downloadUrl, |
| | | file?.path, |
| | | file?.filePath, |
| | | file?.tempPath, |
| | | file?.urlFull, |
| | | ].filter(Boolean); |
| | | for (const raw of candidates) { |
| | | if (!isLikelyPathValue(raw)) continue; |
| | | const normalized = normalizeFileUrl(raw); |
| | | if (normalized) return normalized; |
| | | } |
| | | return ""; |
| | | }; |
| | | |
| | | const buildVisitImageList = row => { |
| | | const rawList = []; |
| | | if (Array.isArray(row?.commonFileList)) rawList.push(...row.commonFileList); |
| | | if (Array.isArray(row?.storageBlobDTO)) rawList.push(...row.storageBlobDTO); |
| | | const uniq = []; |
| | | const seen = new Set(); |
| | | rawList.forEach(item => { |
| | | const url = buildVisitFilePreviewUrl(item); |
| | | if (!url || seen.has(url)) return; |
| | | seen.add(url); |
| | | uniq.push({ ...item, url }); |
| | | }); |
| | | visitImageList.value = uniq; |
| | | }; |
| | | |
| | | const previewVisitImage = idx => { |
| | | const urls = visitImageList.value.map(item => item?.url).filter(Boolean); |
| | | if (!urls.length) return; |
| | | uni.previewImage({ urls, current: urls[idx] || urls[0] }); |
| | | }; |
| | | |
| | | // 返回上一页 |
| | | const goBack = () => { |
| | |
| | | const row = uni.getStorageSync("clientVisit"); |
| | | if (row) { |
| | | form.value = { ...row }; |
| | | buildVisitImageList(row); |
| | | } else { |
| | | showToast("暂无拜访记录数据"); |
| | | } |
| | |
| | | text-align: right; |
| | | } |
| | | |
| | | .photo-item { |
| | | align-items: flex-start; |
| | | } |
| | | |
| | | .photo-wrap { |
| | | flex: 1; |
| | | display: flex; |
| | | justify-content: flex-start; |
| | | } |
| | | |
| | | .photo-list { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | justify-content: flex-start; |
| | | gap: 8px; |
| | | width: 100%; |
| | | } |
| | | |
| | | .photo-img { |
| | | width: 72px; |
| | | height: 72px; |
| | | border-radius: 8px; |
| | | background: #f5f5f5; |
| | | } |
| | | |
| | | .empty-text { |
| | | color: #999; |
| | | line-height: 22px; |
| | | } |
| | | |
| | | .multi-line { |
| | | text-align: left; |
| | | word-break: break-all; |