فهرست منبع

Merge remote-tracking branch 'origin/dev-frontend' into dev-frontend

panqiuyao 9 ماه پیش
والد
کامیت
a9fc67ee1e

+ 20 - 1
frontend/src/apis/user.ts

@@ -1,4 +1,4 @@
-import { GET,POST } from "@/utils/http";
+import { GET,POST ,UPLOAD } from "@/utils/http";
 import type { UserRequest } from "@/apis/types/user";
 
 export async function getUserInfo(data:UserRequest){
@@ -9,3 +9,22 @@ export async function login(data:UserRequest){
     return POST('/api/auth/login',data)
 
 }
+
+
+export function upload(params: any) {
+    const form = new FormData()
+    Object.keys(params).map((item) => {
+      form.append(item, params[item])
+    })
+    return POST('/upload', form, {
+      cancelRepeatKey: `${Date.now}${Math.random()}`,
+      headers: {
+        'Content-Type': 'multipart/form-data',
+      },
+    })
+  }
+// 获取验证码
+export function sendCode(data:UserRequest) {
+    return POST('/api/auth/send_code',data)
+  }
+  

BIN
frontend/src/assets/images/Photography/loading-bg.png


BIN
frontend/src/assets/images/Photography/wenjian.png


BIN
frontend/src/assets/images/Photography/zhuangshi.png


BIN
frontend/src/assets/images/check/icon1.png


BIN
frontend/src/assets/images/check/icon2.png


+ 12 - 0
frontend/src/assets/images/login/login-code.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>safety certificate</title>
+    <g id="切图" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g transform="translate(-562.000000, -57.000000)" fill-rule="nonzero" id="safety-certificate">
+            <g transform="translate(562.000000, 57.000000)">
+                <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="24" height="24"></rect>
+                <path d="M20.3179688,3.98203125 L12.3539063,1.26796875 C12.2578125,1.23515625 12.1289063,1.21875 12,1.21875 C11.8710938,1.21875 11.7421875,1.23515625 11.6460938,1.26796875 L3.68203125,3.98203125 C3.4875,4.04765625 3.328125,4.27265625 3.328125,4.47890625 L3.328125,15.7851562 C3.328125,15.9914062 3.46171875,16.2632812 3.6234375,16.3921875 L11.7023438,22.6875 C11.784375,22.7507813 11.8898438,22.7835938 11.9976563,22.7835938 C12.1054688,22.7835938 12.2132813,22.7507813 12.2929688,22.6875 L20.371875,16.3921875 C20.5335938,16.265625 20.6672883,15.99375 20.6672883,15.7851562 L20.6672883,4.47890625 C20.671875,4.27265625 20.5125,4.05 20.3179688,3.98203125 Z M18.984375,15.3351562 L12,20.7773438 L5.015625,15.3351562 L5.015625,5.31328125 L12,2.93203125 L18.984375,5.31328125 L18.984375,15.3351562 Z M9.4734375,10.6242187 C9.403125,10.528125 9.290625,10.4695312 9.16875,10.4695312 L7.875,10.4695312 C7.72265625,10.4695312 7.63359375,10.6429687 7.72265625,10.7671875 L10.6851562,14.8453125 C10.8351562,15.0515625 11.1445312,15.0515625 11.2945312,14.8453125 L16.2773438,7.98515625 C16.3664062,7.8609375 16.2773438,7.6875 16.125,7.6875 L14.83125,7.6875 C14.7117187,7.6875 14.596875,7.74609375 14.5265625,7.8421875 L10.9898437,12.7125 L9.4734375,10.6242187 Z" id="形状" fill="#606266"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 12 - 0
frontend/src/assets/images/login/login-lock.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>lock</title>
+    <g id="切图" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g transform="translate(-487.000000, -57.000000)" fill-rule="nonzero" id="lock">
+            <g transform="translate(487.000000, 57.000000)">
+                <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="24" height="24"></rect>
+                <path d="M19.5,10.875 L17.90625,10.875 L17.90625,5.625 C17.90625,3.96796875 16.5632813,2.625 14.90625,2.625 L9.09375,2.625 C7.43671875,2.625 6.09375,3.96796875 6.09375,5.625 L6.09375,10.875 L4.5,10.875 C4.08515625,10.875 3.75,11.2101563 3.75,11.625 L3.75,20.625 C3.75,21.0398437 4.08515625,21.375 4.5,21.375 L19.5,21.375 C19.9148437,21.375 20.25,21.0398437 20.25,20.625 L20.25,11.625 C20.25,11.2101563 19.9148437,10.875 19.5,10.875 Z M7.78125,5.625 C7.78125,4.90078125 8.36953125,4.3125 9.09375,4.3125 L14.90625,4.3125 C15.6304687,4.3125 16.21875,4.90078125 16.21875,5.625 L16.21875,10.875 L7.78125,10.875 L7.78125,5.625 Z M18.5625,19.6875 L5.4375,19.6875 L5.4375,12.5625 L18.5625,12.5625 L18.5625,19.6875 Z M11.34375,16.4296875 L11.34375,17.671875 C11.34375,17.775 11.428125,17.859375 11.53125,17.859375 L12.46875,17.859375 C12.571875,17.859375 12.65625,17.775 12.65625,17.671875 L12.65625,16.4296875 C12.9398438,16.2257812 13.125,15.8929688 13.125,15.515625 C13.125,14.8945312 12.6210938,14.390625 12,14.390625 C11.3789062,14.390625 10.875,14.8945312 10.875,15.515625 C10.875,15.8929688 11.0601562,16.2257812 11.34375,16.4296875 Z" id="形状" fill="#606266"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 12 - 0
frontend/src/assets/images/login/login-mobile.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>mobile</title>
+    <g id="切图" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g transform="translate(-527.000000, -57.000000)" fill-rule="nonzero" id="mobile">
+            <g transform="translate(527.000000, 57.000000)">
+                <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="24" height="24"></rect>
+                <path d="M17.4375,1.453125 L6.5625,1.453125 C5.73515625,1.453125 5.0625,2.12578125 5.0625,2.953125 L5.0625,20.953125 C5.0625,21.7804687 5.73515625,22.453125 6.5625,22.453125 L17.4375,22.453125 C18.2648437,22.453125 18.9375,21.7804687 18.9375,20.953125 L18.9375,2.953125 C18.9375,2.12578125 18.2648437,1.453125 17.4375,1.453125 Z M17.25,20.765625 L6.75,20.765625 L6.75,3.140625 L17.25,3.140625 L17.25,20.765625 Z M11.0625,18.375 C11.0625,18.892767 11.482233,19.3125 12,19.3125 C12.517767,19.3125 12.9375,18.892767 12.9375,18.375 C12.9375,17.857233 12.517767,17.4375 12,17.4375 C11.482233,17.4375 11.0625,17.857233 11.0625,18.375 Z" id="形状" fill="#606266"></path>
+            </g>
+        </g>
+    </g>
+</svg>

+ 12 - 0
frontend/src/assets/images/login/login-user.svg

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>user</title>
+    <g id="切图" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g transform="translate(-450.000000, -57.000000)" fill-rule="nonzero" id="user">
+            <g transform="translate(450.000000, 57.000000)">
+                <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="24" height="24"></rect>
+                <path d="M20.1210938,17.896875 C19.678125,16.846875 19.040625,15.9046875 18.2320313,15.0960937 C17.4234375,14.2875 16.48125,13.6523438 15.43125,13.2070312 C15.421875,13.2023437 15.4125,13.2 15.403125,13.1953125 C16.8632813,12.140625 17.8125,10.4226562 17.8125,8.484375 C17.8125,5.2734375 15.2109375,2.671875 12,2.671875 C8.7890625,2.671875 6.1875,5.2734375 6.1875,8.484375 C6.1875,10.4226562 7.13671875,12.140625 8.596875,13.1976563 C8.5875,13.2023438 8.578125,13.2046875 8.56875,13.209375 C7.51875,13.6523438 6.5765625,14.2875 5.76796875,15.0984375 C4.959375,15.9070313 4.32421875,16.8492188 3.87890625,17.8992188 C3.44296875,18.9257812 3.2109375,20.015625 3.18745233,21.1359375 C3.18515625,21.2414062 3.26953125,21.328125 3.375,21.328125 L4.78125,21.328125 C4.884375,21.328125 4.96640625,21.2460938 4.96875,21.1453125 C5.015625,19.3359375 5.7421875,17.6414063 7.0265625,16.3570313 C8.35546875,15.028125 10.1203125,14.296875 12,14.296875 C13.8796875,14.296875 15.6445313,15.028125 16.9734375,16.3570313 C18.2578125,17.6414063 18.984375,19.3359375 19.03125,21.1453125 C19.0335938,21.2484375 19.115625,21.328125 19.21875,21.328125 L20.625,21.328125 C20.7304688,21.328125 20.8148438,21.2414062 20.8125477,21.1359375 C20.7890625,20.015625 20.5570313,18.9257812 20.1210938,17.896875 Z M12,12.515625 C10.9242188,12.515625 9.91171875,12.0960938 9.15,11.334375 C8.38828125,10.5726563 7.96875,9.56015625 7.96875,8.484375 C7.96875,7.40859375 8.38828125,6.39609375 9.15,5.634375 C9.91171875,4.87265625 10.9242188,4.453125 12,4.453125 C13.0757813,4.453125 14.0882813,4.87265625 14.85,5.634375 C15.6117188,6.39609375 16.03125,7.40859375 16.03125,8.484375 C16.03125,9.56015625 15.6117188,10.5726563 14.85,11.334375 C14.0882813,12.0960938 13.0757813,12.515625 12,12.515625 Z" id="形状" fill="#606266"></path>
+            </g>
+        </g>
+    </g>
+</svg>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 7 - 0
frontend/src/assets/images/xinxi.svg


+ 186 - 0
frontend/src/components/check/index.vue

@@ -0,0 +1,186 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="title"
+    :close-on-click-modal="false"
+    width="500px"
+    class="hardware-check-dialog"
+  >
+    <div class="hardware-check-container">
+      <el-timeline>
+        <!-- Step 1 -->
+        <el-timeline-item
+          :type="'primary'"
+          :hollow="true">
+          <template #dot>
+            <img src="@/assets/images/check/icon1.png" class="custom-timeline-icon" />
+          </template>
+          <div class="step-content">
+            <div class="step-title">
+              <template v-if="checkLoading">正在初始化检测硬件......</template>
+              <template v-else-if="checkSuccess">硬件检测完成</template>
+              <template v-else>初始化检测硬件失败</template>
+            </div>
+            <el-progress 
+              :percentage="progress" 
+              :colors="['#2FB0FF', '#B863FB']"
+              :stroke-width="8"
+              v-if="checkLoading"
+            ></el-progress>
+            <div class="check-error-result" v-else-if="!checkSuccess">失败的原因,比如请检查硬件链接</div>
+          </div>
+        </el-timeline-item>
+        
+        <!-- Step 2 -->
+        <el-timeline-item
+          :type="'primary'"
+          :hollow="true">
+          <template #dot>
+            <img src="@/assets/images/check/icon2.png" class="custom-timeline-icon" />
+          </template>
+          <div class="step-content">
+            <div class="step-title">自我检查</div>
+            <div class="check-result">
+              <p>补光灯电源和链接器是否正常连上。</p>
+              <p>请检查红外线器是否正常显示。</p>
+            </div>
+          </div>
+        </el-timeline-item>
+      </el-timeline>
+    </div>
+    <template #footer v-if="!checkLoading">
+        <div class="flex" v-if="!checkSuccess">
+            <div class="check-btn" @click="reCheck">重新监测</div>
+        </div>
+        <div class="flex" v-else>
+            <div class="check-btn">正常,开始下一步</div>
+        </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { ref, computed, watch, onBeforeUnmount, nextTick } from 'vue';
+
+const props = defineProps({
+  modelValue: {
+    type: Boolean,
+    default: false
+  },
+  title: {
+    type: String,
+    default: '检测硬件'
+  }
+});
+const checkSuccess = ref(false);
+const checkLoading = ref(false)
+const emit = defineEmits(['update:modelValue', 'confirm']);
+
+const progress = ref(80);
+let timer = null;
+
+const visible = computed({
+  get() {
+    return props.modelValue;
+  },
+  set(val) {
+    emit('update:modelValue', val);
+  }
+});
+
+function startProgress() {
+  progress.value = 0;
+  checkLoading.value = true;
+  timer = setInterval(() => {
+    if (progress.value < 80) {
+      progress.value += 1;
+    } else {
+      checkSuccess.value = false;
+      clearInterval(timer);
+    }
+  }, 50);
+}
+
+function reCheck() {
+    startProgress();
+}
+
+
+function handleConfirm() {
+  emit('confirm');
+  visible.value = false;
+}
+
+watch(visible, (val) => {
+  if (val) {
+    nextTick(() => {
+      startProgress();
+    });
+  } else {
+    clearInterval(timer);
+  }
+});
+
+onBeforeUnmount(() => {
+  clearInterval(timer);
+});
+</script>
+
+<style lang="scss" scoped>
+.hardware-check-container {
+  padding: 20px 0;
+}
+
+.step-content {
+  flex: 1;
+}
+
+.step-title {
+font-weight: 600;
+font-size: 14px;
+color: #000000;
+  margin-bottom: 15px;
+  padding-top: 4px;
+  text-align: left;
+}
+
+.progress-text {
+  text-align: right;    
+  margin-top: 5px;
+  color: #606266;
+}
+
+.check-result {
+    color: rgba(0,0,0,0.45);
+  text-align: left;
+  p {
+    margin: 8px 0;
+  }
+}
+
+:deep(.el-progress-bar__inner) {
+  background-image: linear-gradient(to right, #409EFF, #7e57c2);
+}
+.custom-timeline-icon{
+    width: 24px;
+    height: 24px;
+    position: relative;
+    left: -7px;
+}
+.check-btn{
+    width: 100px;
+    height: 40px;
+    background: linear-gradient( 135deg, #2FB0FF 0%, #B863FB 100%);
+    border-radius: 4px;
+    font-weight: 400;
+    font-size: 16px;
+    color: #FFFFFF;
+    text-align: center;
+    line-height: 40px;
+}
+.check-error-result{
+    font-size: 14px;
+    text-align: left;
+    color: #FF4C00;
+}
+</style>

+ 381 - 0
frontend/src/components/login/index.vue

@@ -0,0 +1,381 @@
+<template>
+  <div class="login-box">
+    <el-dialog
+      v-model="showDialog"
+      width="354px"
+      custom-class="login-dialog"
+      :close-on-click-modal="false"
+      :show-close="true"
+      @close="emit('update:dialogVisible',false)"
+    >
+    <div class="login-box">
+      <el-tabs
+        class="login-box__tabs"
+        v-model="activeTab"
+      >
+        <el-tab-pane label="账号密码登录" name="0" />
+        <el-tab-pane label="验证码登录" name="1" />
+      </el-tabs>
+
+      <el-form
+        ref="loginForm"
+        :model="loginForm"  
+        :rules="loginRules"
+        autocomplete="on"
+        label-position="left"
+      >
+        <div class="title__main">欢迎!</div>
+        <div class="title_sub">登录拍照机系统</div>
+
+        <el-form-item class="login-input" prop="username">
+          <div class="login-icon">
+            <img src="@/assets/images/login/login-user.svg" v-if="activeTab === '0'"/>
+            <img src="@/assets/images/login/login-code.svg" v-else/>
+          </div>
+          <el-input
+            v-model="loginForm.username"
+            placeholder="请输入用户名"
+            name="username"
+            type="text"
+            maxlength="30"
+            style="width: 270px;"
+            tabindex="1"
+            autocomplete="on"
+          >
+          </el-input>
+        </el-form-item>
+
+        <template v-if="activeTab === '0'">
+          <el-form-item
+            class="login-input"
+            prop="password"
+          >
+            <div class="login-icon">
+             <img src="@/assets/images/login/login-lock.svg" />
+            </div>
+            <el-input
+              v-model="loginForm.password"
+              placeholder="请输入密码"
+              type="password"
+            style="width: 270px;"
+              show-password
+              maxlength="30"
+              tabindex="2"
+              autocomplete="on"
+            >
+            </el-input>
+          </el-form-item>
+        </template>
+
+        <template v-else>
+          <el-form-item
+            class="login-input"
+            prop="code"
+          >
+            <div class="login-icon">
+              <img src="@/assets/images/login/login-lock.svg" />
+            </div>
+            <el-input
+              v-model="loginForm.password"
+              placeholder="请输入验证码"
+              type="text"
+              maxlength="6"
+              tabindex="2"
+              style="width: 270px;"
+              autocomplete="off"
+            >
+              <template #append>
+                <el-button 
+                  :disabled="isCodeSending" 
+                  @click="sendVerificationCode"
+                >
+                  {{ codeButtonText }}
+                </el-button>
+              </template>
+            </el-input>
+          </el-form-item>
+        </template>
+
+        <el-button class="login-button" type="primary" :loading="loading" @click="handleLogin">
+          登录
+        </el-button>
+      </el-form>
+    </div>
+  </el-dialog>
+  </div>
+</template>
+
+<script>
+import { defineComponent } from 'vue'
+
+export default defineComponent({
+  name: 'Login',
+})
+</script>
+<script setup>
+import { ref, reactive, computed } from 'vue'
+import { ElMessage } from 'element-plus'
+import { login, getUserInfo, sendCode } from '@/apis/user';
+import useUserInfo from "@/stores/modules/user";
+const showDialog = computed(() => props.dialogVisible)
+
+const props = defineProps({
+  dialogVisible: {
+    type: Boolean,
+    default: false
+  }
+})
+const emit = defineEmits(['update:dialogVisible', 'login-success'])
+
+const useUserInfoStore = useUserInfo();
+const activeTab = ref('0')
+const loading = ref(false)
+const isCodeSending = ref(false)
+const countdown = ref(60)
+const loginForm = reactive({
+  username: '',
+  password: '',
+  code: ''
+})
+
+const codeButtonText = computed(() => {
+  return isCodeSending.value ? `${countdown.value}s后重新获取` : '获取验证码'
+})
+
+const loginRules = reactive({
+  username: [
+    { required: true, message: '请输入用户名', trigger: 'blur' },
+    { min: 3, max: 30, message: '长度在 3 到 30 个字符', trigger: 'blur' }
+  ],
+  password: [
+    { required: true, message: '请输入密码', trigger: 'blur' },
+    { min: 6, max: 30, message: '长度在 6 到 30 个字符', trigger: 'blur' }
+  ],
+  code: [
+    { required: true, message: '请输入验证码', trigger: 'blur' },
+    { len: 6, message: '验证码长度为6位', trigger: 'blur' }
+  ]
+})
+
+
+const handleLogin = async () => {
+  const res = await login({
+    "site":1,
+    "username":loginForm.username,
+    "password":loginForm.password,
+    "type": activeTab.value,
+    "device":"aigc-photo"
+  })
+
+  const userRes = await getUserInfo({
+    "site":1,
+    "token":res.data.token
+  })
+
+  useUserInfoStore.updateUserInfo(userRes.data)
+}
+
+const sendVerificationCode = () => {
+  if (!loginForm.username) {
+    ElMessage.warning('请先输入手机号')
+    return
+  }
+  
+  sendCode({
+    phone: loginForm.username
+  }).then(() => {
+    isCodeSending.value = true
+    countdown.value = 60
+    
+  // 这里应该调用发送验证码API
+  // await sendCode(loginForm.username)
+  
+  const timer = setInterval(() => {
+    countdown.value--
+    if (countdown.value <= 0) {
+      clearInterval(timer)
+      isCodeSending.value = false
+    }
+  }, 1000)
+  })
+}
+
+</script>
+
+<style lang="scss" scoped>
+.login-box {
+  width: 100%;
+  min-height: 404px;
+  background-color: #fff;
+
+  .title__main {
+    font-size: 20px;
+    font-weight: bold;
+    color: #333;
+    line-height: 1.4;
+    text-align: left;
+  }
+  .title_sub {
+    font-size: 16px;
+    color: #666;
+    text-align: left;
+    line-height: 1.4;
+  }
+
+  .login-input {
+    :deep(.el-form-item__content) {
+      display: flex;
+      align-items: center;
+    }
+    :deep(.el-input__inner) {
+      height: 40px;
+      line-height: 40px;
+      border-top-left-radius: 0;
+      border-bottom-left-radius: 0;
+    }
+    :deep(.el-input-group__prepend) {
+      padding: 0;
+      background-color: initial;
+    }
+    :deep(.el-input__suffix-inner) {
+      line-height: 40px;
+      padding: 0 10px;
+    }
+
+    .login-icon {
+      flex-shrink: 0;
+      width: 40px;
+      height: 40px;
+      border: 1px solid #dcdfe6;
+      border-top-left-radius: 4px;
+      border-bottom-left-radius: 4px;
+      border-right: 0;
+
+      font-size: 22px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      img{
+        width: 22px;
+        height: 22px;
+      }
+    }
+  }
+
+  .login-button {
+    width: 100%;
+    margin-top: 10px;
+    background: linear-gradient( 135deg, #2FB0FF 0%, #B863FB 100%);
+    
+  }
+  :deep(.el-form-item) + .login-button {
+    margin-top: 20px;
+  }
+
+  .login-box__tabs {
+    :deep(.el-tabs__nav) {
+      float: initial;
+      display: flex;
+    }
+    :deep(.el-tabs__item) {
+      flex-grow: 1;
+      text-align: center;
+      height: 50px;
+      line-height: 50px;
+      padding: 0;
+      margin: 0;
+      &.is-active {
+        color: #333;
+      }
+    }
+
+    :deep(.el-tabs__active-bar) {
+      height: 3px;
+    }
+    :deep(.el-tabs__nav-wrap::after) {
+      height: 1px;
+      background-color: #ebeef5;
+    }
+    :deep(.el-tabs__header) {
+      margin-bottom: 0;
+    }
+    :deep(.el-tabs__active-bar) {
+      background-color: initial;
+      &::before {
+        content: '';
+        display: flex;
+        width: 50px;
+        height: 100%;
+        background-color: var(--el-color-primary);
+        margin: 0 auto;
+      }
+    }
+    :deep(.el-tabs__content) {
+      padding: 0;
+    }
+  }
+
+  :deep(.el-form) {
+    padding: 20px 0;
+  }
+
+  :deep(.el-form-item) {
+    margin-bottom: 0;
+    margin-top: 20px;
+  }
+
+  .company-list {
+    height: 220px;
+    overflow-y: auto;
+    .item {
+      background: #fff;
+      border-radius: 3px;
+      overflow: hidden;
+      margin-top: 10px;
+      cursor: pointer;
+      &:hover {
+        opacity: 0.9;
+      }
+      &:active {
+        opacity: 0.8;
+      }
+
+      .abbreviation {
+        background: #4f4f4f;
+        height: 42px;
+        line-height: 42px;
+        padding: 0 10px;
+        color: #fff;
+        font-size: 14px;
+      }
+      &.active {
+        position: relative;
+      }
+      &.active:before {
+        content: '';
+        display: block;
+        width: 44px;
+        height: 42px;
+        background: url('/static/images/active.png');
+        position: absolute;
+        right: 0px;
+      }
+    }
+  }
+}
+.login-box {
+    :deep(.login-dialog) {
+      border-radius: 10px;
+    }
+    :deep(.el-dialog__header) {
+      padding: 0;
+    }
+    :deep(.el-dialog__body) {
+      padding-top: 10px;
+    }
+    :deep(.el-input-group__append){
+      background-color: #fff;
+      color: #2FB0FF;
+    }
+}
+</style>

+ 122 - 0
frontend/src/components/upload/index.vue

@@ -0,0 +1,122 @@
+<template>
+  <el-upload class="upload-warp" action="#" list-type="picture-card" :http-request="upload">
+
+    <div class="flex col">
+      <el-icon>
+        <Plus />
+      </el-icon>
+      <div class="add-text">添加</div>
+    </div>
+    <template #file="{ file }">
+      <div>
+        <img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
+        <span class="el-upload-list__item-actions">
+          <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
+            <el-icon><zoom-in /></el-icon>
+          </span>
+          <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleDownload(file)">
+            <el-icon>
+              <Download />
+            </el-icon>
+          </span>
+          <span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
+            <el-icon>
+              <Delete />
+            </el-icon>
+          </span>
+        </span>
+      </div>
+    </template>
+  </el-upload>
+
+  <el-dialog v-model="dialogVisible">
+    <img w-full :src="dialogImageUrl" alt="Preview Image" />
+  </el-dialog>
+</template>
+<script lang="ts" setup>
+import { ref, defineEmits } from 'vue'
+import { Delete, Download, Plus, ZoomIn } from '@element-plus/icons-vue'
+import type { UploadFile } from 'element-plus'
+import { imagesUpload } from '@/utils/appconfig'
+import { ElMessage } from 'element-plus'
+import * as  userApi from '@/apis/user'
+
+// 定义自定义事件
+const emit = defineEmits(['input']);
+
+interface Props {
+  value: string
+  hasDel?: boolean
+  hasSize?: boolean
+  width?: number
+  height?: number
+  fileSize?: number
+  styleWidth?: string
+  accept?: string[]
+}
+const props = withDefaults(defineProps<Props>(), {
+  value: '',
+  hasDel: true,
+  hasSize: true,
+  width: 0,
+  height: 0,
+  fileSize: imagesUpload.fileSize,
+  styleWidth: '160',
+  accept: () => imagesUpload.accept
+})
+
+const dialogImageUrl = ref('')
+
+const dialogVisible = ref(false)
+
+const disabled = ref(false)
+
+const handleRemove = (file: UploadFile) => {
+  console.log(file)
+}
+
+const handlePictureCardPreview = (file: UploadFile) => {
+  dialogImageUrl.value = file.url!
+  dialogVisible.value = true
+}
+
+const handleDownload = (file: UploadFile) => {
+  console.log(file)
+}
+// 上传
+const upload = async (params) => {
+  const isLt2M = params.file.size / 1024 < props.fileSize
+  if (props.accept.indexOf(params.file.name.toLocaleLowerCase().split('.')[params.file.name.toLocaleLowerCase().split('.').length - 1]) < 0) {
+    ElMessage.error('请上传' + props.accept.join(',') + '格式文件')
+    return false
+  }
+  if (!isLt2M) {
+    ElMessage.error('上传文件大小不能超过 ' + props.fileSize + 'KB!')
+    return false
+  }
+  const res = await userApi.upload({
+    file: params.file
+  })
+  if (!props.hasSize) {
+    emit('input', res.data.url)
+    return;
+  }
+  emit('input', res.data.url)
+}
+</script>
+
+
+<style lang="scss" scoped>
+.upload-warp {
+  ::v-deep {
+    .el-upload--picture-card {
+      background-color: #fff;
+
+      .el-icon,
+      .add-text {
+        color: #2957FF;
+      }
+    }
+  }
+}
+</style>

+ 11 - 1
frontend/src/router/index.ts

@@ -31,7 +31,17 @@ const routes: RouteRecordRaw[] = [
         path: "/photography/shot",
         name: "PhotographyShot",
         component: () => import("@/views/Photography/shot.vue"),
-    }
+    },
+    {
+        path: "/photography/detail",
+        name: "PhotographyDetail",
+        component: () => import("@/views/Photography/detail.vue"),
+    },
+    {
+        path: "/photography/seniorDetail",
+        name: "PhotographySeniorDetail",
+        component: () => import("@/views/Photography/seniorDetail.vue"),
+    },
 ];
 
 

+ 1 - 0
frontend/src/style.css

@@ -63,6 +63,7 @@ button:focus-visible {
   margin: 0 auto;
   padding: 2rem;*/
   text-align: center;
+  width: 100%;
 }
 
 @media (prefers-color-scheme: light) {

+ 11 - 0
frontend/src/utils/appconfig.ts

@@ -0,0 +1,11 @@
+export const imagesUpload = {
+  accept: ['jpg', 'png', 'gif'],
+  fileSize: 2048 * 10,
+  maxLength: 5
+}
+
+export const imagesUploadLimit = {
+  accept: ['jpg', 'jpeg', 'png'],
+  fileSize: 2048 * 10,
+  fileLimit: '20MB'
+}

+ 127 - 80
frontend/src/utils/oss.ts

@@ -1,117 +1,164 @@
-
-import axios from "axios";
-
 /**
  * oss resize 参数
  * 详情见 https://help.aliyun.com/document_detail/44688.html?spm=a2c4g.11186623.6.751.7434663c1Ne07e
+ *
+ * oss watermark 参数
+ * 详情见 https://help.aliyun.com/document_detail/44957.html
+ *
+ * resize 参数被提到最外层(兼容 + 使用最频繁)
+ * 其他(如 watermark)使用对象
  */
 
-interface OSSResizeOptions {
-  scale?: number;
-  w?: number;
-  h?: number;
-  watermark?: string;
-  [key: string]: any;
-}
+import encBase64Url from 'crypto-js/enc-base64url'
+import encUtf8 from 'crypto-js/enc-utf8'
+
+export type IOSSResize =
+  | {
+      m: 'lfit' | 'mfit' | 'fixed' | 'fixed' | 'fill' | 'pad'
+      w?: number
+      h?: number
+      l?: number
+      s?: number
+      color?: string
+      limit?: 0 | 1
+    }
+  | {
+      p?: number
+    }
 
-interface OSSImageInfoOptions {
-  [key: string]: any;
+export type IOSSOptions = {
+  watermark?: IOSSWatermark | IOSSWatermark[]
+  quality?: IOSSQuality
+} & {
+  [key: string]: object
 }
 
-interface ImageInfo {
-  [key: string]: any;
+export type IOSSWatermark = {
+  t?: number
+  g?: 'nw' | 'north' | 'ne' | 'west' | 'center' | 'east' | 'sw' | 'south' | 'se'
+  x?: number
+  y?: number
+  voffset?: number
+
+  // 图片
+  image?: string
+  P?: number
+
+  // 文字
+  text?: string
+  type?: string
+  size?: number
+  shadow?: number
+  rotate?: number
+  fill?: 0 | 1
+  color?: string
+
+  // 混合
+  order?: 0 | 1
+  align?: 0 | 1 | 2
+  interval?: number
+} & (
+  | {
+      image: string
+      // text?: string
+    }
+  | {
+      // image?: string
+      text: string
+    }
+)
+export type IOSSQuality = {
+  q?: number
+  Q?: number
 }
 
+function createParams(key: string, options: Record<string, any>) {
+  const list = [key]
+  Object.keys(options).forEach((key) => {
+    const value = options[key]
+    if (value !== undefined && value !== null) {
+      list.push(`${key}_${value}`)
+    }
+  })
 
-
-function createParmas(key: string, value: any) {
-  return `${key}_${value}`
+  return list.join(',')
 }
 
-export function ossResize(url: string, options: OSSResizeOptions = {}): string {
+export function ossResize(
+  url?: string,
+  resize: IOSSResize = {},
+  options: IOSSOptions = {}
+) {
   if (!url) return ''
   try {
     const target = new URL(url)
     const parmas = target.searchParams
 
-    const list = ['image/resize']
-    //scale 追加scale 参数 默认为2
-    if(options.scale === undefined){
-      options.scale  = 2
-    }
-    if(options.w ){
-      options.w = options.w*options.scale
-    }
-    if(options.h){
-      options.h = options.h*options.scale
-    }
+    const { watermark, quality, ...otherOptions } = options
+    const list = ['image']
 
+    // 尺寸调整
+    if ('m' in resize || 'p' in resize) {
+      list.push(createParams('resize', resize))
+    }
 
-/*
     // 水印
-    if (options.watermark) {
-      list.push(
-        createParmas('watermark', {
-          ...options.watermark,
-        })
+    if (watermark) {
+      ;(Array.isArray(watermark) ? watermark : [watermark]).forEach(
+        (watermark) => {
+          list.push(createParams('watermark', watermark))
+        }
       )
     }
 
-*/
-
-
-    Object.keys(options).forEach((key) => {
-      if(!['watermark','scale'].includes(key)){
-        const value = options[key]
-        list.push(createParmas(key, value))
-      }
+    // 图片质量
+    list.push(
+      createParams(
+        'quality',
+        quality ?? {
+          Q: 80,
+        }
+      )
+    )
 
+    // 其他
+    Object.keys(otherOptions).forEach((key) => {
+      list.push(createParams(key, otherOptions[key]))
     })
 
-
-    const watermark = []
-    if (options.watermark) {
-      watermark.push('image/watermark')
-      watermark.push('image_' + options.watermark)
-    }
-
-    let str = list.join(',')
-    if(watermark.length  >0){
-      if(str){
-        str+= ','+watermark.join(',')
-      }else{
-        str+= watermark.join(',')
-      }
-    }
-    parmas.set('x-oss-process',str + '/quality,Q_80')
+    parmas.set('x-oss-process', list.join('/'))
 
     return target.toString()
   } catch (error) {
-    console.warn(url)
-    console.warn(error)
+    console.error(error)
     return url
   }
 }
 
+export function createWaterMarkText(text: string) {
+  return encBase64Url.stringify(encUtf8.parse(text))
+}
 
-
-export async function ossImageInfo(url: string, options: OSSImageInfoOptions = {}): Promise<ImageInfo | undefined> {
-  if (!url) return ''
-  try {
-    const target = new URL(url)
-    const parmas = target.searchParams
-    const list = ['image/info']
-    Object.keys(options).forEach((key) => {
-      const value = options[key]
-      list.push(createParmas(key, value))
-    })
-    parmas.set('x-oss-process', list.join(',') + '/quality,Q_80')
-    const ossUrl = target.toString()
-    const { data } = await axios.get(ossUrl)
-    return data
-  } catch (error) {
-    console.error(error)
-    return undefined
+export function splitText(text: string) {
+  if (!text) return ''
+  const reg = /^[\u0000-\u00ff]$/
+  const len = text.length
+  let result = ''
+  let count = 0
+  for (let i = 0; i < len; i += 1) {
+    const char = text.charAt(i)
+    count += reg.test(char) ? 1 : 2
+
+    if (count === 32 && i === len - 1) {
+      result += char
+      break
+    } else if (count > 29) {
+      result += '...'
+      break
+    } else {
+      result += char
+    }
   }
-}
 
+  return result
+}

+ 37 - 14
frontend/src/views/Home/index.vue

@@ -1,17 +1,17 @@
 <template>
 
-  <template v-if="!show">
-    <div>{{useUserInfoStore.userInfo.account_name}}</div>
-    <div>{{useUserInfoStore.userInfo.phone}}</div>
-  </template>
-  <template v-else>
-    <img :src="src" width="500px" height="500px"/>
-    <img :src="preview" width="500px" height="500px"/>
-  </template>
-
 
   <div class="mb-4">
 
+    <template v-if="!show">
+      <div>{{useUserInfoStore.userInfo.account_name}}</div>
+      <div>{{useUserInfoStore.userInfo.phone}}</div>
+    </template>
+    <template v-else>
+      <img :src="src" width="500px" height="500px"/>
+      <img :src="preview" width="500px" height="500px"/>
+  </template>
+
     <el-button @click="showVideo">实时预览</el-button>
     <el-button @click="hideVideo">关闭预览</el-button>
     <el-button @click="takePictures">拍照</el-button>
@@ -20,6 +20,13 @@
 
 
     <el-button @click="loginError">登录失败</el-button>
+    <el-button type="primary" @click="loginIn">登录成功</el-button>
+    <el-button type="success" @click="showLoginDialog">登录</el-button>
+    <el-button type="info" @click="checkVisible = true">软件检查</el-button>
+    <el-button type="warning">Warning</el-button>
+    <el-button type="danger">Danger</el-button>
+    <hardware-check v-model="checkVisible" @confirm="onCheckComplete"></hardware-check>
+    <Login v-model:dialogVisible="dialogVisible" @login-success="handleLoginSuccess"/>
     <el-button type="primary"  @click="loginIn">登录成功</el-button>
   </div>
 
@@ -29,20 +36,38 @@
       <el-input v-model="paramsValue" placeholder="参数值"></el-input>
       <el-button @click="setParams">保存</el-button>
   </div>
+
 </template>
 
 <script setup lang="ts">
 import { ref } from 'vue'
 import useUserInfo from "@/stores/modules/user";
+import { login, getUserInfo } from '@/apis/user';
+import Login from '@/components/login/index.vue';
+import HardwareCheck from '@/components/check/index.vue'
+
+
 const useUserInfoStore = useUserInfo();
-import { login,getUserInfo } from '@/apis/user'
+const dialogVisible = ref(false);
+const checkVisible = ref(false)
 import client from "@/stores/modules/client";
 import  icpList from '@/utils/ipc'
 const clientStore = client();
 console.log(icpList);
 
+function showLoginDialog() {
+  dialogVisible.value = true;
+}
 
-async  function loginIn() {
+function handleLoginSuccess() {
+  // 处理登录成功后的逻辑
+  console.log('登录成功');
+}
+function onCheckComplete(){
+
+}
+
+async function loginIn() {
   console.log('aaa')
   const res = await login({
     "site":1,
@@ -58,10 +83,8 @@ async  function loginIn() {
   })
 
   useUserInfoStore.updateUserInfo(userRes.data)
-
-
-
 }
+
 async function loginError() {
   const res = await login({
     "site":1,

+ 150 - 0
frontend/src/views/Photography/components/LoadingDialog.vue

@@ -0,0 +1,150 @@
+<template>
+  <el-dialog
+    v-model="visible"
+    :show-close="false"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    width="400px"
+    custom-class="loading-dialog-EL"
+    align-center
+    append-to-body
+  >
+    <div class="loading-content mar-top-10">
+      <div class="progress-container">
+        <div class="progress-bar">
+          <div 
+            class="progress-inner"
+            :style="{ width: `${progress}%` }"
+          ></div>
+        </div>
+        <span class="progress-text">{{ progress }}%</span>
+      </div>
+      
+      <div class="message">{{ message }}</div>
+      
+      <el-button
+        :disabled="showButton"
+        type="primary"
+        class="action-button   button--primary1  mar-top-20"
+        @click="handleButtonClick"
+      >
+        {{ buttonText }}
+      </el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, defineProps, defineEmits , watch } from 'vue'
+
+interface Props {
+  modelValue: boolean
+  progress?: number
+  message?: string
+  showButton?: boolean
+  buttonText?: string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  progress: 0,
+  message: '正在为您处理,请稍后',
+  showButton: false,
+  buttonText: '全部处理完毕,点击打开最终图片目录'
+})
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: boolean): void
+  (e: 'button-click'): void
+}>()
+
+const visible = ref(props.modelValue)
+
+// 监听visible变化
+watch(() => visible.value, (newVal) => {
+  emit('update:modelValue', newVal)
+})
+
+// 监听props.modelValue变化
+watch(() => props.modelValue, (newVal) => {
+  visible.value = newVal
+})
+
+const handleButtonClick = () => {
+  emit('button-click')
+}
+</script>
+<style>
+.loading-dialog-EL{
+  .el-dialog__header {
+    display: none;
+    padding: 0;
+  }
+ .el-dialog__body {
+    background-image: url(@/assets/images/Photography/loading-bg.png);  /* 添加背景图片 */
+    background-size: cover;  /* 确保图片覆盖整个区域 */
+    background-position: center;  /* 图片居中显示 */
+    background-repeat: no-repeat;  /* 防止图片重复 */
+    padding: 40px 20px;  /* 添加内边距 */
+  }
+
+}
+</style>
+
+<style lang="scss" scoped>
+.loading-dialog {
+  :deep(.el-dialog__header) {
+    display: none;
+    padding: 0;
+  }
+  
+  :deep(.el-dialog__body) {
+    padding:0;
+  }
+}
+
+.loading-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  text-align: center;
+}
+
+.progress-container {
+  width: 100%;
+  margin-bottom: 20px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+
+  .progress-bar {
+    flex: 1;
+    height: 6px;
+    background: #E4E7ED;
+    border-radius: 3px;
+    overflow: hidden;
+
+    .progress-inner {
+      height: 100%;
+      background: linear-gradient(90deg, #2FB0FF,#B863FB);
+      border-radius: 3px;
+      transition: width 0.3s ease;
+    }
+  }
+
+  .progress-text {
+    min-width: 45px;
+    color: #606266;
+    font-size: 14px;
+  }
+}
+
+.message {
+  color: #606266;
+  font-size: 14px;
+  margin-bottom: 20px;
+}
+
+.action-button {
+  padding: 10px  20px;
+}
+</style> 

+ 240 - 0
frontend/src/views/Photography/detail.vue

@@ -0,0 +1,240 @@
+<template>
+  <div class="detail-container">
+    <!-- 主图LOGO部分 -->
+    <div class="logo-section flex left top">
+      <div class="fw-600">主图LOGO:</div>
+      <upload v-model="queryParams.logoImage"></upload>
+    </div>
+    <el-divider />
+    <!-- 选择详情模板部分 -->
+    <div class="template-section ">
+      <div class="flex between">
+        <span>选择详情模版</span>
+        <div class="template-pagination">
+          <el-pagination background layout="prev, pager, next" v-model:current-page="queryParams.current"
+            v-model:page-size.sync="queryParams.size" :total="totalPage" @current-change="onCurrentChange"
+            @size-change="onSizeChange" />
+        </div>
+      </div>
+
+      <div class="template-list">
+        <div v-for="(template, index) in visibleTemplates" :key="index" class="template-item"
+          @click="queryParams.templateId = template.id">
+          <div class="select-warp" :class="queryParams.templateId == template.id ? 'active' : ''">
+            <el-icon color="#FFFFFF">
+              <Select />
+            </el-icon>
+          </div>
+          <div class="template-info">
+            <span class="mar-left-10">{{ template.templateId }}</span>
+            <div class="template-view" @click="viewTemplate(template)">查看</div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 详情资料准备部分 -->
+    <div class="data-prep-section">
+
+      <div class="flex-item left">详情资料准备 (2选1)</div>
+      <div class="flex-item left">
+        <el-radio-group v-model="queryParams.radio1" class="ml-4">
+          <el-radio label="1" size="large">EXCEL文件选择</el-radio>
+          <el-radio label="2" size="large">系统对接(和业务员联系)</el-radio>
+        </el-radio-group>
+      </div>
+      <div v-if="dataPrepOption === 'excel'" class="excel-upload">
+        <el-row :gutter="20">
+          <el-col :span="1"></el-col>
+          <el-col :span="4">商品基础资料EXCEL文件选择:</el-col>
+          <el-col :span="18">
+            <el-input type="textarea" v-model="excelFilePath" />
+          </el-col>
+          <el-col :span="1"></el-col>
+
+        </el-row>
+      </div>
+    </div>
+
+    <!-- 开始生成按钮 -->
+    <button class="generate-button button--primary1" @click="generate">开始生成</button>
+  </div>
+</template>
+
+<script lang="ts" setup>
+
+
+import { ref, computed, reactive } from 'vue';
+import { Select } from '@element-plus/icons-vue'
+
+
+import upload from '@/components/upload'
+
+// 模拟数据
+const templates = [
+  { id: 1101, templateId: '某某模版编号1', preview: '...' },
+  { id: 1102, templateId: '某某模版编号2', preview: '...' },
+  { id: 1103, templateId: '某某模版编号3', preview: '...' },
+  { id: 1104, templateId: '某某模版编号4', preview: '...' },
+  { id: 1105, templateId: '某某模版编号5', preview: '...' },
+  { id: 1106, templateId: '某某模版编号6', preview: '...' },
+  { id: 1107, templateId: '某某模版编号7', preview: '...' },
+  { id: 1108, templateId: '某某模版编号8', preview: '...' },
+  { id: 1109, templateId: '某某模版编号9', preview: '...' },
+  { id: 1110, templateId: '某某模版编号10', preview: '...' },
+  { id: 1111, templateId: '某某模版编号11', preview: '...' },
+  { id: 1112, templateId: '某某模版编号12', preview: '...' },
+  // 更多模板...
+];
+const itemsPerPage = 4; // 每页显示的模板数量
+// 状态变量
+const currentPage = ref(1);
+const selectedTemplateIndex = ref(0);
+const dataPrepOption = ref('excel');
+const excelFilePath = ref('D:\\MyDocuments\\PythonCode\\MyPython\\red_dragonfly\\deal_pics\\auto_capture_V2\\auto_photo');
+const totalPage = ref(3);
+
+const queryParams = reactive({
+  logoImage: '',
+  size: 1,
+  current: 1,
+  templateId: 1101,
+  radio1: 1
+})
+
+// 计算属性,获取当前页可见的模板
+const visibleTemplates = computed(() => {
+  const startIndex = (queryParams.current - 1) * itemsPerPage;
+  const data = templates.slice(startIndex, startIndex + itemsPerPage);
+  return data
+});
+// 查看模板详情
+const viewTemplate = (template) => {
+  console.log('查看模板详情', template);
+};
+const onCurrentChange = (page) => {
+  queryParams.current = page;
+};
+const onSizeChange = (data) => {
+
+};
+
+// 开始生成操作
+const generate = () => {
+  console.log('开始生成');
+  // 这里添加实际生成主图和详情的逻辑
+};
+</script>
+
+<style lang="scss" scoped>
+.detail-container {
+  padding: 20px 0 20px 20px;
+  max-width: 1200px;
+  background-color: #EAECED;
+  width: 100%;
+
+}
+
+.logo-section,
+.template-section,
+.data-prep-section {
+  margin-bottom: 20px;
+}
+
+.logo-upload {
+  border: 1px dashed #ccc;
+  border-radius: 5px;
+  padding: 50px 0;
+  text-align: center;
+  cursor: pointer;
+}
+
+.template-pagination button {
+  margin-right: 5px;
+}
+
+.template-pagination span {
+  display: inline-block;
+  width: 20px;
+  height: 20px;
+  line-height: 20px;
+  text-align: center;
+  border: 1px solid #ccc;
+  border-radius: 5px;
+  margin-right: 5px;
+  cursor: pointer;
+}
+
+
+.template-list {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  margin-top: 10px;
+
+
+  .template-item {
+    width: calc(25% - 34px);
+    border: 1px solid #ccc;
+    border-radius: 10px;
+    padding: 10px;
+    height: 320px;
+    cursor: pointer;
+    background: #f0f0f0;
+    position: relative;
+    overflow: hidden;
+
+    .template-info {
+      position: absolute;
+      bottom: 0;
+      left: 0;
+      background: rgba($color: #000000, $alpha: .3);
+      width: 100%;
+      height: 36px;
+      line-height: 36px;
+      color: #eee;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+
+      .template-view {
+        background: #DFE2E3;
+        color: #3366FF;
+        height: 30px;
+        line-height: 30px;
+        padding: 0 10px;
+        border-radius: 4px;
+        margin-right: 10px;
+        font-size: 14px;
+      }
+    }
+  }
+}
+
+
+
+.excel-upload {
+  width: 100%;
+  background: #F7F7F7;
+  padding: 20px 0;
+}
+
+.generate-button {
+  padding: 10px 20px;
+  color: white;
+  border: none;
+  border-radius: 5px;
+  cursor: pointer;
+}
+
+.select-warp {
+  width: 18px;
+  height: 18px;
+  border-radius: 4px;
+  background-color: #fff;
+
+  &.active {
+    background-color: #1677FF;
+  }
+}
+</style>

+ 281 - 0
frontend/src/views/Photography/seniorDetail.vue

@@ -0,0 +1,281 @@
+<template>
+  <div class="image-config">
+    <div class="config-card">
+      <!-- 图片报图与货号图生成 -->
+      <div class="section">
+        <div class="section-title">
+          <img src="@/assets/images/Photography/zhuangshi.png" style="width: 32px; height: 32px;" />
+          图片报图与货号图生成
+        </div>
+        <div class="section-content">
+
+
+          <div class="instruction-out flex top left">
+            <img style="fill: #000" src="@/assets/images/xinxi.svg" />
+            <ol class="instruction-list">
+              <li>请在下方确认图片拍摄过程中的顺序,确保所有拍摄的图片的顺序一致。</li>
+              <li>使用中英文语号分隔。</li>
+              <li>图片的名称不能随意修改,否则无法正常生成详情页。</li>
+              <li>现有图片名称有:俯视、侧视、后视、鞋底、内里</li>
+            </ol>
+            <el-icon class="close-icon">
+              <Close />
+            </el-icon>
+          </div>
+
+          <!-- 货号文件夹 -->
+          <div class="form-item">
+            <div class="label">货号文件夹:</div>
+            <div class="folder-warp">
+              <div class="folder-input">
+                <el-input style="width: 60%;" v-model="folderPath" type="textarea" placeholder="请选择货号文件夹" />
+                <el-button class="check-button" type="primary">
+                  <img src="@/assets/images/Photography/wenjian.png" style="width: 14px; " />
+                  选择目标文件夹</el-button>
+              </div>
+              <div class="hint">
+                <el-icon>
+                  <Warning />
+                </el-icon> <text>选择货号的上级文件夹</text>
+              </div>
+            </div>
+          </div>
+
+          <!-- 报图模式 -->
+          <div class="form-item">
+            <div class="label">抠图模式:</div>
+            <el-radio-group v-model="reportMode">
+              <el-radio label="normal">普通模式</el-radio>
+              <el-radio label="optimized">精细化报图</el-radio>
+            </el-radio-group>
+          </div>
+        </div>
+      </div>
+      <el-divider />
+
+      <!-- 详情高级配置 -->
+      <div class="section">
+        <div class="section-title">
+          <img src="@/assets/images/Photography/zhuangshi.png" style="width: 32px; height: 32px;" />
+          详情高级配置
+        </div>
+        <div class="section-content">
+          <!-- 图片顺序 -->
+          <div class="form-item">
+            <div class="label">图片顺序:</div>
+            <el-input v-model="imageOrder" placeholder="请输入图片顺序" class="specific-page-input">
+              <template #append>
+                <el-button class="explain-btn" link type="primary">说明</el-button>
+              </template>
+            </el-input>
+          </div>
+
+          <!-- 同款检验 -->
+          <div class="form-item">
+            <div class="label">同款检验:</div>
+            <el-checkbox v-model="checkSimilar">同款下货号必须齐全</el-checkbox>
+          </div>
+
+          <!-- 可指定页面独修改 -->
+          <div class="form-item">
+            <div class="label">可指定页面独修改:</div>
+            <el-input v-model="specificPage" placeholder="请输入入需要单独修改的页面,示例:4:1 (需修改模版的编号:第一张)"
+              class="specific-page-input">
+              <template #append>
+                <el-button class="explain-btn" link type="primary">说明</el-button>
+              </template>
+            </el-input>
+          </div>
+        </div>
+      </div>
+
+      <!-- 底部按钮 -->
+      <div class="footer">
+        <el-button class="button--primary1  footer-button" type="primary">保存配置</el-button>
+      
+        <el-button class="button--primary1 footer-button" type="primary" @click="startProcess">开始处理</el-button>
+      </div>
+  
+    </div>
+
+
+    <loading-dialog v-model="dialogVisible" :progress="progress" :message="message" :show-button="showButton"
+      @button-click="handleComplete" />
+  </div>
+
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { Close, Warning } from '@element-plus/icons-vue'
+import LoadingDialog from '@/views/Photography/components/LoadingDialog.vue'
+
+const folderPath = ref('')
+const reportMode = ref('normal')
+const checkSimilar = ref(false)
+const specificPage = ref('')
+const imageOrder = ref('俯视、侧视、后跟、鞋底、内里、组合、组合2、组合3')
+
+
+const dialogVisible = ref(false)
+const progress = ref(0)
+const message = ref('正在为您处理,请稍后')
+const showButton = ref(false)
+const startProcess = () => {
+  dialogVisible.value = true
+  progress.value = 0
+  showButton.value = false
+
+  // 模拟进度更新
+  const interval = setInterval(() => {
+    if (progress.value < 100) {
+      progress.value += 10
+    } else {
+      clearInterval(interval)
+      message.value = '全部处理完毕'
+      showButton.value = true
+    }
+  }, 500)
+}
+
+const handleComplete = () => {
+  dialogVisible.value = false
+  // 这里可以添加打开目录的逻辑
+  console.log('打开目录')
+}
+
+</script>
+
+<style lang="scss" scoped>
+.image-config {
+  padding: 20px;
+
+  .config-card {
+    background: #EAECED;
+    padding: 20px;
+
+    .card-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+
+      .header-icons {
+        display: flex;
+        gap: 8px;
+      }
+    }
+  }
+
+  .section {
+    margin-bottom: 24px;
+
+    .section-title {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      font-weight: bold;
+      margin-bottom: 16px;
+      color: #474747;
+    }
+
+    .section-content {
+      padding-left: 16px;
+    }
+  }
+
+  .instruction-out {
+    background: #EAF3FF;
+    border-radius: 4px;
+    border: 1px solid #CBE1FF;
+    padding: 10px 20px;
+    width: 80%;
+    position: relative;
+
+    .instruction-list {
+      margin: 0px 0 0 10px;
+      padding-left: 20px;
+
+      li {
+        margin-bottom: 4px;
+        text-align: left;
+        font-size: 14px;
+      }
+    }
+
+    .close-icon {
+      position: absolute;
+      top: 19px;
+      right: 19px;
+    }
+  }
+
+
+
+  .form-item {
+    margin: 16px 0;
+    display: flex;
+
+
+    .label {
+      min-width: 120px;
+      margin-right: 12px;
+    }
+
+    .folder-warp {
+      width: 100%;
+      display: flex;
+      flex-direction: column;
+
+      .folder-input {
+        flex: 1;
+        display: flex;
+        align-items: center;
+
+        .check-button {
+          background: #DFE2E3;
+          border-radius: 6px;
+          padding: 6px 12px;
+          color: #2957FF;
+          margin-left: 20px;
+        }
+
+      }
+    }
+
+    .hint {
+      text-align: left;
+      color: #999;
+      margin-top: 6px;
+      font-size: 14px;
+      color: #FF4C00;
+      font-style: normal;
+    }
+
+    .specific-page-input {
+      flex: 1;
+    }
+  }
+
+  .image-order {
+    flex: 1;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+  }
+
+  .footer {
+    display: flex;
+    justify-content: center;
+    margin-top: 24px;
+    .footer-button{
+      padding: 10px  20px;
+    }
+  }
+}
+
+.explain-btn {
+  padding-left: 20px;
+  padding-right: 20px;
+  color: #2957FF !important;
+}
+</style>

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است