tangning 2 ヶ月 前
コミット
1fc70db698

+ 15 - 15
components.d.ts

@@ -7,20 +7,20 @@ export {}
 
 declare module 'vue' {
   export interface GlobalComponents {
-    AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider'];
-    RouterLink: typeof import('vue-router')['RouterLink'];
-    RouterView: typeof import('vue-router')['RouterView'];
-    TitleBar: typeof import('./src/components/TitleBar/index.vue')['default'];
-    VanButton: typeof import('vant/es')['Button'];
-    VanCellGroup: typeof import('vant/es')['CellGroup'];
-    VanField: typeof import('vant/es')['Field'];
-    VanForm: typeof import('vant/es')['Form'];
-    VanList: typeof import('vant/es')['List'];
-    VanPopup: typeof import('vant/es')['Popup'];
-    VanRadio: typeof import('vant/es')['Radio'];
-    VanRadioGroup: typeof import('vant/es')['RadioGroup'];
-    VanTab: typeof import('vant/es')['Tab'];
-    VanTabs: typeof import('vant/es')['Tabs'];
-    VanUploader: typeof import('vant/es')['Uploader'];
+    AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
+    RouterLink: typeof import('vue-router')['RouterLink']
+    RouterView: typeof import('vue-router')['RouterView']
+    TitleBar: typeof import('./src/components/TitleBar/index.vue')['default']
+    VanButton: typeof import('vant/es')['Button']
+    VanCellGroup: typeof import('vant/es')['CellGroup']
+    VanField: typeof import('vant/es')['Field']
+    VanForm: typeof import('vant/es')['Form']
+    VanList: typeof import('vant/es')['List']
+    VanPopup: typeof import('vant/es')['Popup']
+    VanRadio: typeof import('vant/es')['Radio']
+    VanRadioGroup: typeof import('vant/es')['RadioGroup']
+    VanTab: typeof import('vant/es')['Tab']
+    VanTabs: typeof import('vant/es')['Tabs']
+    VanUploader: typeof import('vant/es')['Uploader']
   }
 }

+ 2 - 0
index.html

@@ -10,11 +10,13 @@
     <meta name="format-detection" content="telephone=no, email=no, date=no, address=no" />
     <title>四维时代售后</title>
     <script src="//res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>
+    <script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
   </head>
   <body>
     <div id="app"></div>
     <script type="module" src="/src/main.ts"></script>
     <script>
