Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front

This commit is contained in:
X1627315083
2025-10-31 23:47:12 +08:00
14 changed files with 229 additions and 43 deletions

26
package-lock.json generated
View File

@@ -10,6 +10,7 @@
"hasInstallScript": true,
"dependencies": {
"axios": "^1.3.6",
"crypto-js": "^4.2.0",
"gsap": "^3.13.0",
"markdown-it": "^14.1.0",
"normalize.css": "^8.0.1",
@@ -22,6 +23,7 @@
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.2.0",
"@types/crypto-js": "^4.2.2",
"@types/node": "^18.16.0",
"@vant/auto-import-resolver": "^1.3.0",
"@vitejs/plugin-vue": "^4.0.0",
@@ -654,6 +656,13 @@
"node": ">=10.13.0"
}
},
"node_modules/@types/crypto-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.1.tgz",
@@ -2104,6 +2113,12 @@
"node": ">= 8"
}
},
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/css-select": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/css-select/-/css-select-4.3.0.tgz",
@@ -8935,6 +8950,12 @@
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
"dev": true
},
"@types/crypto-js": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz",
"integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==",
"dev": true
},
"@types/estree": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.1.tgz",
@@ -10039,6 +10060,11 @@
"which": "^2.0.1"
}
},
"crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"css-select": {
"version": "4.3.0",
"resolved": "https://registry.npmmirror.com/css-select/-/css-select-4.3.0.tgz",

View File

