|
@@ -0,0 +1,434 @@
|
|
|
+<template>
|
|
|
+ <view class="w-full h-screen relative">
|
|
|
+ <camera
|
|
|
+ :device-position="position"
|
|
|
+ resolution="high"
|
|
|
+ :flash="flashMode"
|
|
|
+ frame-size="large"
|
|
|
+ @initdone="initdone"
|
|
|
+ @ready="ready"
|
|
|
+ class="w-full h-85%"
|
|
|
+ ></camera>
|
|
|
+ <view class="absolute left-20rpx bottom-25% text-white">
|
|
|
+ <view class="mb24rpx max-w-180rpx">
|
|
|
+ <view
|
|
|
+ class="font-semibold bg-white h45rpx rounded-8rpx p8rpx flex items-center"
|
|
|
+ ><view class="bg-amber p-4rpx text-black rounded-8rpx">打卡</view>
|
|
|
+ <view class="time-text ml8rpx">{{ dayjs().format("HH:mm") }}</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="px8rpx border-l border-l-solid border-l-amber">
|
|
|
+ <view class=""
|
|
|
+ >贵州省贵阳市观山湖区世纪城街道北京西路8号观山湖区办公大楼13层观山湖区文体广电旅游局</view
|
|
|
+ >
|
|
|
+ <view class="mt2">
|
|
|
+ {{ dayjs().format("YYYY.MM.DD") }} {{ dayjs().format("dddd") }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <canvas
|
|
|
+ id="watermarkCanvas"
|
|
|
+ canvas-id="watermarkCanvas"
|
|
|
+ style="position: fixed; top: -10000px"
|
|
|
+ :style="{
|
|
|
+ width: cavansInfo?.width + 'px',
|
|
|
+ height: cavansInfo?.height + 'px',
|
|
|
+ }"
|
|
|
+ ></canvas>
|
|
|
+ <view
|
|
|
+ class="bg-black h15% bottom-safe-area px120rpx box-border pb20rpx flex items-center w-full justify-between w-full"
|
|
|
+ >
|
|
|
+ <view @click="changeFlashMode">
|
|
|
+ <image
|
|
|
+ src="@/subPack/static/close.png"
|
|
|
+ class="w64rpx h64rpx"
|
|
|
+ v-if="flashMode == 'off'"
|
|
|
+ />
|
|
|
+ <image src="@/subPack/static/open.png" class="w64rpx h64rpx" v-else />
|
|
|
+ </view>
|
|
|
+ <view @click="takePhoto">
|
|
|
+ <image src="@/subPack/static/pic.png" class="w128rpx h128rpx" />
|
|
|
+ </view>
|
|
|
+ <view @click="changePosition">
|
|
|
+ <image src="@/subPack/static/back.png" class="w64rpx h64rpx" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <page-container
|
|
|
+ :show="uploading"
|
|
|
+ :duration="false"
|
|
|
+ :overlay="false"
|
|
|
+ @beforeleave="beforeleave"
|
|
|
+ >
|
|
|
+ <wd-message-box></wd-message-box>
|
|
|
+ </page-container>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { BASE_URL } from "@/config";
|
|
|
+import router from "@/router";
|
|
|
+import type { CameraDevicePosition, CameraFlash } from "@uni-helper/uni-types";
|
|
|
+import "dayjs/locale/zh-cn";
|
|
|
+import dayjs from "dayjs";
|
|
|
+const flashMode = ref<CameraFlash>("off");
|
|
|
+const position = ref<CameraDevicePosition>("back");
|
|
|
+dayjs.locale("zh-cn");
|
|
|
+const cavansInfo = ref<UniApp.GetImageInfoSuccessData>();
|
|
|
+const isShow = ref(false);
|
|
|
+const messageBox = useMessage();
|
|
|
+const FilePath = ref();
|
|
|
+const { send, abort, uploading, data } = useRequest(
|
|
|
+ (FormData) =>
|
|
|
+ Apis.sys.upload({
|
|
|
+ data: FormData,
|
|
|
+ requestType: "upload",
|
|
|
+ fileType: "image",
|
|
|
+ }),
|
|
|
+ {
|
|
|
+ immediate: false,
|
|
|
+ },
|
|
|
+);
|
|
|
+async function initdone() {
|
|
|
+ console.log("相机初始化完成!!!");
|
|
|
+}
|
|
|
+function ready() {
|
|
|
+ console.log("相机初始化成功!!!");
|
|
|
+}
|
|
|
+function changeFlashMode() {
|
|
|
+ flashMode.value = flashMode.value == "off" ? "on" : "off";
|
|
|
+}
|
|
|
+function changePosition() {
|
|
|
+ position.value = position.value == "back" ? "front" : "back";
|
|
|
+}
|
|
|
+function takePhoto() {
|
|
|
+ const ctx = wx.createCameraContext();
|
|
|
+ isShow.value = true;
|
|
|
+ uni.showLoading({ mask: true });
|
|
|
+ ctx.takePhoto({
|
|
|
+ quality: "high",
|
|
|
+ success: async (res) => {
|
|
|
+ if (res.tempImagePath) {
|
|
|
+ const img = await addWatermarkToImage(res.tempImagePath);
|
|
|
+ FilePath.value = img;
|
|
|
+ await send({ filePath: img, name: "file" });
|
|
|
+ console.log(data.value);
|
|
|
+
|
|
|
+ // const path = `${BASE_URL}/sys/common/static/${resData.message}`;
|
|
|
+ // console.log(path, "上传文件");
|
|
|
+ // useCameraStore().setImg(path);
|
|
|
+ // isShow.value = false;
|
|
|
+ // router.back();
|
|
|
+ // uni.hideLoading();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail(err) {
|
|
|
+ isShow.value = false;
|
|
|
+ uni.hideLoading();
|
|
|
+ },
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function beforeleave() {
|
|
|
+ messageBox
|
|
|
+ .confirm({
|
|
|
+ title: "正在上传图片,确认退出吗?",
|
|
|
+ cancelButtonText: "退出",
|
|
|
+ confirmButtonText: "继续上传",
|
|
|
+ closeOnClickModal: false,
|
|
|
+ })
|
|
|
+ .then(async () => {
|
|
|
+ console.log("点击了确定按钮");
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ abort();
|
|
|
+ });
|
|
|
+}
|
|
|
+function addWatermarkToImage(imagePath: string): Promise<string> {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ const ctx = uni.createCanvasContext("watermarkCanvas");
|
|
|
+ console.log("addWatermarkToImage", imagePath);
|
|
|
+
|
|
|
+ uni.getImageInfo({
|
|
|
+ src: imagePath,
|
|
|
+ success: (imageInfo) => {
|
|
|
+ const canvasWidth = imageInfo.width;
|
|
|
+ const canvasHeight = imageInfo.height;
|
|
|
+ cavansInfo.value = imageInfo;
|
|
|
+ ctx.drawImage(imagePath, 0, 0, canvasWidth, canvasHeight);
|
|
|
+ // 字体大小统一为45px
|
|
|
+ const fontSize = 45;
|
|
|
+ const lineHeight = fontSize * 1.2;
|
|
|
+ const padding = 15;
|
|
|
+ const cornerRadius = 10;
|
|
|
+
|
|
|
+ // 计算水印总高度
|
|
|
+ const watermarkHeight = lineHeight * 6 + padding * 3;
|
|
|
+
|
|
|
+ // 计算文本宽度
|
|
|
+ ctx.setFontSize(fontSize);
|
|
|
+ const timeText = dayjs().format("HH:mm");
|
|
|
+ const checkInText = "打卡";
|
|
|
+ const addressText =
|
|
|
+ "贵州省贵阳市观山湖区世纪城街道北京西路8号观山湖区办公大楼13层观山湖区文体广电旅游局";
|
|
|
+ const dateText = `${dayjs().format("YYYY.MM.DD")} ${dayjs().format("dddd")}`;
|
|
|
+ const timeWidth = ctx.measureText(timeText).width;
|
|
|
+ const checkInWidth = ctx.measureText(checkInText).width + padding * 4; // 增加padding确保文字不溢出
|
|
|
+ const firstLineWidth = checkInWidth + timeWidth + padding * 2;
|
|
|
+
|
|
|
+ // 水印位置:左下角,留出边距
|
|
|
+ const margin = 30;
|
|
|
+ const startX = margin;
|
|
|
+ const startY = canvasHeight - watermarkHeight - margin;
|
|
|
+
|
|
|
+ // 1. 绘制第一行白色背景圆角矩形
|
|
|
+ ctx.setFillStyle("#ffffff");
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(startX + cornerRadius, startY);
|
|
|
+ ctx.arcTo(
|
|
|
+ startX + firstLineWidth,
|
|
|
+ startY,
|
|
|
+ startX + firstLineWidth,
|
|
|
+ startY + lineHeight + padding * 2,
|
|
|
+ cornerRadius,
|
|
|
+ );
|
|
|
+ ctx.arcTo(
|
|
|
+ startX + firstLineWidth,
|
|
|
+ startY + lineHeight + padding * 2,
|
|
|
+ startX,
|
|
|
+ startY + lineHeight + padding * 2,
|
|
|
+ cornerRadius,
|
|
|
+ );
|
|
|
+ ctx.arcTo(
|
|
|
+ startX,
|
|
|
+ startY + lineHeight + padding * 2,
|
|
|
+ startX,
|
|
|
+ startY,
|
|
|
+ cornerRadius,
|
|
|
+ );
|
|
|
+ ctx.arcTo(
|
|
|
+ startX,
|
|
|
+ startY,
|
|
|
+ startX + firstLineWidth,
|
|
|
+ startY,
|
|
|
+ cornerRadius,
|
|
|
+ );
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+
|
|
|
+ // 2. 绘制"打卡"黄色背景圆角矩形
|
|
|
+ const checkInY = startY + padding;
|
|
|
+ ctx.setFillStyle("#ffcc00");
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(startX + padding + cornerRadius, checkInY);
|
|
|
+ ctx.arcTo(
|
|
|
+ startX + checkInWidth - padding,
|
|
|
+ checkInY,
|
|
|
+ startX + checkInWidth - padding,
|
|
|
+ checkInY + lineHeight,
|
|
|
+ cornerRadius,
|
|
|
+ );
|
|
|
+ ctx.arcTo(
|
|
|
+ startX + checkInWidth - padding,
|
|
|
+ checkInY + lineHeight,
|
|
|
+ startX + padding,
|
|
|
+ checkInY + lineHeight,
|
|
|
+ cornerRadius,
|
|
|
+ );
|
|
|
+ ctx.arcTo(
|
|
|
+ startX + padding,
|
|
|
+ checkInY + lineHeight,
|
|
|
+ startX + padding,
|
|
|
+ checkInY,
|
|
|
+ cornerRadius,
|
|
|
+ );
|
|
|
+ ctx.arcTo(
|
|
|
+ startX + padding,
|
|
|
+ checkInY,
|
|
|
+ startX + checkInWidth - padding,
|
|
|
+ checkInY,
|
|
|
+ cornerRadius,
|
|
|
+ );
|
|
|
+ ctx.closePath();
|
|
|
+ ctx.fill();
|
|
|
+
|
|
|
+ // 3. 绘制"打卡"文字
|
|
|
+ ctx.setFillStyle("#000000");
|
|
|
+ ctx.setFontSize(fontSize);
|
|
|
+ ctx.font = `bold ${fontSize}px sans-serif`; // 设置字体加粗
|
|
|
+ ctx.fillText(
|
|
|
+ checkInText,
|
|
|
+ startX + padding * 2,
|
|
|
+ checkInY + lineHeight - 5,
|
|
|
+ );
|
|
|
+
|
|
|
+ // 4. 绘制渐变时间文字
|
|
|
+ const timeX = startX + checkInWidth + padding;
|
|
|
+ const timeY = checkInY + lineHeight - 5;
|
|
|
+
|
|
|
+ // 创建渐变
|
|
|
+ const gradient = ctx.createLinearGradient(
|
|
|
+ 0,
|
|
|
+ timeY,
|
|
|
+ 0,
|
|
|
+ timeY + lineHeight,
|
|
|
+ );
|
|
|
+ gradient.addColorStop(0, "#ff6b00"); // 顶部颜色
|
|
|
+ gradient.addColorStop(1, "#ff3c00"); // 底部颜色
|
|
|
+
|
|
|
+ ctx.setFillStyle(gradient);
|
|
|
+ ctx.font = `bold ${fontSize}px sans-serif`; // 设置字体加粗
|
|
|
+ ctx.fillText(timeText, timeX, timeY);
|
|
|
+ // 5. 绘制地址信息(带左边框,支持自动换行)
|
|
|
+ const maxWidth = canvasWidth - startX * 2 - padding * 6; // 最大宽度
|
|
|
+ const addressStartY = startY + lineHeight + padding * 3 + 60;
|
|
|
+
|
|
|
+ // 绘制地址文本(支持换行)
|
|
|
+ function drawTextWithWrap(
|
|
|
+ text: string,
|
|
|
+ x: number,
|
|
|
+ y: number,
|
|
|
+ maxWidth: number,
|
|
|
+ lineHeight: number,
|
|
|
+ ) {
|
|
|
+ let line = "";
|
|
|
+ let currentY = y;
|
|
|
+ for (let i = 0; i < text.length; i++) {
|
|
|
+ const testLine = line + text[i];
|
|
|
+ const metrics = ctx.measureText(testLine);
|
|
|
+ const testWidth = metrics.width;
|
|
|
+
|
|
|
+ if (testWidth > maxWidth && i > 0) {
|
|
|
+ // 绘制当前行并换行
|
|
|
+ ctx.fillText(line, x, currentY);
|
|
|
+ line = text[i];
|
|
|
+ currentY += lineHeight;
|
|
|
+ } else {
|
|
|
+ line = testLine;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 绘制最后一行
|
|
|
+ ctx.fillText(line, x, currentY);
|
|
|
+ return currentY; // 返回最后一行的Y坐标
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制地址左边框(支持多行)
|
|
|
+ ctx.setStrokeStyle("#ffcc00");
|
|
|
+ ctx.setLineWidth(6);
|
|
|
+
|
|
|
+ // 先测量地址文本需要多少行
|
|
|
+ const tempCtx = uni.createCanvasContext("watermarkCanvas");
|
|
|
+ tempCtx.setFontSize(fontSize);
|
|
|
+ let addressLines = [];
|
|
|
+ let addressLine = "";
|
|
|
+ for (let i = 0; i < addressText.length; i++) {
|
|
|
+ const testLine = addressLine + addressText[i];
|
|
|
+ const metrics = tempCtx.measureText(testLine);
|
|
|
+ const testWidth = metrics.width;
|
|
|
+
|
|
|
+ if (testWidth > maxWidth && i > 0) {
|
|
|
+ addressLines.push(addressLine);
|
|
|
+ addressLine = addressText[i];
|
|
|
+ } else {
|
|
|
+ addressLine = testLine;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ addressLines.push(addressLine);
|
|
|
+
|
|
|
+ // 绘制地址左边框(每行一个)
|
|
|
+ for (let i = 0; i < addressLines.length; i++) {
|
|
|
+ const lineY = addressStartY + i * lineHeight;
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(startX + padding, lineY - lineHeight + 5);
|
|
|
+ ctx.lineTo(startX + padding, lineY + 5);
|
|
|
+ ctx.stroke();
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx.setFillStyle("#ffffff");
|
|
|
+ // 绘制地址文本(自动换行)
|
|
|
+ const lastAddressLineY = drawTextWithWrap(
|
|
|
+ addressText,
|
|
|
+ startX + padding * 3,
|
|
|
+ addressStartY,
|
|
|
+ maxWidth,
|
|
|
+ lineHeight,
|
|
|
+ );
|
|
|
+
|
|
|
+ // 6. 绘制日期信息(带左边框)
|
|
|
+ const dateY = lastAddressLineY + lineHeight;
|
|
|
+ // 绘制黄色左边框
|
|
|
+ ctx.setStrokeStyle("#ffcc00");
|
|
|
+ ctx.setLineWidth(6);
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(startX + padding, dateY - lineHeight + 5);
|
|
|
+ ctx.lineTo(startX + padding, dateY + 5);
|
|
|
+ ctx.stroke();
|
|
|
+
|
|
|
+ ctx.setFillStyle("#ffffff");
|
|
|
+ // 增加与左边框的距离
|
|
|
+ ctx.fillText(dateText, startX + padding * 3, dateY);
|
|
|
+ ctx.draw(false, () => {
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.canvasToTempFilePath({
|
|
|
+ canvasId: "watermarkCanvas",
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ width: canvasWidth,
|
|
|
+ height: canvasHeight,
|
|
|
+ destWidth: canvasWidth,
|
|
|
+ destHeight: canvasHeight,
|
|
|
+ quality: 1,
|
|
|
+ success: (res) => {
|
|
|
+ console.log("水印图片生成成功:", res.tempFilePath);
|
|
|
+ resolve(res.tempFilePath);
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error("导出水印图片失败:", err);
|
|
|
+ reject(err);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }, 100);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error("获取图片信息失败:", err);
|
|
|
+ reject(err);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function getArryBuffer(filePath: string) {
|
|
|
+ return new Promise<ArrayBuffer>((resolve, reject) => {
|
|
|
+ uni.getFileSystemManager().readFile({
|
|
|
+ filePath: filePath,
|
|
|
+ encoding: "binary",
|
|
|
+ success(res) {
|
|
|
+ resolve(res.data as ArrayBuffer);
|
|
|
+ },
|
|
|
+ fail(err) {
|
|
|
+ reject(err);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ });
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.time-text {
|
|
|
+ background: linear-gradient(to bottom, #02459f, #000);
|
|
|
+ -webkit-background-clip: text;
|
|
|
+ -webkit-text-fill-color: transparent;
|
|
|
+ background-clip: text;
|
|
|
+ text-fill-color: transparent;
|
|
|
+}
|
|
|
+</style>
|
|
|
+<route lang="json">
|
|
|
+{
|
|
|
+ "name": "Camera",
|
|
|
+ "style": {
|
|
|
+ "navigationBarTitleText": "",
|
|
|
+ "navigationStyle": "custom",
|
|
|
+ "disableScroll": true
|
|
|
+ }
|
|
|
+}
|
|
|
+</route>
|