+       var vConsole = new window.VConsole();
       window.onload = function () {
         document.addEventListener('touchstart', function (event) {
           if (event.touches.length > 1) {

+ 2 - 1
package.json

@@ -38,7 +38,8 @@
     "vant": "^4.0.11",
     "vue": "^3.2.45",
     "vue-i18n": "^9.2.2",
-    "vue-router": "^4.1.6"
+    "vue-router": "^4.1.6",
+    "vue-signature-pad": "^3.0.2"
   },
   "devDependencies": {
     "@typescript-eslint/eslint-plugin": "^5.47.0",

+ 7 - 0
src/api/feedback.ts

@@ -31,3 +31,10 @@ export const feedbackAdd = (params) =>
     method: 'post',
     data: params,
   });
+
+export const signFor = (params) =>
+  request({
+    url: `/service/sale/salePersonnel/signFor`,
+    method: 'post',
+    data: params,
+  });

+ 1 - 1
src/router/index.ts

@@ -11,7 +11,7 @@ const router: Router = createRouter({
 
 router.beforeEach(async (_to, _from, next) => {
   console.log('_to', _to);
-  const wxOpenId = useCookies().get('wxOpenId');
+  const wxOpenId = 'asdasdas';
   if (_to.name == 'feedback' || _to.name == 'feedbacksuccess') {
     const current = window.navigator.language || window.navigator.userLanguage || localStorage.getItem('lang') || null;
     let mytitle = '产品建议';

+ 9 - 0
src/router/routes.ts

@@ -77,6 +77,15 @@ const routes = [
         },
       },
       {
+        path: 'signature/:id',
+        name: 'evaluate',
+        component: () => import('/@/views/detail/signature.vue'),
+        meta: {
+          title: '评价',
+          keepAlive: true,
+        },
+      },
+      {
         path: 'feedback',
         name: 'feedback',
         component: () => import('/@/views/feedback/index.vue'),

+ 1 - 1
src/store/modules/user.ts

@@ -16,7 +16,7 @@ export const useUserStore = defineStore({
   id: 'user',
   state: (): StoreUser => ({
     token: token,
-    wxOpenId: '',
+    wxOpenId: '20250603151032200',
     info: {
       name: 'test',
       wxOpenId: 'test1',

+ 151 - 0
src/views/detail/popup/SignatureModal.vue

@@ -0,0 +1,151 @@
+<template>
+  <van-popup v-model:show="isShowSignature" :style="{ height: '100%', width: '100%', maxWidth: '100vw' }" @close="handleClose">
+    <!-- 横屏提示,可根据需求决定是否保留 -->
+    <div class="signature-tip" v-if="isLandscape"> 请横屏进行签名操作 </div>
+    <div class="signature-container">
+      <vue-signature-pad ref="signaturePad" :options="signaturePadOptions" class="signature-pad" />
+      <div class="signature-btns">
+        <van-button type="primary" @click="handleClear">重新签名</van-button>
+        <van-button type="info" @click="handleConfirm">确认签名</van-button>
+      </div>
+    </div>
+  </van-popup>
+</template>
+
+<script setup>
+  import { ref, onMounted, watch } from 'vue';
+  // import { VanPopup, VanButton } from 'vant';
+  import { VueSignaturePad } from 'vue-signature-pad';
+  import { useWindowSize } from '@vueuse/core'; // 用于监听窗口尺寸变化
+  const emit = defineEmits(['submit']);
+  const isShowSignature = ref(false);
+  const signaturePad = ref(null);
+  const signaturePadOptions = ref({
+    penColor: 'rgb(0, 0, 0)', // 画笔颜色
+    backgroundColor: 'rgb(255, 255, 255)', // 画布背景色
+  });
+  const { width, height } = useWindowSize(); // 获取窗口宽高
+  const isLandscape = ref(false); // 是否横屏标识
+
+  // 监听窗口尺寸变化,判断是否横屏
+  watch([width, height], () => {
+    isLandscape.value = width.value > height.value;
+  });
+
+  // 打开签名弹窗方法,供外部调用
+  const openSignature = () => {
+    isShowSignature.value = true;
+    // 这里可尝试触发横屏,部分设备或浏览器可能不支持自动强制横屏,可提示用户手动横屏
+    // 比如:screen.orientation.lock('landscape').catch(err => console.warn(err));
+  };
+  defineExpose({
+    openSignature,
+  });
+  //将base64转换为file
+  const dataURLtoFile = (dataurl, filename) => {
+    var arr = dataurl.split(','),
+      mime = arr[0].match(/:(.*?);/)[1],
+      bstr = atob(arr[1]),
+      n = bstr.length,
+      u8arr = new Uint8Array(n);
+    while (n--) {
+      u8arr[n] = bstr.charCodeAt(n);
+    }
+    return new File([u8arr], filename, { type: mime });
+  };
+  function blobToFile(blob, fileName, options = {}) {
+    // 如果已经是 File 对象,直接返回
+    if (blob instanceof File) {
+      return blob;
+    }
+
+    // 提取或设置 MIME 类型
+    const type = options.type || blob.type || 'application/octet-stream';
+
+    // 创建 File 对象
+    const file = new File([blob], fileName, {
+      type,
+      lastModified: options.lastModified || new Date().getTime(),
+    });
+
+    return file;
+  }
+  // 清空签名
+  const handleClear = () => {
+    if (signaturePad.value) {
+      signaturePad.value.clearSignature();
+    }
+  };
+  // 关闭弹窗
+  const getImg = () => {
+    const response = signaturePad.value.saveSignature();
+    if (response.isEmpty) {
+      return response;
+    } else {
+      // 转成二进制形式
+      const binaryData = convertBase64ToBinary(response.data);
+      const blob = new Blob([binaryData], { type: 'image/png' });
+      // console.log('+子组件43+', blob)
+      return blob;
+    }
+  };
+
+  // 确认签名(可根据需求将签名数据传递给父组件等)
+  const handleConfirm = () => {
+    let img = getImg();
+    emit('submit', blobToFile(img, '签名图片.png'));
+    isShowSignature.value = false;
+  };
+
+  function convertBase64ToBinary(base64Str) {
+    // 去除data:image/png;base64,这部分,只保留Base64编码的字符串
+    const base64Data = base64Str.split(',')[1];
+    // 使用atob函数解码Base64字符串
+    const binaryStr = atob(base64Data);
+    // 创建一个Uint8Array来保存二进制数据
+    const len = binaryStr.length;
+    const bytes = new Uint8Array(len);
+    for (let i = 0; i < len; i++) {
+      bytes[i] = binaryStr.charCodeAt(i);
+    }
+    // 返回Uint8Array对象,或者根据需要进一步处理
+    return bytes;
+  }
+  // 关闭弹窗
+  const handleClose = () => {
+    isShowSignature.value = false;
+    handleClear(); // 关闭时清空签名,可根据需求调整
+  };
+</script>
+
+<style scoped>
+  .van-popup {
+    display: flex;
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+  }
+  .signature-tip {
+    margin-bottom: 10px;
+    font-size: 14px;
+    color: #999;
+  }
+  .signature-container {
+    width: 90%;
+    height: 60%;
+    border: 1px solid #eee;
+    border-radius: 8px;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+  }
+  .signature-pad {
+    flex: 1;
+  }
+  .signature-btns {
+    display: flex;
+    justify-content: space-around;
+    padding: 10px;
+    background-color: #fff;
+  }
+</style>

+ 263 - 0
src/views/detail/signature.vue

@@ -0,0 +1,263 @@
+<template>
+  <div class="container">
+    <div class="header">
+      <span>签名确认</span>
+      <span class="close" @click="onClose">×</span>
+    </div>
+    <div class="content">
+      <div class="info">
+        <p> 尊敬的<span class="blue">张先生</span>: </p>
+        <p>
+          您送修的
+          <span class="blue">iPhone 14 Pro Max</span>
+          已维修完毕。如已拿到货物,请在下方进行签收,感谢您的配合!
+        </p>
+      </div>
+      <div class="sign-area">
+        <div class="sign-title">请在下方签名</div>
+        <div class="sign-tip">签名需清晰可辨识</div>
+        <div class="sign-box" @click="startSign">
+          <canvas ref="canvasRef" class="sign-canvas"></canvas>
+          <div class="sign-placeholder" v-if="isEmpty">
+            <i class="icon-pen"></i>
+            点击此处开始签名
+          </div>
+          <img v-else :src="signatureImg.url" alt="">
+        </div>
+        <div class="sign-actions">
+          <button class="reset-btn" @click="clearSign">重新签名</button>
+        </div>
+      </div>
+    </div>
+    <div class="footer">
+      <van-button class="close-btn" @click="onClose">关闭</van-button>
+      <van-button type="primary" class="confirm-btn" @click="onConfirm">确认签名</van-button>
+    </div>
+    <SignatureModal ref="signatureModalRef" @submit="handleImg"/>
+  </div>
+</template>
+
+<script setup>
+  import { ref, onMounted, unref  } from 'vue';
+  import { signFor } from '/@/api/feedback'
+  import { showLoadingToast, showToast } from 'vant';
+  import { useRouter } from 'vue-router';
+  import axios from 'axios';
+  import SignatureModal from './popup/SignatureModal.vue';
+  const canvasRef = ref(null);
+  const signaturePad = ref(null);
+  const isEmpty = ref(true);
+  const isShowSignature = ref(false);
+  const signatureModalRef = ref(null);
+  const signatureImg = ref({
+    file: '',
+    url: ''
+  });
+  onMounted(() => {
+  });
+
+  const router = useRouter();
+  const { id } = unref(router.currentRoute)?.params;
+
+  function startSign() {
+    // 触发canvas签名
+    console.log('signatureModalRef', signatureModalRef.value);
+     signatureModalRef.value.openSignature();
+  }
+  function handleImg(val) {
+    // 触发canvas签名
+    signatureImg.value = {
+      file: val,
+      url: URL.createObjectURL(val)
+    };
+    isEmpty.value = false
+    console.log('signatureModalRef', val, signatureImg.value);
+    //  signatureModalRef.value.openSignature();
+  }
+
+  function onClose() {
+    // 关闭弹窗逻辑
+  }
+  function clearSign() {
+    isEmpty.value = true
+    signatureImg.value = {
+      file: '',
+      url: ''
+    };
+    signatureModalRef.value.openSignature();
+    // 关闭弹窗逻辑
+  }
+  
+  async function onConfirm() {
+    if (isEmpty.value) {
+      alert('请先签名');
+      return;
+    }
+    let formData = new FormData();
+    showLoadingToast({
+      message: 'loading...',
+      forbidClick: true,
+    });
+    // 调用append()方法添加数据
+    formData.append('file', signatureImg.value.file);
+    let {data} = await axios({
+      url: '/service/manage/common/upload/files',
+      method: 'POST',
+      data: formData,
+      headers: {
+        'Content-Type': 'multipart/form-data',
+      },
+    });
+    await signFor({
+      repairId: id,
+      trackingImg: data.data
+    })
+    showToast('操作成功');
+    setTimeout(() => {
+      router.replace('/home');
+      }, 500);
+    // 提交签名图片
+  }
+</script>
+
+<style scoped lang="scss">
+  .container {
+    max-width: 400px;
+    margin: 0 auto;
+    border-radius: 20px;
+    border: 1px solid #eee;
+    background: #fff;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    min-height: 90vh;
+  }
+  .header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 1.2rem 1.5rem;
+    font-size: 16px;
+    border-bottom: 1px solid #f0f0f0;
+  }
+  .close {
+    font-size: 14px;
+    cursor: pointer;
+  }
+  .content {
+    flex: 1;
+    padding: 1.5rem;
+  }
+  .info {
+    background: #f7f8fa;
+    border-radius: 10px;
+    padding: 1rem;
+    margin-bottom: 1.5rem;
+    font-size: 14px;
+    color: #333;
+  }
+  .blue {
+    color: #1677ff;
+  }
+  .sign-area {
+    margin-top: 1rem;
+  }
+  .sign-title {
+    font-weight: bold;
+    margin-bottom: 0.3rem;
+  }
+  .sign-tip {
+    color: #999;
+    font-size: 14px;
+    margin-bottom: 0.5rem;
+  }
+  .sign-box {
+    position: relative;
+    border: 1px dashed #ccc;
+    border-radius: 8px;
+    height: 120px;
+    background: #fafbfc;
+    margin-bottom: 0.5rem;
+    img{
+      width: 100%;
+    height: 100%;
+    object-fit: scale-down;
+    position: absolute;
+    top: 0;
+    }
+  }
+  .sign-canvas {
+    width: 100%;
+    height: 120px;
+    display: block;
+    border-radius: 8px;
+  }
+  .sign-placeholder {
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    color: #bbb;
+    font-size: 12px;
+    pointer-events: none;
+  }
+  .icon-pen {
+    display: block;
+    width: 24px;
+    height: 24px;
+    background: url('data:image/svg+xml;utf8,<svg fill="gray" viewBox="0 0 24 24" ... />') no-repeat center/contain;
+    margin-bottom: 0.3rem;
+  }
+  .sign-actions {
+    display: flex;
+    justify-content: flex-end;
+  }
+  .reset-btn {
+    background: none;
+    border: none;
+    color: #1677ff;
+    cursor: pointer;
+    font-size: 12px;
+  }
+  .footer {
+    display: flex;
+    // border-top: 1px solid #f0f0f0;
+  }
+  .close-btn,
+  .confirm-btn {
+    flex: 1;
+    padding: 1rem 0;
+    margin: 1rem;
+    font-size: 14px;
+    border: none;
+    cursor: pointer;
+  }
+  .close-btn {
+    background: #f7f8fa;
+    color: #333;
+  }
+  .confirm-btn {
+    background: #1677ff;
+    color: #fff;
+    // border-radius: 0 0 0 20px;
+  }
+  @media (max-width: 500px) {
+    .container {
+      max-width: 100vw;
+      min-height: 100vh;
+      border-radius: 0;
+    }
+    .header,
+    .content,
+    .footer {
+      padding-left: 1rem;
+      padding-right: 1rem;
+    }
+  }
+</style>

ファイルの差分が大きいため隠しています
+ 8522 - 0
yarn.lock