@@ -14,6 +14,7 @@
},
"dependencies": {
"axios": "^1.3.6",
"crypto-js": "^4.2.0",
"gsap": "^3.13.0",
"markdown-it": "^14.1.0",
"normalize.css": "^8.0.1",
@@ -26,6 +27,7 @@
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.2.0",
"@types/crypto-js": "^4.2.2",
"@types/node": "^18.16.0",
"@vant/auto-import-resolver": "^1.3.0",
"@vitejs/plugin-vue": "^4.0.0",

View File

@@ -1,13 +1,38 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useGenerateStore } from '@/stores/modules/generate'
const VerifyIDs = (num: number) => {
const ids = [
!!useGenerateStore().customerId,
!!useGenerateStore().visitRecordId,
!!useGenerateStore().styleId,
// !!useGenerateStore().modelPhotoId,
true,
!!useGenerateStore().originalTryOnId,
];
return ids.splice(0, num).every(id => id) ? true : "/stylist/customer";
}
/**
* 路由缓存机制:
* 1. 设置路由的meta属性为{ cache: true },表示需要缓存
* 2. App.vue中使用RouteCache组件通过路由的name来进行匹配
* 3. 路由的name默认是文件名,如果文件名与name不一致,通过defineOptions({ name: 'componentName' })来设置
*
* 自定义验证规则:
* meta{ verify: => boolean || string }
* 1. boolean true 跳转 false 不跳转
* 2. string 跳转的路由path
*/
/** 验证id
* @param num 验证id的数量1-5
* 1. 顾客id
* 2. 进店记录id
* 3. 服装id
* 4. 模特照片id
* 5. 原始试穿id-优先AI魔改
* @returns boolean
*/
const router = createRouter({
history: createWebHistory('/'),
// history: createWebHistory(import.meta.env.VITE_APP_URL),
@@ -45,22 +70,25 @@ const router = createRouter({
{
path: 'index',
name: 'index',
component: () => import('@/views/stylist/index.vue')
component: () => import('@/views/stylist/index.vue'),
meta: { verify: ()=> VerifyIDs(2) }
},
{
path: 'sex',
name: 'sex',
component: () => import('@/views/stylist/sex.vue')
component: () => import('@/views/stylist/sex.vue'),
meta: { verify: ()=> VerifyIDs(2) }
},
{
path: 'dressfor',
name: 'dressfor',
component: () => import('@/views/stylist/dressfor.vue')
component: () => import('@/views/stylist/dressfor.vue'),
meta: { verify: ()=> VerifyIDs(2) }
},
{
path: 'customer',
name: 'customer',
component: () => import('@/views/stylist/customer.vue')
component: () => import('@/views/stylist/customer.vue'),
}
]
},
@@ -68,7 +96,7 @@ const router = createRouter({
path: '/asistant',
name: 'asistant',
component: () => import('../views/asistant/index.vue'),
meta: { cache: true }
meta: { cache: true, verify: ()=> VerifyIDs(2) }
},
{
path: '/workshop',
@@ -83,57 +111,67 @@ const router = createRouter({
path: '/workshop/selectStyle',
name: 'SelectStyle',
component: () => import('../views/Workshop/selectStyle.vue'),
meta: { verify: ()=> VerifyIDs(2) }
},
{
path: '/workshop/selectModel',
name: 'SelectModel',
component: () => import('../views/Workshop/selectModel.vue')
component: () => import('../views/Workshop/selectModel.vue'),
meta: { verify: ()=> VerifyIDs(3) }
},
{
path: '/workshop/product',
name: 'Product',
component: () => import('../views/Workshop/product.vue')
component: () => import('../views/Workshop/product.vue'),
meta: { verify: ()=> VerifyIDs(4) }
},
{
// 上传照片1
path: '/workshop/uploadFace',
name: 'uploadFace',
component: () => import('../views/Workshop/uploadFace1.vue')
component: () => import('../views/Workshop/uploadFace1.vue'),
meta: { verify: ()=> VerifyIDs(5) }
},
{
// 上传照片2
path: '/workshop/uploadFace2',
name: 'uploadFace2',
component: () => import('../views/Workshop/uploadFace2.vue')
component: () => import('../views/Workshop/uploadFace2.vue'),
meta: { verify: ()=> VerifyIDs(5) }
},
{
// 自定义创作
path: '/workshop/customize',
name: 'customize',
component: () => import('../views/Workshop/customize.vue')
component: () => import('../views/Workshop/customize.vue'),
meta: { verify: ()=> VerifyIDs(5) }
},
{
// library
path: '/workshop/library',
name: 'library',
component: () => import('../views/Workshop/library.vue')
component: () => import('../views/Workshop/library.vue'),
meta: { verify: ()=> VerifyIDs(2) }
},
{
path: '/workshop/profile',
name: 'profile',
component: () => import('../views/Workshop/profile.vue')
component: () => import('../views/Workshop/profile.vue'),
meta: { verify: ()=> VerifyIDs(1) }
},
{
// creation
path: '/workshop/creation',
name: 'creation',
component: () => import('../views/Workshop/creation/index.vue')
component: () => import('../views/Workshop/creation/index.vue'),
meta: { verify: ()=> VerifyIDs(2) }
},
{
// 完成创建
path: '/workshop/end',
name: 'end',
component: () => import('../views/Workshop/end.vue')
component: () => import('../views/Workshop/end.vue'),
meta: { verify: ()=> VerifyIDs(2) }
}
]
}

View File

@@ -5,10 +5,22 @@ const whiteList = ['/login']
console.log(whiteList)
router.beforeEach((to, from, next) => {
next()
requestAnimationFrame(() => {
const verify = to.meta?.verify;
if (typeof verify === 'function') {
const res = verify()
if (res === false) {
return next(false)
} else if (typeof res === 'string') {
console.log(res)
return next({ path: res })
}
}
next()
})
})
router.afterEach(() => {
// finish progress bar
// NProgress.done()
// finish progress bar
// NProgress.done()
})

View File

@@ -1,7 +1,6 @@
// 每一个存储的模块命名规则use开头store结尾
import { defineStore } from 'pinia'
import MyEvent from '@/utils/myEvent'
import { uploadCustomerPhoto } from '@/api/workshop'
MyEvent.add('clear-generate-state', () => useGenerateStore().clearGenerateData())
export const useGenerateStore = defineStore({

View File

@@ -1,3 +1,5 @@
import CryptoJS from 'crypto-js'
function getUniversalZoomLevel() {
// 现代浏览器方案
if (window.visualViewport) {
@@ -146,3 +148,12 @@ export async function DownloadImages(list: Array<{ url: string, name?: string }>
}
typeof onSuccess === "function" && onSuccess(successCount, errCount);
}
/**
* MD5加密密码
* @param password 原始密码
* @returns MD5加密后的密码
*/
export function encryptPassword(password: string): string {
return CryptoJS.MD5(password).toString()
}

View File

@@ -281,6 +281,7 @@
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
}
}
> .icon-selected {

View File

@@ -220,6 +220,7 @@
border: 0.24rem solid #000;
display: flex;
align-items: center;
background-color: #fff;
> .icon {
margin: 0 1.8rem;
}
@@ -246,6 +247,7 @@
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
}
}
}

View File

@@ -179,6 +179,7 @@ const { isLoading } = toRefs(data);
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
> .operation{
position: absolute;

View File

@@ -196,7 +196,9 @@ const startRecording = () => {
if (!speechRecognition) {
// 检查浏览器支持
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
alert('您的浏览器不支持语音识别功能')
// alert('您的浏览器不支持语音识别功能')
showToast('Your browser does not support speech recognition, please try again with another browser')
isRecording.value = false
return
}
@@ -244,7 +246,7 @@ const startRecording = () => {
// 显示临时结果(可选)
if (interimTranscript) {
console.log('临时识别结果:', interimTranscript)
console.log('语音转文字识别中:', interimTranscript)
}
}
@@ -261,6 +263,7 @@ const startRecording = () => {
console.error('语音识别错误:', event.error)
isRecording.value = false
// alert('语音识别失败,请重试')
showToast('Speech recognition failed, please try again')
showToast(event.error)
}

View File

@@ -50,6 +50,7 @@ import { useUserInfoStore } from '@/stores'
import { showToast } from 'vant'
import { google } from '@/assets/base64'
import { fetchRegisterOrLogin } from '@/api/login'
import { encryptPassword } from '@/utils/tools'
const router = useRouter()
const userInfoStore = useUserInfoStore()
@@ -109,7 +110,8 @@ const handleLogin = async () => {
isLoading.value = true
fetchRegisterOrLogin({ ...formData, operationType: 'LOGIN' }).then((response) => {
const encryptedPassword = encryptPassword(formData.password)
fetchRegisterOrLogin({ ...formData, password: encryptedPassword, operationType: 'LOGIN' }).then((response) => {
console.log('登录成功', response)
userInfoStore.setToken(response.token)
userInfoStore.setUserInfo(response.user)

View File

@@ -22,7 +22,6 @@
v-else-if="step === 'verify'"
:ct="emailCode"
@nextStep="handleCheckVerifyCode"
@resend="handleSendVerifyCode"
/>
<Password v-else-if="step === 'password'" @sucess="handleSuccess" />
</div>
@@ -42,7 +41,8 @@ import Mail from './components/Mail.vue'
import Verify from './components/Verify.vue'
import Password from './components/Password.vue'
import { showToast } from 'vant'
import { precheckEmail, resetPassword } from '@/api/login'
import { resetPassword } from '@/api/login'
import { encryptPassword } from '@/utils/tools'
const router = useRouter()
const step = ref<'mail' | 'verify' | 'password'>('mail')
@@ -79,10 +79,8 @@ const handleSendVerifyCode = (data: any) => {
if (data?.email) {
fromData.value.email = data?.email
}
precheckEmail({ email: fromData.value.email }).then(() => {
showToast('the verification code has been sent to your email')
handleStep('verify')
})
// 只切换步骤,验证码的发送由 Verify 组件负责
handleStep('verify')
}
const handleCheckVerifyCode = (data: any) => {
@@ -92,7 +90,7 @@ const handleCheckVerifyCode = (data: any) => {
}
const handleSuccess = (data: any) => {
fromData.value.password = data.password
fromData.value.password = encryptPassword(data.password)
resetPassword(fromData.value).then((res) => {
// console.log('res', res)
showToast('the password has been reset')

View File

@@ -56,6 +56,7 @@ import { useRouter } from 'vue-router'
import { showToast } from 'vant'
import { google } from '@/assets/base64'
import { fetchRegisterOrLogin } from '@/api/login'
import { encryptPassword } from '@/utils/tools'
const router = useRouter()
@@ -121,7 +122,8 @@ const handleConfirm = async () => {
isLoading.value = true
fetchRegisterOrLogin({ ...formData, operationType: 'REGISTER' })
const encryptedPassword = encryptPassword(formData.password)
fetchRegisterOrLogin({ ...formData, password: encryptedPassword, operationType: 'REGISTER' })
.then((res) => {
console.log('res', res)
showToast('register success')

View File

@@ -31,8 +31,10 @@
<div class="btn" @click="handleConfirmCaptcha">Confirm</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { showToast } from 'vant'
import { getLocal, removeLocal, setLocal } from '@/utils/local'
import { precheckEmail } from '@/api/login'
// Props
const props = defineProps({
@@ -48,7 +50,7 @@ const props = defineProps({
const agreePolicy = ref(false)
// Emits
const emit = defineEmits(['nextStep','resend'])
const emit = defineEmits(['nextStep'])
// Reactive data
const loading = ref(false)
@@ -66,24 +68,88 @@ const cIndex = computed(() => {
// const lastCode = computed(() => getCtData.value[ctSize.value - 1])
const countdown = ref(60)
const handleCountdown = () => {
const timer = setInterval(() => {
const countdownTimer = ref<number | null>(null)
const COUNTDOWN_KEY = 'verify_code_countdown'
const COUNTDOWN_EMAIL_KEY = 'verify_code_email'
const COUNTDOWN_DURATION = 60 // 60秒倒计时
// 清除倒计时数据
const clearCountdownData = () => {
removeLocal(COUNTDOWN_KEY)
removeLocal(COUNTDOWN_EMAIL_KEY)
}
// 保存倒计时开始时间
const saveCountdownStart = () => {
const timestamp = Date.now()
setLocal(String(timestamp), COUNTDOWN_KEY)
setLocal(props.email, COUNTDOWN_EMAIL_KEY)
}
// 从 localStorage 恢复倒计时
const restoreCountdown = () => {
const savedTimestamp = getLocal(COUNTDOWN_KEY)
const savedEmail = getLocal(COUNTDOWN_EMAIL_KEY)
// 检查是否有保存的倒计时,且是同一个邮箱
if (savedTimestamp && savedEmail === props.email) {
const elapsed = Math.floor((Date.now() - Number(savedTimestamp)) / 1000)
const remaining = COUNTDOWN_DURATION - elapsed
if (remaining > 0) {
// 如果还有剩余时间,恢复倒计时
countdown.value = remaining
startCountdown()
return true
} else {
// 如果已经过期,清除数据
clearCountdownData()
}
}
return false
}
// 开始倒计时
const startCountdown = () => {
// 清除之前的定时器
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
}
countdownTimer.value = setInterval(() => {
countdown.value--
if (countdown.value <= 0) {
clearInterval(timer)
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
countdownTimer.value = null
}
clearCountdownData()
}
}, 1000)
}, 1000) as unknown as number
}
const handleSendVerifyCode = () => {
handleCountdown()
// 发送验证码
const handleSendVerifyCode = async () => {
if (loading.value) return
loading.value = true
try {
await precheckEmail({ email: props.email })
showToast('the verification code has been sent to your email')
countdown.value = COUNTDOWN_DURATION
saveCountdownStart()
startCountdown()
} catch (error) {
console.error('发送验证码失败:', error)
showToast('Failed to send verification code, please try again')
} finally {
loading.value = false
}
}
const handleResend = () => {
const handleResend = async () => {
if (countdown.value > 0) return
countdown.value = 60
handleSendVerifyCode()
emit('resend')
await handleSendVerifyCode()
}
const handleConfirmCaptcha = () => {
@@ -97,6 +163,12 @@ const handleConfirmCaptcha = () => {
showToast('please enter the correct verification code.')
return
}
// 验证成功后清除倒计时数据
clearCountdownData()
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
countdownTimer.value = null
}
emit('nextStep', { code: password })
}
@@ -208,7 +280,24 @@ watch(cIndex, () => {
// Lifecycle
onMounted(() => {
resetCaret()
handleSendVerifyCode()
// 尝试恢复倒计时,如果无法恢复则发送验证码
const restored = restoreCountdown()
if (!restored) {
// 如果没有恢复倒计时,说明倒计时已结束或不存在,需要发送验证码
handleSendVerifyCode()
}
// 如果恢复了倒计时,说明倒计时还未结束,不发送验证码
})
onUnmounted(() => {
// 组件卸载时清理定时器
if (countdownTimer.value) {
clearInterval(countdownTimer.value)
countdownTimer.value = null
}
if (timeout.value) {
clearTimeout(timeout.value)
}
})
// Expose methods for parent component