UNPKG

vue3-watermark-camera

Version:

A Vue 3 component for taking photos with watermark support

462 lines (461 loc) 15.3 kB
import { defineComponent as Q, ref as N, watch as H, onMounted as Z, createElementBlock as E, openBlock as P, createBlock as ee, createCommentVNode as K, createElementVNode as h, Teleport as te, withModifiers as oe, normalizeStyle as ne, unref as M } from "vue"; function z() { return { width: window.innerWidth, height: window.innerHeight, clientWidth: document.documentElement.clientWidth, clientHeight: document.documentElement.clientHeight }; } function ae(a, t, o = {}) { return new Promise((e, r) => { const { showTime: i = !0, showLocation: c = !0, showName: d = !0, showCompany: p = !0, backgroundColor: m = "rgba(0,0,0,0.5)", textColor: g = "#fff", fontSize: y = 14 } = o, k = t.name || "用户", b = t.company || "公司", x = z(), C = x.width, v = x.height, l = window.devicePixelRatio || 1, n = document.getElementById("watermarkCanvas"), s = n.getContext("2d") || {}; n.width = C * l, n.height = v * l, n.style.width = C + "px", n.style.height = v + "px", s.setTransform(l, 0, 0, l, 0, 0); const f = new Image(); f.crossOrigin = "anonymous", f.onload = () => { var X; s.drawImage(f, 0, 0, C, v); const w = 16, S = C - w * 2, W = 12, F = o.height || 110, A = v - F - w - 20, D = w; s.fillStyle = m, ie(s, D, A, S, F, W), s.fill(), s.fillStyle = g, s.textBaseline = "top"; let U = A + 26; const _ = S * 0.4, I = S * 0.6, R = D, L = D + _; if (i && t.time) { const u = ((X = t.time.split(" ")[1]) == null ? void 0 : X.slice(0, 5)) || ""; s.font = "bold 32px Arial"; const $ = s.measureText(u).width, J = R + (_ - $) / 2; s.fillText(u, J, U), U += 36; const V = t.time.split(" ")[0] || ""; s.font = `bold ${y}px Arial`; const Y = s.measureText(V).width, j = R + (_ - Y) / 2; s.fillText(V, j, U); } s.beginPath(), s.moveTo(L - 16, A + 12), s.lineTo(L - 16, A + F - 12), s.strokeStyle = g, s.lineWidth = 2, s.stroke(); let T = A + 16; if (s.textAlign = "left", d && (s.font = "bold 18px Arial", s.fillText(k, L + 16, T), T += 24), p) { s.font = `${y}px Arial`; let u = b; const $ = I - 24; if (s.measureText(u).width > $) { for (; s.measureText(u + "...").width > $ && u.length > 0; ) u = u.slice(0, -1); u += "..."; } s.fillText(u, L, T), T += 20; } if (c && t.lat && t.lng) { const u = se(t.lat, t.lng); s.font = `${y}px Arial`, s.fillText(u, L, T), T += 20; } if (c && t.address) { s.font = `${y}px Arial`; let u = t.address; u.length > 30 && (u = u.slice(0, 30) + "..."), re(s, u, L, T, I - 12, 18, !1); } const G = n.toDataURL("image/jpeg", 0.9); e(G); }, f.onerror = () => r(new Error("图片加载失败")), f.src = a; }); } function re(a, t, o, e, r, i, c = !1) { let d = ""; for (let m = 0; m < t.length; m++) { const g = d + t[m]; if (a.measureText(g).width > r && m > 0) { let k = o; if (c) { const b = a.measureText(d).width; k = o + (r - b) / 2; } a.fillText(d, k, e), e += i, d = t[m]; } else d = g; } let p = o; if (c) { const m = a.measureText(d).width; p = o + (r - m) / 2; } a.fillText(d, p, e); } function ie(a, t, o, e, r, i) { a.beginPath(), a.moveTo(t + i, o), a.lineTo(t + e - i, o), a.arcTo(t + e, o, t + e, o + i, i), a.lineTo(t + e, o + r - i), a.arcTo(t + e, o + r, t + e - i, o + r, i), a.lineTo(t + i, o + r), a.arcTo(t, o + r, t, o + r - i, i), a.lineTo(t, o + i), a.arcTo(t, o, t + i, o, i), a.closePath(); } function se(a, t) { return !a || !t ? "" : `${Number(a).toFixed(6)}°N,${Number(t).toFixed(6)}°E`; } async function le() { return new Promise((a, t) => { if (!navigator.geolocation) { t(new Error("浏览器不支持定位")); return; } navigator.geolocation.getCurrentPosition( (o) => { const { latitude: e, longitude: r } = o.coords; a({ latitude: e, longitude: r }); }, (o) => { t(o); }, { enableHighAccuracy: !0, timeout: 8e3, maximumAge: 6e4 } ); }); } async function ce() { return new Promise((a, t) => { if (typeof window.dd > "u") { t(new Error("钉钉 JSAPI 未加载")); return; } window.dd.getLocation({ targetAccuracy: "200", cacheTimeout: 20, coordinate: "1", useCache: !0, withReGeocode: !0, type: 1, success: (o) => { const { address: e, latitude: r, longitude: i } = o; a({ latitude: r, longitude: i, address: e }); }, fail: (o) => { t(o); } }); }); } async function de(a, t, o) { return new Promise((e, r) => { const i = new XMLHttpRequest(); i.open("GET", `https://restapi.amap.com/v3/geocode/regeo?key=${o}&location=${t},${a}`), i.onload = () => { try { const c = JSON.parse(i.responseText); c && c.regeocode ? e(c.regeocode.formatted_address) : e(""); } catch (c) { r(c); } }, i.onerror = () => r(new Error("网络请求失败")), i.send(); }); } function O() { if (typeof navigator > "u") return "browser"; const a = navigator.userAgent.toLowerCase(); return a.includes("dingtalk") ? "dingtalk" : a.includes("micromessenger") ? "wechat" : a.includes("alipay") ? "alipay" : "browser"; } async function ue(a) { const t = a || "https://quan.suning.com/getSysTime.do"; return new Promise((o) => { const e = new XMLHttpRequest(); e.open("GET", t), e.onload = () => { try { const r = JSON.parse(e.responseText); r && r.sysTime2 ? o(r.sysTime2) : o((/* @__PURE__ */ new Date()).toLocaleString()); } catch { o((/* @__PURE__ */ new Date()).toLocaleString()); } }, e.onerror = () => o((/* @__PURE__ */ new Date()).toLocaleString()), e.send(); }); } function B() { return (/* @__PURE__ */ new Date()).toLocaleString(); } async function me(a) { return new Promise((t, o) => { const e = document.createElement("input"); e.type = "file", e.accept = "image/*", e.capture = a == null ? void 0 : a.facingMode, e.style.display = "none", document.body.appendChild(e), e.onchange = (r) => { var c; const i = (c = r.target.files) == null ? void 0 : c[0]; if (i) { const d = new FileReader(); d.onload = (p) => { var g; const m = (g = p.target) == null ? void 0 : g.result; t(m), document.body.removeChild(e); }, d.onerror = () => { o(new Error("文件读取失败")), document.body.removeChild(e); }, d.readAsDataURL(i); } else o(new Error("未选择文件")), document.body.removeChild(e); }, e.onerror = () => { o(new Error("相机调用失败")), document.body.removeChild(e); }, e.click(); }); } async function fe(a, t, o) { const e = new FormData(); e.append("file", a); const r = await fetch(t, { method: "POST", body: e, headers: { ...o // 不要设置 Content-Type,让浏览器自动设置 } }); if (!r.ok) throw new Error(`上传失败: ${r.status}`); return r.json(); } function q(a, t) { var d; const o = a.split(","), e = ((d = o[0].match(/:(.*?);/)) == null ? void 0 : d[1]) || "image/jpeg", r = atob(o[1]); let i = r.length; const c = new Uint8Array(i); for (; i--; ) c[i] = r.charCodeAt(i); return new File([c], t, { type: e }); } const he = { class: "watermark-camera" }, we = { key: 0, class: "camera-btn-group" }, ge = { key: 1, class: "photo-thumb" }, pe = ["src"], ye = { class: "modal-content" }, ke = ["src"], ve = /* @__PURE__ */ Q({ __name: "WatermarkCamera", props: { modelValue: {}, name: { default: "用户" }, company: { default: "公司" }, action: { default: "" }, amapKey: { default: "" }, watermarkConfig: { default: () => ({ showTime: !0, showLocation: !0, showName: !0, showCompany: !0, backgroundColor: "rgba(0,0,0,0.5)", textColor: "#fff", fontSize: 14, height: 110 }) }, locationConfig: { default: () => ({ enableLocation: !0, locationProvider: "h5" }) }, timeConfig: { default: () => ({ enableNetworkTime: !0, networkTimeUrl: "https://quan.suning.com/getSysTime.do" }) }, uploadConfig: { default: () => ({ enableAutoUpload: !0 }) }, cameraConfig: { default: () => ({ enableCamera: !0 }) } }, emits: ["update:modelValue", "upload-success", "location-success", "location-error", "time-success", "time-error", "camera-success", "camera-error"], setup(a, { expose: t, emit: o }) { const e = a, r = o, i = N(e.modelValue || ""), c = N(!1), d = N(O()), p = N(!1), m = z(), g = m.width, y = m.height; H(() => e.modelValue, (l) => i.value = l || ""), H(i, (l) => r("update:modelValue", l)), H(c, (l) => { l ? document.body.style.overflow = "hidden" : document.body.style.overflow = ""; }); async function k() { try { const { locationConfig: l } = e; if (!(l != null && l.enableLocation)) throw new Error("定位功能已禁用"); let n; switch (l.locationProvider) { case "dingtalk": n = await ce(); break; case "custom": if (l.customLocationFn) n = await l.customLocationFn(); else throw new Error("自定义定位函数未提供"); break; case "h5": default: n = await le(); break; } if (e.amapKey && !n.address) try { n.address = await de(n.latitude, n.longitude, e.amapKey); } catch (s) { console.warn("获取地址信息失败:", s), n.address = "位置信息获取失败了,请申请高德地图key,再配置amapKey,啊哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈"; } return r("location-success", n), n; } catch (l) { throw r("location-error", l), l; } } async function b() { try { const { timeConfig: l } = e; let n; return l != null && l.enableNetworkTime && l.customTimeFn ? n = await l.customTimeFn() : l != null && l.enableNetworkTime ? n = await ue(l.networkTimeUrl) : n = B(), r("time-success", n), n; } catch (l) { return r("time-error", l), B(); } } async function x(l) { try { const n = await me({ facingMode: l }); p.value = !0; const [s, f] = await Promise.allSettled([ k(), b() ]), w = s.status === "fulfilled" ? s.value : { latitude: 0, longitude: 0, address: "位置信息获取失败了,请申请高德地图key,再配置amapKey,啊哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈" }, S = f.status === "fulfilled" ? f.value : B(), W = await ae(n, { address: w.address || "位置信息获取失败了,请申请高德地图key,再配置amapKey,啊哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈", time: S, lat: w.latitude, lng: w.longitude, name: e.name, company: e.company }, e.watermarkConfig); i.value = W, c.value = !0, await C(W); } catch (n) { console.error("拍照失败:", n), alert("拍照失败,请重试"); } } async function C(l) { try { const { uploadConfig: n, action: s } = e; if (!(n != null && n.enableAutoUpload)) return null; let f; if (n.customUploadFn) { const w = q(l, "photo.jpg"); f = await n.customUploadFn(w); } else { const w = q(l, "photo.jpg"); f = await fe(w, s); } return r("upload-success", f), f; } catch (n) { return console.error("上传失败:", n), null; } } function v() { i.value = ""; } return t({ takePhoto: () => x("environment"), deletePhoto: v, getLocation: k, getNetworkTime: b }), Z(() => { d.value = O(); }), (l, n) => (P(), E("div", he, [ i.value ? (P(), E("div", ge, [ h("img", { src: i.value, class: "thumb-img", onClick: n[1] || (n[1] = (s) => c.value = !0) }, null, 8, pe), h("span", { class: "delete-btn", onClick: v }, n[5] || (n[5] = [ h("svg", { viewBox: "0 0 24 24", width: "16", height: "16" }, [ h("line", { x1: "6", y1: "6", x2: "18", y2: "18", stroke: "#fff", "stroke-width": "2", "stroke-linecap": "round" }), h("line", { x1: "18", y1: "6", x2: "6", y2: "18", stroke: "#fff", "stroke-width": "2", "stroke-linecap": "round" }) ], -1) ])) ])) : (P(), E("div", we, [ h("button", { class: "camera-btn", onClick: n[0] || (n[0] = (s) => x("environment")) }, n[4] || (n[4] = [ h("span", { class: "camera-icon" }, null, -1) ])) ])), (P(), ee(te, { to: "body" }, [ c.value ? (P(), E("div", { key: 0, class: "modal-mask", onClick: n[3] || (n[3] = (s) => c.value = !1) }, [ h("div", { class: "close-btn", onClick: n[2] || (n[2] = oe((s) => c.value = !1, ["stop"])) }, n[6] || (n[6] = [ h("svg", { viewBox: "0 0 24 24", width: "24", height: "24" }, [ h("line", { x1: "6", y1: "6", x2: "18", y2: "18", stroke: "#fff", "stroke-width": "2", "stroke-linecap": "round" }), h("line", { x1: "18", y1: "6", x2: "6", y2: "18", stroke: "#fff", "stroke-width": "2", "stroke-linecap": "round" }) ], -1) ])), h("div", ye, [ h("img", { src: i.value, class: "modal-img-full" }, null, 8, ke) ]) ])) : K("", !0) ])), p.value ? (P(), E("canvas", { key: 2, "canvas-id": "watermarkCanvas", id: "watermarkCanvas", style: ne(`width:${M(g)}px; height:${M(y)}px; position: absolute; left: -9999px; top: -9999px;`), type: "2d" }, null, 4)) : K("", !0) ])); } }), Te = (a, t) => { const o = a.__vccOpts || a; for (const [e, r] of t) o[e] = r; return o; }, be = /* @__PURE__ */ Te(ve, [["__scopeId", "data-v-6321a964"]]), Le = (a) => { a.component("WatermarkCamera", be); }; export { be as WatermarkCamera, Le as install };