修改登录页

This commit is contained in:
2026-04-29 14:37:44 +08:00
parent 554a91f78e
commit 71eae187f2
12 changed files with 1118 additions and 132 deletions
+2 -2
View File
@@ -1,8 +1,8 @@
# 页面标题
VITE_APP_TITLE = 若依管理系统
VITE_APP_TITLE = 报价管理系统
# 开发环境配置
VITE_APP_ENV = 'development'
# 若依管理系统/开发环境
# 报价管理系统/开发环境
VITE_APP_BASE_API = '/dev-api'
+2 -2
View File
@@ -1,10 +1,10 @@
# 页面标题
VITE_APP_TITLE = 若依管理系统
VITE_APP_TITLE = 报价管理系统
# 生产环境配置
VITE_APP_ENV = 'production'
# 若依管理系统/生产环境
# 报价管理系统/生产环境
VITE_APP_BASE_API = '/prod-api'
# 是否在打包时开启压缩,支持 gzip 和 brotli
+2 -2
View File
@@ -1,10 +1,10 @@
# 页面标题
VITE_APP_TITLE = 若依管理系统
VITE_APP_TITLE = 报价管理系统
# 生产环境配置
VITE_APP_ENV = 'staging'
# 若依管理系统/生产环境
# 报价管理系统/生产环境
VITE_APP_BASE_API = '/stage-api'
# 是否在打包时开启压缩,支持 gzip 和 brotli
+1
View File
@@ -25,6 +25,7 @@
"element-plus": "2.13.1",
"file-saver": "2.0.5",
"fuse.js": "7.1.0",
"gsap": "^3.15.0",
"js-beautify": "1.15.4",
"js-cookie": "3.0.5",
"jsencrypt": "3.3.2",
@@ -0,0 +1,34 @@
<template>
<el-checkbox-group v-model="selectedItem" @change="handleChange">
<el-checkbox v-for="item in props.data" :key="item.value" :label="item.label" border size="large">
{{ item.label }}
</el-checkbox>
</el-checkbox-group>
</template>
<script setup>
import { ref, defineEmits, defineProps } from "vue";
const props = defineProps({
data: {
type: Array,
required: true,
},
count: {
type: Number,
default: 1, // 默认只能选择一个
},
});
const selectedItem = ref([]);
const emit = defineEmits(["update:modelValue", "change"]);
const handleChange = (val) => {
// 实现单选逻辑:如果选中多个,只保留最后一个
if (val.length > props.count) {
selectedItem.value = val.slice(-props.count);
}
emit("update:modelValue", selectedItem.value);
emit("change", selectedItem.value);
};
</script>
<style scoped></style>
@@ -0,0 +1,97 @@
<template>
<div
ref="eyeRef"
class="eyeball"
:style="{
width: `${props.size}px`,
height: props.isBlinking ? '2px' : `${props.size}px`,
backgroundColor: props.eyeColor,
}"
>
<template v-if="!props.isBlinking">
<div
class="eyeball-pupil"
:style="{
width: `${props.pupilSize}px`,
height: `${props.pupilSize}px`,
backgroundColor: props.pupilColor,
transform: `translate(${pupilOffsetX.value}px, ${pupilOffsetY.value}px)`,
}"
></div>
</template>
</div>
</template>
<script setup>
import { ref, watch, computed, onMounted, onUnmounted } from "vue";
const props = defineProps({
size: { type: Number, default: 48 },
pupilSize: { type: Number, default: 16 },
maxDistance: { type: Number, default: 10 },
eyeColor: { type: String, default: "white" },
pupilColor: { type: String, default: "black" },
isBlinking: { type: Boolean, default: false },
forceLookX: { type: Number, default: undefined },
forceLookY: { type: Number, default: undefined },
});
const eyeRef = ref();
const pupilOffsetX = ref(0);
const pupilOffsetY = ref(0);
const mouseX = ref(0);
const mouseY = ref(0);
const updateMousePosition = (e) => {
mouseX.value = e.clientX;
mouseY.value = e.clientY;
updateOffset();
};
const updateOffset = () => {
if (!eyeRef.value) return;
if (props.forceLookX !== undefined && props.forceLookY !== undefined) {
pupilOffsetX.value = props.forceLookX;
pupilOffsetY.value = props.forceLookY;
return;
}
const rect = eyeRef.value.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const deltaX = mouseX.value - centerX;
const deltaY = mouseY.value - centerY;
const distance = Math.min(Math.sqrt(deltaX ** 2 + deltaY ** 2), props.maxDistance);
const angle = Math.atan2(deltaY, deltaX);
pupilOffsetX.value = Math.cos(angle) * distance;
pupilOffsetY.value = Math.sin(angle) * distance;
};
onMounted(() => {
window.addEventListener("mousemove", updateMousePosition);
updateOffset();
});
onUnmounted(() => {
window.removeEventListener("mousemove", updateMousePosition);
});
watch(
() => [props.forceLookX, props.forceLookY],
() => updateOffset(),
);
</script>
<style scoped>
/* 眼球组件 */
.eyeball {
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
transition-property: all;
transition-duration: 150ms;
transition-timing-function: ease;
overflow: hidden;
}
.eyeball-pupil {
border-radius: 9999px;
transition: transform 0.1s ease-out;
}
</style>
@@ -0,0 +1,35 @@
<template>
<div class="eyeball-pupil" :style="pupilStyle"></div>
</template>
<script setup lang="ts">
interface Props {
size?: string;
pupilSize?: string;
maxDistance?: number;
eyeColor?: string;
pupilColor?: string;
}
const { size = "8px", pupilSize = "12px", maxDistance = 10, eyeColor = "white", pupilColor = "black" } = defineProps<Props>();
const eyeballStyle = {
width: size,
height: size,
borderRadius: "50%",
backgroundColor: eyeColor,
display: "flex",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
willChange: "height",
};
const pupilStyle = {
width: pupilSize,
height: pupilSize,
borderRadius: "50%",
backgroundColor: pupilColor,
willChange: "transform",
};
</script>
@@ -0,0 +1,571 @@
<template>
<div ref="containerRef" :style="containerStyle">
<!-- 紫色角色 -->
<div ref="purpleRef" :style="purpleBodyStyle">
<div ref="purpleFaceRef" :style="purpleFaceStyle">
<EyeBall :size="20" :pupil-size="7" :max-distance="5" eye-color="white" pupil-color="#2D2D2D" />
<EyeBall :size="20" :pupil-size="7" :max-distance="5" eye-color="white" pupil-color="#2D2D2D" />
</div>
</div>
<!-- 黑色角色 -->
<div ref="blackRef" :style="blackBodyStyle">
<div ref="blackFaceRef" :style="blackFaceStyle">
<EyeBall :size="20" :pupil-size="7" :max-distance="5" eye-color="white" pupil-color="#2D2D2D" />
<EyeBall :size="20" :pupil-size="7" :max-distance="5" eye-color="white" pupil-color="#2D2D2D" />
</div>
</div>
<!-- 橘黄色角色 -->
<div ref="orangeRef" :style="orangeBodyStyle">
<div ref="orangeFaceRef" :style="orangeFaceStyle">
<Pupil size="8px" :max-distance="5" pupil-color="#2D2D2D" />
<Pupil size="8px" :max-distance="5" pupil-color="#2D2D2D" />
</div>
</div>
<!-- 黄色角色 -->
<div ref="yellowRef" :style="yellowBodyStyle">
<div ref="yellowFaceRef" :style="yellowFaceStyle">
<Pupil size="8px" :max-distance="5" pupil-color="#2D2D2D" />
<Pupil size="8px" :max-distance="5" pupil-color="#2D2D2D" />
</div>
<div ref="yellowMouthRef" :style="yellowMouthStyle" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, onBeforeUnmount, watch, toRef } from "vue";
import gsap from "gsap";
import Pupil from "./Pupil.vue";
import EyeBall from "./EyeBall.vue";
interface Props {
isTyping?: boolean;
showPassword?: boolean;
passwordLength?: number;
}
const props = withDefaults(defineProps<Props>(), {
isTyping: false,
showPassword: false,
passwordLength: 0,
});
const containerRef = ref<HTMLElement | null>(null);
const mouseRef = reactive({ x: 0, y: 0 });
const rafIdRef = ref<number>(0);
const purpleRef = ref<HTMLElement | null>(null);
const blackRef = ref<HTMLElement | null>(null);
const yellowRef = ref<HTMLElement | null>(null);
const orangeRef = ref<HTMLElement | null>(null);
const purpleFaceRef = ref<HTMLElement | null>(null);
const blackFaceRef = ref<HTMLElement | null>(null);
const yellowFaceRef = ref<HTMLElement | null>(null);
const orangeFaceRef = ref<HTMLElement | null>(null);
const yellowMouthRef = ref<HTMLElement | null>(null);
const purpleBlinkTimerRef = ref<ReturnType<typeof setTimeout>>();
const blackBlinkTimerRef = ref<ReturnType<typeof setTimeout>>();
const purplePeekTimerRef = ref<ReturnType<typeof setTimeout>>();
const isHidingPassword = toRef(() => props.passwordLength > 0 && !props.showPassword);
const isShowingPassword = toRef(() => props.passwordLength > 0 && props.showPassword);
const isLookingRef = ref(false);
const lookingTimerRef = ref<ReturnType<typeof setTimeout>>();
const stateRef = reactive({
isTyping: false,
isHidingPassword: false,
isShowingPassword: false,
isLooking: false,
});
watch(
() => [props.isTyping, isHidingPassword.value, isShowingPassword.value, isLookingRef.value] as const,
([isTyping, isHiding, isShowing, isLooking]) => {
stateRef.isTyping = isTyping;
stateRef.isHidingPassword = isHiding;
stateRef.isShowingPassword = isShowing;
stateRef.isLooking = isLooking;
},
);
// GSAP quickTo instances
const quickToRef = ref<Record<string, any> | null>(null);
const containerStyle = {
position: "relative" as const,
width: "550px",
height: "400px",
};
const purpleBodyStyle = ref<any>({
position: "absolute",
bottom: 0,
left: "70px",
width: "180px",
height: "400px",
backgroundColor: "#6C3FF5",
borderRadius: "10px 10px 0 0",
zIndex: 1,
transformOrigin: "bottom center",
willChange: "transform",
});
const blackBodyStyle = ref<any>({
position: "absolute",
bottom: 0,
left: "240px",
width: "120px",
height: "310px",
backgroundColor: "#2D2D2D",
borderRadius: "8px 8px 0 0",
zIndex: 2,
transformOrigin: "bottom center",
willChange: "transform",
});
const orangeBodyStyle = ref<any>({
position: "absolute",
bottom: 0,
left: 0,
width: "240px",
height: "200px",
backgroundColor: "#FF9B6B",
borderRadius: "120px 120px 0 0",
zIndex: 3,
transformOrigin: "bottom center",
willChange: "transform",
});
const yellowBodyStyle = ref<any>({
position: "absolute",
bottom: 0,
left: "310px",
width: "140px",
height: "230px",
backgroundColor: "#E8D754",
borderRadius: "70px 70px 0 0",
zIndex: 4,
transformOrigin: "bottom center",
willChange: "transform",
});
const purpleFaceStyle = ref<any>({
position: "absolute",
display: "flex",
gap: "32px",
left: "45px",
top: "40px",
});
const blackFaceStyle = ref<any>({
position: "absolute",
display: "flex",
gap: "24px",
left: "26px",
top: "32px",
});
const orangeFaceStyle = ref<any>({
position: "absolute",
display: "flex",
gap: "32px",
left: "82px",
top: "90px",
});
const yellowFaceStyle = ref<any>({
position: "absolute",
display: "flex",
gap: "24px",
left: "52px",
top: "40px",
});
const yellowMouthStyle = ref<any>({
position: "absolute",
width: "80px",
height: "4px",
backgroundColor: "#2D2D2D",
borderRadius: "9999px",
left: "40px",
top: "88px",
});
// Initialize GSAP
onMounted(() => {
gsap.set(".pupil", { x: 0, y: 0 });
gsap.set(".eyeball-pupil", { x: 0, y: 0 });
});
onMounted(() => {
if (!purpleRef.value || !blackRef.value || !orangeRef.value || !yellowRef.value || !purpleFaceRef.value || !blackFaceRef.value || !orangeFaceRef.value || !yellowFaceRef.value || !yellowMouthRef.value) return;
const qt = {
purpleSkew: gsap.quickTo(purpleRef.value, "skewX", { duration: 0.3, ease: "power2.out" }),
blackSkew: gsap.quickTo(blackRef.value, "skewX", { duration: 0.3, ease: "power2.out" }),
orangeSkew: gsap.quickTo(orangeRef.value, "skewX", { duration: 0.3, ease: "power2.out" }),
yellowSkew: gsap.quickTo(yellowRef.value, "skewX", { duration: 0.3, ease: "power2.out" }),
purpleX: gsap.quickTo(purpleRef.value, "x", { duration: 0.3, ease: "power2.out" }),
blackX: gsap.quickTo(blackRef.value, "x", { duration: 0.3, ease: "power2.out" }),
purpleHeight: gsap.quickTo(purpleRef.value, "height", { duration: 0.3, ease: "power2.out" }),
purpleFaceLeft: gsap.quickTo(purpleFaceRef.value, "left", { duration: 0.3, ease: "power2.out" }),
purpleFaceTop: gsap.quickTo(purpleFaceRef.value, "top", { duration: 0.3, ease: "power2.out" }),
blackFaceLeft: gsap.quickTo(blackFaceRef.value, "left", { duration: 0.3, ease: "power2.out" }),
blackFaceTop: gsap.quickTo(blackFaceRef.value, "top", { duration: 0.3, ease: "power2.out" }),
orangeFaceX: gsap.quickTo(orangeFaceRef.value, "x", { duration: 0.2, ease: "power2.out" }),
orangeFaceY: gsap.quickTo(orangeFaceRef.value, "y", { duration: 0.2, ease: "power2.out" }),
yellowFaceX: gsap.quickTo(yellowFaceRef.value, "x", { duration: 0.2, ease: "power2.out" }),
yellowFaceY: gsap.quickTo(yellowFaceRef.value, "y", { duration: 0.2, ease: "power2.out" }),
mouthX: gsap.quickTo(yellowMouthRef.value, "x", { duration: 0.2, ease: "power2.out" }),
mouthY: gsap.quickTo(yellowMouthRef.value, "y", { duration: 0.2, ease: "power2.out" }),
};
quickToRef.value = qt;
const calcPos = (el: HTMLElement) => {
const rect = el.getBoundingClientRect();
const cx = rect.left + rect.width / 2;
const cy = rect.top + rect.height / 3;
const dx = mouseRef.x - cx;
const dy = mouseRef.y - cy;
return {
faceX: Math.max(-15, Math.min(15, dx / 20)),
faceY: Math.max(-10, Math.min(10, dy / 30)),
bodySkew: Math.max(-6, Math.min(6, -dx / 120)),
};
};
const calcEyePos = (el: HTMLElement, maxDist: number) => {
const r = el.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const dx = mouseRef.x - cx;
const dy = mouseRef.y - cy;
const dist = Math.min(Math.sqrt(dx ** 2 + dy ** 2), maxDist);
const angle = Math.atan2(dy, dx);
return { x: Math.cos(angle) * dist, y: Math.sin(angle) * dist };
};
const tick = () => {
const container = containerRef.value;
if (!container) return;
const { isTyping: typing, isHidingPassword: hiding, isShowingPassword: showing, isLooking: looking } = stateRef;
if (purpleRef.value && !showing) {
const pp = calcPos(purpleRef.value);
if (typing || hiding) {
qt.purpleSkew(pp.bodySkew - 12);
qt.purpleX(40);
qt.purpleHeight(440);
} else {
qt.purpleSkew(pp.bodySkew);
qt.purpleX(0);
qt.purpleHeight(400);
}
}
if (blackRef.value && !showing) {
const bp = calcPos(blackRef.value);
if (looking) {
qt.blackSkew(bp.bodySkew * 1.5 + 10);
qt.blackX(20);
} else if (typing || hiding) {
qt.blackSkew(bp.bodySkew * 1.5);
qt.blackX(0);
} else {
qt.blackSkew(bp.bodySkew);
qt.blackX(0);
}
}
if (orangeRef.value && !showing) {
const op = calcPos(orangeRef.value);
qt.orangeSkew(op.bodySkew);
}
if (yellowRef.value && !showing) {
const yp = calcPos(yellowRef.value);
qt.yellowSkew(yp.bodySkew);
}
if (purpleRef.value && !showing && !looking) {
const pp = calcPos(purpleRef.value);
const purpleFaceX = pp.faceX >= 0 ? Math.min(25, pp.faceX * 1.5) : pp.faceX;
qt.purpleFaceLeft(45 + purpleFaceX);
qt.purpleFaceTop(40 + pp.faceY);
}
if (blackRef.value && !showing && !looking) {
const bp = calcPos(blackRef.value);
qt.blackFaceLeft(26 + bp.faceX);
qt.blackFaceTop(32 + bp.faceY);
}
if (orangeRef.value && !showing) {
const op = calcPos(orangeRef.value);
qt.orangeFaceX(op.faceX);
qt.orangeFaceY(op.faceY);
}
if (yellowRef.value && !showing) {
const yp = calcPos(yellowRef.value);
qt.yellowFaceX(yp.faceX);
qt.yellowFaceY(yp.faceY);
qt.mouthX(yp.faceX);
qt.mouthY(yp.faceY);
}
if (!showing) {
const allPupils = container.querySelectorAll(".pupil");
allPupils.forEach((p) => {
const el = p as HTMLElement;
console.log("--> maxDistance: ", el.dataset.maxDistance);
const maxDist = Number(el.dataset.maxDistance) || 5;
const ePos = calcEyePos(el, maxDist);
gsap.set(el, { x: ePos.x, y: ePos.y });
});
if (!looking) {
const allEyeballs = container.querySelectorAll(".eyeball");
allEyeballs.forEach((eb) => {
const el = eb as HTMLElement;
const maxDist = Number(el.dataset.maxDistance) || 10;
const pupil = el.querySelector(".eyeball-pupil") as HTMLElement;
if (!pupil) return;
const ePos = calcEyePos(el, maxDist);
gsap.set(pupil, { x: ePos.x, y: ePos.y });
});
}
}
rafIdRef.value = requestAnimationFrame(tick);
};
const onMove = (e: MouseEvent) => {
mouseRef.x = e.clientX;
mouseRef.y = e.clientY;
};
window.addEventListener("mousemove", onMove, { passive: true });
rafIdRef.value = requestAnimationFrame(tick);
onBeforeUnmount(() => {
window.removeEventListener("mousemove", onMove);
cancelAnimationFrame(rafIdRef.value);
});
});
// Purple character blink
onMounted(() => {
const purpleEyeballs = purpleRef.value?.querySelectorAll(".eyeball");
if (!purpleEyeballs?.length) return;
const scheduleBlink = () => {
purpleBlinkTimerRef.value = setTimeout(
() => {
purpleEyeballs.forEach((el) => {
gsap.to(el, { height: 2, duration: 0.08, ease: "power2.in" });
});
setTimeout(() => {
purpleEyeballs.forEach((el) => {
const size = Number((el as HTMLElement).style.width.replace("px", "")) || 18;
gsap.to(el, { width: size, height: size, duration: 0.08, ease: "power2.out" });
});
scheduleBlink();
}, 150);
},
Math.random() * 4000 + 3000,
);
};
scheduleBlink();
onBeforeUnmount(() => clearTimeout(purpleBlinkTimerRef.value));
});
// Black character blink
onMounted(() => {
const blackEyeballs = blackRef.value?.querySelectorAll(".eyeball");
if (!blackEyeballs?.length) return;
const scheduleBlink = () => {
blackBlinkTimerRef.value = setTimeout(
() => {
blackEyeballs.forEach((el) => {
gsap.to(el, { height: 2, duration: 0.08, ease: "power2.in" });
});
setTimeout(() => {
blackEyeballs.forEach((el) => {
const size = Number((el as HTMLElement).style.width.replace("px", "")) || 16;
gsap.to(el, { height: size, duration: 0.08, ease: "power2.out" });
});
scheduleBlink();
}, 150);
},
Math.random() * 4000 + 3000,
);
};
scheduleBlink();
onBeforeUnmount(() => clearTimeout(blackBlinkTimerRef.value));
});
const applyLookAtEachOther = () => {
const qt = quickToRef.value;
if (qt) {
qt.purpleFaceLeft(55);
qt.purpleFaceTop(65);
qt.blackFaceLeft(32);
qt.blackFaceTop(12);
}
purpleRef.value?.querySelectorAll(".eyeball-pupil").forEach((p) => {
gsap.to(p, { x: 3, y: 4, duration: 0.3, ease: "power2.out", overwrite: "auto" });
});
blackRef.value?.querySelectorAll(".eyeball-pupil").forEach((p) => {
gsap.to(p, { x: 0, y: -4, duration: 0.3, ease: "power2.out", overwrite: "auto" });
});
};
const applyHidingPassword = () => {
const qt = quickToRef.value;
if (qt) {
qt.purpleFaceLeft(55);
qt.purpleFaceTop(65);
}
};
const applyShowPassword = () => {
const qt = quickToRef.value;
if (qt) {
qt.purpleSkew(0);
qt.blackSkew(0);
qt.orangeSkew(0);
qt.yellowSkew(0);
qt.purpleX(0);
qt.blackX(0);
qt.purpleHeight(400);
qt.purpleFaceLeft(20);
qt.purpleFaceTop(35);
qt.blackFaceLeft(10);
qt.blackFaceTop(28);
qt.orangeFaceX(50 - 82);
qt.orangeFaceY(85 - 90);
qt.yellowFaceX(20 - 52);
qt.yellowFaceY(35 - 40);
qt.mouthX(10 - 40);
qt.mouthY(0);
}
purpleRef.value?.querySelectorAll(".eyeball-pupil").forEach((p) => {
gsap.to(p, { x: -4, y: -4, duration: 0.3, ease: "power2.out", overwrite: "auto" });
});
blackRef.value?.querySelectorAll(".eyeball-pupil").forEach((p) => {
gsap.to(p, { x: -4, y: -4, duration: 0.3, ease: "power2.out", overwrite: "auto" });
});
orangeRef.value?.querySelectorAll(".pupil").forEach((p) => {
gsap.to(p, { x: -5, y: -4, duration: 0.3, ease: "power2.out", overwrite: "auto" });
});
yellowRef.value?.querySelectorAll(".pupil").forEach((p) => {
gsap.to(p, { x: -5, y: -4, duration: 0.3, ease: "power2.out", overwrite: "auto" });
});
};
// Password peek effect
watch(
() => [isShowingPassword.value, props.passwordLength],
([showing, len]) => {
if (!showing || (len as number) <= 0) {
clearTimeout(purplePeekTimerRef.value);
return;
}
const purpleEyePupils = purpleRef.value?.querySelectorAll(".eyeball-pupil");
if (!purpleEyePupils?.length) return;
const schedulePeek = () => {
purplePeekTimerRef.value = setTimeout(
() => {
purpleEyePupils.forEach((p) => {
gsap.to(p, {
x: 4,
y: 5,
duration: 0.3,
ease: "power2.out",
overwrite: "auto",
});
});
const qt = quickToRef.value;
if (qt) {
qt.purpleFaceLeft(20);
qt.purpleFaceTop(35);
}
setTimeout(() => {
purpleEyePupils.forEach((p) => {
gsap.to(p, {
x: -4,
y: -4,
duration: 0.3,
ease: "power2.out",
overwrite: "auto",
});
});
schedulePeek();
}, 800);
},
Math.random() * 3000 + 2000,
);
};
schedulePeek();
onBeforeUnmount(() => clearTimeout(purplePeekTimerRef.value));
},
);
// Look at each other when typing
watch(
() => [props.isTyping, isShowingPassword.value],
([typing, showing]) => {
if (typing && !showing) {
isLookingRef.value = true;
stateRef.isLooking = true;
applyLookAtEachOther();
clearTimeout(lookingTimerRef.value);
lookingTimerRef.value = setTimeout(() => {
isLookingRef.value = false;
stateRef.isLooking = false;
purpleRef.value?.querySelectorAll(".eyeball-pupil").forEach((p) => {
gsap.killTweensOf(p);
});
}, 800);
} else {
clearTimeout(lookingTimerRef.value);
isLookingRef.value = false;
stateRef.isLooking = false;
}
},
);
// Password state effects
watch(
() => {
return [isShowingPassword.value, isHidingPassword.value];
},
([showing, hiding]) => {
if (showing) {
applyShowPassword();
} else if (hiding) {
applyHidingPassword();
}
},
);
</script>
+1 -1
View File
@@ -62,6 +62,6 @@ export default {
/**
* 底部版权文本内容
*/
footerContent: 'Copyright © 2018-2026 RuoYi. All Rights Reserved.'
footerContent: 'Copyright © 2018-2026 LINGTAO. All Rights Reserved.'
}
+156 -125
View File
@@ -1,62 +1,49 @@
<template>
<div class="login">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">{{ title }}</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
type="text"
size="large"
auto-complete="off"
placeholder="账号"
>
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
size="large"
auto-complete="off"
placeholder="密码"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
</el-input>
</el-form-item>
<el-form-item prop="code" v-if="captchaEnabled">
<el-input
v-model="loginForm.code"
size="large"
auto-complete="off"
placeholder="验证码"
style="width: 63%"
@keyup.enter="handleLogin"
>
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
</el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/>
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
<el-form-item style="width:100%;">
<el-button
:loading="loading"
size="large"
type="primary"
style="width:100%;"
@click.prevent="handleLogin"
>
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
<div style="float: right;" v-if="register">
<router-link class="link-type" :to="'/register'">立即注册</router-link>
</div>
</el-form-item>
</el-form>
<div class="character-container">
<LoginCharacter :isTyping="isTyping" :showPassword="showPassword" :passwordLength="loginForm.password.length" />
</div>
<div class="login-container">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">{{ title }}</h3>
<el-label class="login-title">账号</el-label>
<el-form-item prop="username">
<el-input v-model="loginForm.username" @focus="isTyping = true" @blur="isTyping = false" type="text" size="large" auto-complete="off" placeholder="账号"> </el-input>
</el-form-item>
<el-label class="login-title">密码</el-label>
<el-form-item prop="password">
<el-input v-model="loginForm.password" :type="showPassword ? 'text' : 'password'" size="large" auto-complete="off" placeholder="密码" @keyup.enter="handleLogin" @input="inputPassWord">
<template #suffix>
<view @click="showPassword = !showPassword" style="cursor: pointer">
<template v-if="!showPassword">
<el-icon><View /></el-icon>
</template>
<template v-else>
<el-icon><Hide /></el-icon>
</template>
</view>
</template>
</el-input>
</el-form-item>
<el-label class="login-title">验证码</el-label>
<el-form-item prop="code" v-if="captchaEnabled">
<el-input v-model="loginForm.code" size="large" auto-complete="off" placeholder="验证码" style="width: 63%" @keyup.enter="handleLogin"> </el-input>
<div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img" />
</div>
</el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin: 0px 0px 25px 0px">记住密码</el-checkbox>
<el-form-item style="width: 100%">
<el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
<span v-if="!loading"> </span>
<span v-else> 中...</span>
</el-button>
<div style="float: right" v-if="register">
<router-link class="link-type" :to="'/register'">立即注册</router-link>
</div>
</el-form-item>
</el-form>
</div>
<!-- 底部 -->
<div class="el-login-footer">
<span>{{ footerContent }}</span>
@@ -65,131 +52,160 @@
</template>
<script setup>
import { getCodeImg } from "@/api/login"
import Cookies from "js-cookie"
import { encrypt, decrypt } from "@/utils/jsencrypt"
import useUserStore from '@/store/modules/user'
import defaultSettings from '@/settings'
import { getCodeImg } from "@/api/login";
import Cookies from "js-cookie";
import { encrypt, decrypt } from "@/utils/jsencrypt";
import useUserStore from "@/store/modules/user";
import defaultSettings from "@/settings";
import LoginCharacter from "@/components/LoginCharacters";
const title = import.meta.env.VITE_APP_TITLE
const footerContent = defaultSettings.footerContent
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
const { proxy } = getCurrentInstance()
const title = import.meta.env.VITE_APP_TITLE;
const footerContent = defaultSettings.footerContent;
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance();
const loginForm = ref({
username: "admin",
password: "admin123",
rememberMe: false,
code: "",
uuid: ""
})
uuid: "",
});
const loginRules = {
username: [{ required: true, trigger: "blur", message: "请输入您的账号" }],
password: [{ required: true, trigger: "blur", message: "请输入您的密码" }],
code: [{ required: true, trigger: "change", message: "请输入验证码" }]
}
code: [{ required: true, trigger: "change", message: "请输入验证码" }],
};
const codeUrl = ref("")
const loading = ref(false)
const codeUrl = ref("");
const loading = ref(false);
// 验证码开关
const captchaEnabled = ref(true)
const captchaEnabled = ref(true);
// 注册开关
const register = ref(false)
const redirect = ref(undefined)
const register = ref(false);
const redirect = ref(undefined);
watch(route, (newRoute) => {
redirect.value = newRoute.query && newRoute.query.redirect
}, { immediate: true })
const inputPassWordRef = ref(null);
const isTyping = ref(false);
const showPassword = ref(false);
const passwordLength = ref(0);
watch(
route,
(newRoute) => {
redirect.value = newRoute.query && newRoute.query.redirect;
},
{ immediate: true },
);
function handleLogin() {
proxy.$refs.loginRef.validate(valid => {
proxy.$refs.loginRef.validate((valid) => {
if (valid) {
loading.value = true
loading.value = true;
// 勾选了需要记住密码设置在 cookie 中设置记住用户名和密码
if (loginForm.value.rememberMe) {
Cookies.set("username", loginForm.value.username, { expires: 30 })
Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 })
Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 })
Cookies.set("username", loginForm.value.username, { expires: 30 });
Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 });
Cookies.set("rememberMe", loginForm.value.rememberMe, { expires: 30 });
} else {
// 否则移除
Cookies.remove("username")
Cookies.remove("password")
Cookies.remove("rememberMe")
Cookies.remove("username");
Cookies.remove("password");
Cookies.remove("rememberMe");
}
// 调用action的登录方法
userStore.login(loginForm.value).then(() => {
const query = route.query
const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
if (cur !== "redirect") {
acc[cur] = query[cur]
userStore
.login(loginForm.value)
.then(() => {
const query = route.query;
const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
if (cur !== "redirect") {
acc[cur] = query[cur];
}
return acc;
}, {});
router.push({ path: redirect.value || "/", query: otherQueryParams });
})
.catch(() => {
loading.value = false;
// 重新获取验证码
if (captchaEnabled.value) {
getCode();
}
return acc
}, {})
router.push({ path: redirect.value || "/", query: otherQueryParams })
}).catch(() => {
loading.value = false
// 重新获取验证码
if (captchaEnabled.value) {
getCode()
}
})
});
}
})
});
}
function getCode() {
getCodeImg().then(res => {
captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled
getCodeImg().then((res) => {
captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled;
if (captchaEnabled.value) {
codeUrl.value = "data:image/gif;base64," + res.img
loginForm.value.uuid = res.uuid
codeUrl.value = "data:image/gif;base64," + res.img;
loginForm.value.uuid = res.uuid;
}
})
});
}
function getCookie() {
const username = Cookies.get("username")
const password = Cookies.get("password")
const rememberMe = Cookies.get("rememberMe")
const username = Cookies.get("username");
const password = Cookies.get("password");
const rememberMe = Cookies.get("rememberMe");
loginForm.value = {
username: username === undefined ? loginForm.value.username : username,
password: password === undefined ? loginForm.value.password : decrypt(password),
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe)
}
rememberMe: rememberMe === undefined ? false : Boolean(rememberMe),
};
}
getCode()
getCookie()
getCode();
getCookie();
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
.character-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.login {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-image: url("../assets/images/login-background.jpg");
height: 100vh;
width: 100vw;
background-color: #707070;
background-size: cover;
}
.title {
margin: 0px auto 30px auto;
margin: 0px auto 90px auto;
text-align: center;
font-weight: bold;
color: #707070;
font-size: 26px;
}
.login-form {
border-radius: 6px;
.login-container {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
height: 100vh;
width: 100vw;
background: #ffffff;
width: 400px;
}
.login-form {
padding: 25px 25px 5px 25px;
z-index: 1;
.el-input {
height: 40px;
height: 50px;
input {
height: 40px;
height: 50px;
}
}
.input-icon {
@@ -236,4 +252,19 @@ html.dark .login {
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
}
:deep(.el-input__wrapper) {
border-radius: 50px;
}
:deep(.el-input--large) {
font-size: 18px;
}
.login-title {
font-weight: bold;
font-size: 14px;
}
:deep(.el-form-item--default) {
margin-top: 10px;
}
</style>
@@ -0,0 +1,90 @@
<template>
<view>
<view class="line-container">
<view class="form-title">裁切工艺</view>
<ChekcBoxOnly :data="craftData[0].options" v-model="craftData[0].value" />
</view>
<view class="line-container">
<view class="form-title">覆膜工艺</view>
<ChekcBoxOnly :data="craftData[1].options" v-model="craftData[1].value" />
</view>
<view class="line-container">
<!-- 刮刮膜带有子选择 -->
<view class="form-title">刮刮膜</view>
<ChekcBoxOnly :data="craftData[2].options" v-model="craftData[2].value" />
<el-select v-if="craftData[2].value.length > 0" v-model="craftData[2].size" placeholder="选择尺寸" style="width: 100px; margin-left: 20px">
<el-option label="6*18mm" value="6*18" />
</el-select>
</view>
</view>
</template>
<script setup>
import { ref, defineEmits, defineProps, watch } from "vue";
import ChekcBoxOnly from "@/components/CheckBoxOnly";
const craftData = ref([
{
value: ["模切"],
options: [
{ value: "模切", label: "模切" },
{ value: "裁切", label: "裁切" },
],
},
{
value: [],
options: [
{ value: "覆哑膜", label: "覆哑膜" },
{ value: "覆亮膜", label: "覆亮膜" },
],
},
{
value: [],
process: [],
size: "",
options: [
{ value: "配刮刮膜", label: "配刮刮膜" },
{ value: "粘刮刮膜", label: "粘刮刮膜" },
],
},
]);
const emit = defineEmits(["update:modelValue"]);
watch(
craftData,
(val) => {
let params = [];
val.forEach((item) => {
if (item.value.length > 0) {
item.value.forEach((i) => {
let info = {
name: i,
value: i,
};
if (i == "配刮刮膜" || i == "粘刮刮膜") {
info.size = item.size;
}
params.push(info);
});
}
});
emit("update:modelValue", params);
},
{ deep: true },
);
</script>
<style lang="scss" scoped>
.line-container {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.form-title {
display: block;
font-weight: bold;
font-size: 16px;
margin-right: 20px;
}
</style>
@@ -0,0 +1,127 @@
<template>
<view class="container">
<h1 style="font-weight: bold">不干胶</h1>
<el-form>
<el-form-item>
<el-radio-group v-model="data.types" @change="handleChange">
<el-radio label="常用种类" value="常用种类" size="large" border />
<el-radio label="专版打印" value="专版打印" size="large" border />
</el-radio-group>
</el-form-item>
<view class="form-title">品种</view>
<el-form-item>
<el-select v-model="data.material" placeholder="请选择" size="large" style="width: 300px">
<template v-for="item in materials" :key="item.name">
<el-option :label="item.name" :value="item.name"></el-option>
</template>
</el-select>
</el-form-item>
<view class="form-title">尺寸</view>
<el-form-item>
<el-input style="width: 150px" v-model="data.size[0]" type="number">
<template #append> <text>cm</text></template>
</el-input>
<text style="margin: 0 10px">X</text>
<el-input style="width: 150px" v-model="data.size[1]" type="number">
<template #append> <text>cm</text> </template>
</el-input>
</el-form-item>
<view class="form-title">个数</view>
<el-form-item>
<el-input style="width: 300px" v-model="data.count" type="number" placeholder="请输入数量" size="large"></el-input>
</el-form-item>
<view class="form-title">款数</view>
<el-form-item>
<el-input style="width: 300px" v-model="data.number" type="number" placeholder="请输入数量" size="large"></el-input>
</el-form-item>
<view class="form-title">客户旺旺</view>
<el-form-item>
<el-input style="width: 300px" v-model="data.number" placeholder="请输入客户旺旺" size="large"></el-input>
</el-form-item>
<component v-if="currentCraftComponent" :is="currentCraftComponent" v-model="data.craft" />
<el-button type="primary" @click="handlesave">保存</el-button>
</el-form>
</view>
</template>
<script setup>
import { reactive, watch, shallowRef } from "vue";
import copperplate from "./crafts/copperplate.vue";
const materials = [
{ name: "铜板纸不干胶", images: "" },
{ name: "pvc不干胶", images: "" },
{ name: "透明不干胶", images: "" },
{ name: "牛皮纸", images: "" },
{ name: "哑金不干胶", images: "" },
{ name: "哑银不干胶", images: "" },
{ name: "书写纸不干胶", images: "" },
{ name: "银平光", images: "" },
{ name: "拉丝金", images: "" },
{ name: "拉丝银", images: "" },
{ name: "美纹纸", images: "" },
{ name: "PP合成纸", images: "" },
{ name: "易碎纸不干胶", images: "" },
{ name: "刚古水纹超白", images: "" },
{ name: "红底散金", images: "" },
{ name: "布纹纸超白", images: "" },
{ name: "珠光超白", images: "" },
{ name: "10C静电膜", images: "" },
{ name: "树纹纸", images: "" },
{ name: "草香纸/大地纸", images: "" },
{ name: "单防热敏纸(底纸白色)", images: "" },
{ name: "三防热敏纸(底纸蓝色)", images: "" },
];
const craftMap = {
铜板纸不干胶: copperplate,
};
const currentCraftComponent = shallowRef(null);
const handlesave = () => {
console.log(data.value);
};
const data = ref({
types: "常用种类",
material: "",
size: [],
count: null,
number: 1,
craft: [],
});
watch(
() => data.value.material,
(mat) => {
currentCraftComponent.value = craftMap[mat] || null;
data.value.craft = {}; // 重置
},
);
const handleChange = (val) => {
const initData = {
types: val,
material: "",
size: [],
count: null,
number: 1,
craft: [],
};
data.value = initData;
};
</script>
<style lang="scss" scoped>
.el-form-item--default {
margin-bottom: 10px;
}
.container {
margin: 0 20px;
display: flex;
flex-direction: column;
}
.form-title {
display: block;
font-weight: bold;
font-size: 16px;
margin-bottom: 5px;
}
</style>