From b1077b23e65214924c2093d6cfe7799b1753c9cd Mon Sep 17 00:00:00 2001 From: LOG1997 <2694233102@qq.com> Date: Sun, 21 Sep 2025 14:38:57 +0800 Subject: [PATCH 01/16] =?UTF-8?q?refactor:=20=E9=A6=96=E9=A1=B5=E6=8F=90?= =?UTF-8?q?=E5=8F=96=E6=A0=B7=E5=BC=8F=E8=87=B3=E5=8D=95=E7=8B=AC=E7=9A=84?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/Home/index.scss | 341 +++++++++++++++++++++++++++++++++++ src/views/Home/index.vue | 368 ++------------------------------------ 2 files changed, 357 insertions(+), 352 deletions(-) create mode 100644 src/views/Home/index.scss diff --git a/src/views/Home/index.scss b/src/views/Home/index.scss new file mode 100644 index 0000000..b882304 --- /dev/null +++ b/src/views/Home/index.scss @@ -0,0 +1,341 @@ +#menu { + position: absolute; + z-index: 100; + width: 100%; + bottom: 50px; + text-align: center; + margin: 0 auto; + font-size: 32px; +} + +.header-title { + -webkit-animation: tracking-in-expand-fwd 0.8s cubic-bezier(0.215, 0.610, 0.355, 1.000) both; + animation: tracking-in-expand-fwd 0.8s cubic-bezier(0.215, 0.610, 0.355, 1.000) both; +} + +.start { + // 居中 + display: flex; + justify-content: center; +} + +.btn-start { + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + width: 13rem; + overflow: hidden; + height: 3rem; + background-size: 300% 300%; + backdrop-filter: blur(1rem); + border-radius: 5rem; + transition: 0.5s; + animation: gradient_301 5s ease infinite; + border: double 4px transparent; + background-image: linear-gradient(#212121, #212121), linear-gradient(137.48deg, #ffdb3b 10%, #FE53BB 45%, #8F51EA 67%, #0044ff 87%); + background-origin: border-box; + background-clip: content-box, border-box; + -webkit-animation: pulsate-fwd 1.2s ease-in-out infinite both; + animation: pulsate-fwd 1.2s ease-in-out infinite both; +} + +.btn-cancel { + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + width: 13rem; + overflow: hidden; + height: 3rem; + background-size: 300% 300%; + backdrop-filter: blur(1rem); + border-radius: 5rem; + transition: 0.5s; + animation: gradient_301 5s ease infinite; + border: double 4px transparent; + background-image: linear-gradient(#212121, #212121), linear-gradient(137.48deg, #ffdb3b 10%, #FE53BB 45%, #8F51EA 67%, #0044ff 87%); + background-origin: border-box; + background-clip: content-box, border-box; +} + +#container-stars { + position: absolute; + z-index: -1; + width: 100%; + height: 100%; + overflow: hidden; + transition: 0.5s; + backdrop-filter: blur(1rem); + border-radius: 5rem; +} + +strong { + z-index: 2; + font-family: 'Avalors Personal Use'; + font-size: 12px; + letter-spacing: 5px; + color: #FFFFFF; + text-shadow: 0 0 4px white; +} + +#glow { + position: absolute; + display: flex; + width: 12rem; +} + +.circle { + width: 100%; + height: 30px; + filter: blur(2rem); + animation: pulse_3011 4s infinite; + z-index: -1; +} + +.circle:nth-of-type(1) { + background: rgba(254, 83, 186, 0.636); +} + +.circle:nth-of-type(2) { + background: rgba(142, 81, 234, 0.704); +} + +.btn-start:hover #container-stars { + z-index: 1; + background-color: #212121; +} + +.btn-start:hover { + transform: scale(1.1) +} + +.btn-start:active { + border: double 4px #FE53BB; + background-origin: border-box; + background-clip: content-box, border-box; + animation: none; +} + +.btn-start:active .circle { + background: #FE53BB; +} + +#stars { + position: relative; + background: transparent; + width: 200rem; + height: 200rem; +} + +#stars::after { + content: ""; + position: absolute; + top: -10rem; + left: -100rem; + width: 100%; + height: 100%; + animation: animStarRotate 90s linear infinite; +} + +#stars::after { + background-image: radial-gradient(#ffffff 1px, transparent 1%); + background-size: 50px 50px; +} + +#stars::before { + content: ""; + position: absolute; + top: 0; + left: -50%; + width: 170%; + height: 500%; + animation: animStar 60s linear infinite; +} + +#stars::before { + background-image: radial-gradient(#ffffff 1px, transparent 1%); + background-size: 50px 50px; + opacity: 0.5; +} + +@keyframes animStar { + from { + transform: translateY(0); + } + + to { + transform: translateY(-135rem); + } +} + +@keyframes animStarRotate { + from { + transform: rotate(360deg); + } + + to { + transform: rotate(0); + } +} + +@keyframes gradient_301 { + 0% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } + + 100% { + background-position: 0% 50%; + } +} + +@keyframes pulse_3011 { + 0% { + transform: scale(0.75); + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgba(0, 0, 0, 0); + } + + 100% { + transform: scale(0.75); + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); + } +} + +.btn-end { + -webkit-animation: pulsate-fwd 0.9s ease-in-out infinite both; + animation: pulsate-fwd 0.9s ease-in-out infinite both; + cursor: pointer; +} + +.btn-end { + --glow-color: rgb(217, 176, 255); + --glow-spread-color: rgba(191, 123, 255, 0.781); + --enhanced-glow-color: rgb(231, 206, 255); + --btn-color: rgb(100, 61, 136); + border: .25em solid var(--glow-color); + padding: 1em 3em; + color: var(--glow-color); + font-size: 15px; + font-weight: bold; + background-color: var(--btn-color); + border-radius: 1em; + outline: none; + box-shadow: 0 0 1em .25em var(--glow-color), + 0 0 4em 1em var(--glow-spread-color), + inset 0 0 .75em .25em var(--glow-color); + text-shadow: 0 0 .5em var(--glow-color); + position: relative; + transition: all 0.3s; + -webkit-animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both; + animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both; +} + +.btn-end::after { + pointer-events: none; + content: ""; + position: absolute; + top: 120%; + left: 0; + height: 100%; + width: 100%; + background-color: var(--glow-spread-color); + filter: blur(2em); + opacity: .7; + transform: perspective(1.5em) rotateX(35deg) scale(1, .6); +} + +.btn-end:hover { + color: var(--btn-color); + background-color: var(--glow-color); + box-shadow: 0 0 1em .25em var(--glow-color), + 0 0 4em 2em var(--glow-spread-color), + inset 0 0 .75em .25em var(--glow-color); +} + +.btn-end:active { + box-shadow: 0 0 0.6em .25em var(--glow-color), + 0 0 2.5em 2em var(--glow-spread-color), + inset 0 0 .5em .25em var(--glow-color); +} + +// 按钮动画 +@-webkit-keyframes pulsate-fwd { + 0% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 50% { + -webkit-transform: scale(1.1); + transform: scale(1.1); + } + + 100% { + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@keyframes pulsate-fwd { + 0% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 50% { + -webkit-transform: scale(1.2); + transform: scale(1.2); + } + + 100% { + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@-webkit-keyframes tracking-in-expand-fwd { + 0% { + letter-spacing: -0.5em; + -webkit-transform: translateZ(-700px); + transform: translateZ(-700px); + opacity: 0; + } + + 40% { + opacity: 0.6; + } + + 100% { + -webkit-transform: translateZ(0); + transform: translateZ(0); + opacity: 1; + } +} + +@keyframes tracking-in-expand-fwd { + 0% { + letter-spacing: -0.5em; + -webkit-transform: translateZ(-700px); + transform: translateZ(-700px); + opacity: 0; + } + + 40% { + opacity: 0.6; + } + + 100% { + -webkit-transform: translateZ(0); + transform: translateZ(0); + opacity: 1; + } +} \ No newline at end of file diff --git a/src/views/Home/index.vue b/src/views/Home/index.vue index 9351c44..584d9f0 100644 --- a/src/views/Home/index.vue +++ b/src/views/Home/index.vue @@ -119,29 +119,33 @@ function init() { const number = document.createElement('div') number.className = 'card-id' number.textContent = tableData.value[i].uid - if(isShowAvatar.value) number.style.display = 'none' + if (isShowAvatar.value) + number.style.display = 'none' element.appendChild(number) const symbol = document.createElement('div') symbol.className = 'card-name' symbol.textContent = tableData.value[i].name - if(isShowAvatar.value) symbol.className = 'card-name card-avatar-name' + if (isShowAvatar.value) + symbol.className = 'card-name card-avatar-name' element.appendChild(symbol) const detail = document.createElement('div') detail.className = 'card-detail' detail.innerHTML = `${tableData.value[i].department}${tableData.value[i].identity}` - if(isShowAvatar.value) detail.style.display = 'none' + if (isShowAvatar.value) + detail.style.display = 'none' element.appendChild(detail) - const avatar = document.createElement('img'); - avatar.className = 'card-avatar'; - avatar.src = tableData.value[i].avatar; - avatar.alt = 'avatar'; - avatar.style.width = '140px'; - avatar.style.height = '140px'; - if(!isShowAvatar.value) avatar.style.display = 'none' - element.appendChild(avatar); + const avatar = document.createElement('img') + avatar.className = 'card-avatar' + avatar.src = tableData.value[i].avatar + avatar.alt = 'avatar' + avatar.style.width = '140px' + avatar.style.height = '140px' + if (!isShowAvatar.value) + avatar.style.display = 'none' + element.appendChild(avatar) element = useElementStyle(element, tableData.value[i], i, patternList.value, patternColor.value, cardColor.value, cardSize.value, textSize.value) const object = new CSS3DObject(element) @@ -783,345 +787,5 @@ onUnmounted(() => { From 88e773da37f734ea3ee609c93f611cf384711e60 Mon Sep 17 00:00:00 2001 From: LOG1997 <2694233102@qq.com> Date: Sun, 21 Sep 2025 20:24:53 +0800 Subject: [PATCH 02/16] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E4=BB=A3=E7=A0=81=EF=BC=8C=E6=8F=90=E5=8F=96=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 3 + src/utils/index.ts | 78 ++++--- .../Home/components/PrizeList/index.scss | 138 ++++++++++++ .../PrizeList/index.vue} | 151 +------------ src/views/Home/index.ts | 131 +++++++++++ src/views/Home/index.vue | 208 ++++-------------- 6 files changed, 362 insertions(+), 347 deletions(-) create mode 100644 src/views/Home/components/PrizeList/index.scss rename src/views/Home/{PrizeList.vue => components/PrizeList/index.vue} (80%) create mode 100644 src/views/Home/index.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9cda78..7aa8dd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: localforage: specifier: ^1.10.0 version: 1.10.0 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 markdown-it: specifier: ^14.1.0 version: 14.1.0 diff --git a/src/utils/index.ts b/src/utils/index.ts index 0e714f0..046d246 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,50 +1,56 @@ import dayjs from 'dayjs' -// 筛选人员数据 -export function filterData(tableData: any[], localRowCount: number) { - const dataLength = tableData.length - let j = 0 - for (let i = 0; i < dataLength; i++) { - if (i % localRowCount === 0) { - j++ - } - tableData[i].x = i % localRowCount + 1 - tableData[i].y = j - tableData[i].id = i - // 是否中奖 - } - return tableData +/** + * @description: 处理表格数据,添加x,y,id等信息 + * @param tableData 表格数据 + * @param localRowCount 每一行有多少个元素 + * @returns 处理后的表格数据 + */ +export function filterData(tableData: any[], localRowCount: number) { + const dataLength = tableData.length + let j = 0 + for (let i = 0; i < dataLength; i++) { + if (i % localRowCount === 0) { + j++ + } + tableData[i].x = i % localRowCount + 1 + tableData[i].y = j + tableData[i].id = i + // 是否中奖 + } + + return tableData } export function addOtherInfo(personList: any[]) { - const len = personList.length - for (let i = 0; i < len; i++) { - personList[i].id = i - personList[i].createTime = dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss') - personList[i].updateTime = dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss') - personList[i].prizeName = [] as string[] - personList[i].prizeTime = [] as string[] - personList[i].prizeId = [] - personList[i].isWin = false - } + const len = personList.length + for (let i = 0; i < len; i++) { + personList[i].id = i + personList[i].createTime = dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss') + personList[i].updateTime = dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss') + personList[i].prizeName = [] as string[] + personList[i].prizeTime = [] as string[] + personList[i].prizeId = [] + personList[i].isWin = false + } - return personList + return personList } export function selectCard(cardIndexArr: number[], tableLength: number, personId: number): number { - const cardIndex = Math.floor(Math.random() * (tableLength - 1)) - if (cardIndexArr.includes(cardIndex)) { - return selectCard(cardIndexArr, tableLength, personId) - } + const cardIndex = Math.floor(Math.random() * (tableLength - 1)) + if (cardIndexArr.includes(cardIndex)) { + return selectCard(cardIndexArr, tableLength, personId) + } - return cardIndex + return cardIndex } export function themeChange(theme: string) { - // 获取根html - const html = document.querySelectorAll('html') - if (html) { - html[0].setAttribute('data-theme', theme) - localStorage.setItem('theme', theme) - } + // 获取根html + const html = document.querySelectorAll('html') + if (html) { + html[0].setAttribute('data-theme', theme) + localStorage.setItem('theme', theme) + } } diff --git a/src/views/Home/components/PrizeList/index.scss b/src/views/Home/components/PrizeList/index.scss new file mode 100644 index 0000000..1d63d31 --- /dev/null +++ b/src/views/Home/components/PrizeList/index.scss @@ -0,0 +1,138 @@ +.label { + width: 120px; +} + +.prize-list-enter-active { + -webkit-animation: slide-right 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; + animation: slide-right 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; +} + +.prize-list-leave-active { + -webkit-animation: slide-left 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; + animation: slide-left 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) both; +} + +.prize-operate-enter-active { + // 延时显示 + animation: show-operate 0.6s; + -webkit-animation: show-operate 0.6s; +} + +.current-prize { + position: relative; + display: block; + overflow: hidden; + isolation: isolate; + + border-radius: 20px; + padding: 3px; +} + +.current-prize::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 400%; + height: 100%; + background: linear-gradient(115deg, #4fcf70, #fad648, #a767e5, #12bcfe, #44ce7b); + background-size: 25% 100%; + animation: an-at-keyframe-css-at-rule-that-translates-via-the-transform-property-the-background-by-negative-25-percent-of-its-width-so-that-it-gives-a-nice-border-animation_-We-use-the-translate-property-to-have-a-nice-transition-so-it_s-not-a-jerk-of-a-start-or-stop .75s linear infinite; + // animation-play-state: paused; + translate: -5% 0%; + transition: translate 0.25s ease-out; + animation-play-state: running; + transition-duration: 0.75s; + translate: 0% 0%; +} + +.current-prize::after { + content: ""; + position: absolute; + inset: 4px; + border-top-left-radius: 20px; + border-bottom-right-radius: 20px; + z-index: -1; +} + +@keyframes an-at-keyframe-css-at-rule-that-translates-via-the-transform-property-the-background-by-negative-25-percent-of-its-width-so-that-it-gives-a-nice-border-animation_-We-use-the-translate-property-to-have-a-nice-transition-so-it_s-not-a-jerk-of-a-start-or-stop { + to { + transform: translateX(-25%); + } +} + +@-webkit-keyframes slide-right { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + + 100% { + -webkit-transform: translateX(30px); + transform: translateX(30px); + } +} + +@keyframes slide-right { + 0% { + -webkit-transform: translateX(-200px); + transform: translateX(-200px); + } + + 100% { + -webkit-transform: translateX(0); + transform: translateX(0); + } +} + +@-webkit-keyframes slide-left { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + + 100% { + -webkit-transform: translateX(-100px); + transform: translateX(-100px); + } +} + +@keyframes slide-left { + 0% { + -webkit-transform: translateX(0); + transform: translateX(0); + } + + 100% { + -webkit-transform: translateX(-400px); + transform: translateX(-400px); + } +} + +@-webkit-keyframes show-operate { + 0% { + opacity: 0; + } + + 99% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes show-operate { + 0% { + opacity: 0; + } + + 99% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} \ No newline at end of file diff --git a/src/views/Home/PrizeList.vue b/src/views/Home/components/PrizeList/index.vue similarity index 80% rename from src/views/Home/PrizeList.vue rename to src/views/Home/components/PrizeList/index.vue index ea9cfcd..ac745aa 100644 --- a/src/views/Home/PrizeList.vue +++ b/src/views/Home/components/PrizeList/index.vue @@ -1,17 +1,17 @@ From 5b2c1df4018910d237da5541040396da52703fd8 Mon Sep 17 00:00:00 2001 From: LOG1997 <2694233102@qq.com> Date: Tue, 30 Sep 2025 22:47:36 +0800 Subject: [PATCH 04/16] =?UTF-8?q?refactor:=20=E6=8F=90=E5=8F=96=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 3 - src/App.vue | 6 +- src/components.d.ts | 1 - src/components/StarsBackground/index.vue | 71 -- .../Home/components/HeaderTitle/index.vue | 89 +++ .../Home/components/OptionsButton/index.scss | 335 +++++++++ .../Home/components/OptionsButton/index.vue | 79 +++ src/views/Home/components/PrizeList/index.vue | 2 +- .../Home/components/StarsBackground/index.vue | 71 ++ src/views/Home/index.scss | 341 --------- src/views/Home/index.vue | 669 +----------------- src/views/Home/type.ts | 12 + src/views/Home/useViewModel.ts | 606 ++++++++++++++++ src/views/Home/{index.ts => util.ts} | 0 14 files changed, 1219 insertions(+), 1066 deletions(-) delete mode 100644 src/components/StarsBackground/index.vue create mode 100644 src/views/Home/components/HeaderTitle/index.vue create mode 100644 src/views/Home/components/OptionsButton/index.scss create mode 100644 src/views/Home/components/OptionsButton/index.vue create mode 100644 src/views/Home/components/StarsBackground/index.vue delete mode 100644 src/views/Home/index.scss create mode 100644 src/views/Home/type.ts create mode 100644 src/views/Home/useViewModel.ts rename src/views/Home/{index.ts => util.ts} (100%) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7aa8dd3..c9cda78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,9 +29,6 @@ importers: localforage: specifier: ^1.10.0 version: 1.10.0 - lodash-es: - specifier: ^4.17.21 - version: 4.17.21 markdown-it: specifier: ^14.1.0 version: 14.1.0 diff --git a/src/App.vue b/src/App.vue index 0aee9b9..8512bab 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,10 +1,10 @@ - - - - - - - - - - diff --git a/src/views/Home/components/HeaderTitle/index.vue b/src/views/Home/components/HeaderTitle/index.vue new file mode 100644 index 0000000..03e6a4d --- /dev/null +++ b/src/views/Home/components/HeaderTitle/index.vue @@ -0,0 +1,89 @@ + + + + + + {{ topTitle }} + + + + {{ t('button.noInfoAndImport') }} + + + {{ t('button.useDefault') }} + + + + + + diff --git a/src/views/Home/components/OptionsButton/index.scss b/src/views/Home/components/OptionsButton/index.scss new file mode 100644 index 0000000..96d3f8f --- /dev/null +++ b/src/views/Home/components/OptionsButton/index.scss @@ -0,0 +1,335 @@ +#menu { + position: absolute; + z-index: 100; + width: 100%; + bottom: 50px; + text-align: center; + margin: 0 auto; + font-size: 32px; + + .start { + // 居中 + display: flex; + justify-content: center; + } + + .btn-stars { + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + width: 13rem; + overflow: hidden; + height: 3rem; + background-size: 300% 300%; + backdrop-filter: blur(1rem); + border-radius: 5rem; + transition: 0.5s; + animation: gradient_301 5s ease infinite; + border: double 4px transparent; + background-image: linear-gradient(#212121, #212121), linear-gradient(137.48deg, #ffdb3b 10%, #FE53BB 45%, #8F51EA 67%, #0044ff 87%); + background-origin: border-box; + background-clip: content-box, border-box; + -webkit-animation: pulsate-fwd 1.2s ease-in-out infinite both; + animation: pulsate-fwd 1.2s ease-in-out infinite both; + + &:hover #container-stars { + z-index: 1; + background-color: #212121; + } + + &:hover { + transform: scale(1.1) + } + + &:active { + border: double 4px #FE53BB; + background-origin: border-box; + background-clip: content-box, border-box; + animation: none; + } + + &:active .circle { + background: #FE53BB; + } + + strong { + z-index: 2; + font-family: 'Avalors Personal Use'; + font-size: 12px; + letter-spacing: 5px; + color: #FFFFFF; + text-shadow: 0 0 4px white; + } + + #container-stars { + position: absolute; + z-index: -1; + width: 100%; + height: 100%; + overflow: hidden; + transition: 0.5s; + backdrop-filter: blur(1rem); + border-radius: 5rem; + + #stars { + position: relative; + background: transparent; + width: 200rem; + height: 200rem; + + &::after { + content: ""; + position: absolute; + top: -10rem; + left: -100rem; + width: 100%; + height: 100%; + animation: animStarRotate 90s linear infinite; + } + + &::after { + background-image: radial-gradient(#ffffff 1px, transparent 1%); + background-size: 50px 50px; + } + + &::before { + content: ""; + position: absolute; + top: 0; + left: -50%; + width: 170%; + height: 500%; + animation: animStar 60s linear infinite; + } + + &::before { + background-image: radial-gradient(#ffffff 1px, transparent 1%); + background-size: 50px 50px; + opacity: 0.5; + } + } + + + } + + #glow { + position: absolute; + display: flex; + width: 12rem; + + .circle { + width: 100%; + height: 30px; + filter: blur(2rem); + animation: pulse_3011 4s infinite; + z-index: -1; + + &:nth-of-type(1) { + background: rgba(254, 83, 186, 0.636); + } + + &:nth-of-type(2) { + background: rgba(142, 81, 234, 0.704); + } + } + } + } + + .btn-neon { + --glow-color: rgb(217, 176, 255); + --glow-spread-color: rgba(191, 123, 255, 0.781); + --enhanced-glow-color: rgb(231, 206, 255); + --btn-color: rgb(100, 61, 136); + -webkit-animation: pulsate-fwd 0.9s ease-in-out infinite both; + animation: pulsate-fwd 0.9s ease-in-out infinite both; + cursor: pointer; + border: .25em solid var(--glow-color); + padding: 1em 3em; + color: var(--glow-color); + font-size: 15px; + font-weight: bold; + background-color: var(--btn-color); + border-radius: 1em; + outline: none; + box-shadow: 0 0 1em .25em var(--glow-color), + 0 0 4em 1em var(--glow-spread-color), + inset 0 0 .75em .25em var(--glow-color); + text-shadow: 0 0 .5em var(--glow-color); + position: relative; + transition: all 0.3s; + -webkit-animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both; + animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both; + + &::after { + pointer-events: none; + content: ""; + position: absolute; + top: 120%; + left: 0; + height: 100%; + width: 100%; + background-color: var(--glow-spread-color); + filter: blur(2em); + opacity: .7; + transform: perspective(1.5em) rotateX(35deg) scale(1, .6); + } + + &:hover { + color: var(--btn-color); + background-color: var(--glow-color); + box-shadow: 0 0 1em .25em var(--glow-color), + 0 0 4em 2em var(--glow-spread-color), + inset 0 0 .75em .25em var(--glow-color); + } + + &:active { + box-shadow: 0 0 0.6em .25em var(--glow-color), + 0 0 2.5em 2em var(--glow-spread-color), + inset 0 0 .5em .25em var(--glow-color); + } + } + + .btn-cancel { + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + width: 13rem; + overflow: hidden; + height: 3rem; + background-size: 300% 300%; + backdrop-filter: blur(1rem); + border-radius: 5rem; + transition: 0.5s; + animation: gradient_301 5s ease infinite; + border: double 4px transparent; + background-image: linear-gradient(#212121, #212121), linear-gradient(137.48deg, #ffdb3b 10%, #FE53BB 45%, #8F51EA 67%, #0044ff 87%); + background-origin: border-box; + background-clip: content-box, border-box; + } +} + +@keyframes animStar { + from { + transform: translateY(0); + } + + to { + transform: translateY(-135rem); + } +} + +@keyframes animStarRotate { + from { + transform: rotate(360deg); + } + + to { + transform: rotate(0); + } +} + +@keyframes gradient_301 { + 0% { + background-position: 0% 50%; + } + + 50% { + background-position: 100% 50%; + } + + 100% { + background-position: 0% 50%; + } +} + +@keyframes pulse_3011 { + 0% { + transform: scale(0.75); + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgba(0, 0, 0, 0); + } + + 100% { + transform: scale(0.75); + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); + } +} + +// 按钮动画 +@-webkit-keyframes pulsate-fwd { + 0% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 50% { + -webkit-transform: scale(1.1); + transform: scale(1.1); + } + + 100% { + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@keyframes pulsate-fwd { + 0% { + -webkit-transform: scale(1); + transform: scale(1); + } + + 50% { + -webkit-transform: scale(1.2); + transform: scale(1.2); + } + + 100% { + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@-webkit-keyframes tracking-in-expand-fwd { + 0% { + letter-spacing: -0.5em; + -webkit-transform: translateZ(-700px); + transform: translateZ(-700px); + opacity: 0; + } + + 40% { + opacity: 0.6; + } + + 100% { + -webkit-transform: translateZ(0); + transform: translateZ(0); + opacity: 1; + } +} + +@keyframes tracking-in-expand-fwd { + 0% { + letter-spacing: -0.5em; + -webkit-transform: translateZ(-700px); + transform: translateZ(-700px); + opacity: 0; + } + + 40% { + opacity: 0.6; + } + + 100% { + -webkit-transform: translateZ(0); + transform: translateZ(0); + opacity: 1; + } +} \ No newline at end of file diff --git a/src/views/Home/components/OptionsButton/index.vue b/src/views/Home/components/OptionsButton/index.vue new file mode 100644 index 0000000..a220239 --- /dev/null +++ b/src/views/Home/components/OptionsButton/index.vue @@ -0,0 +1,79 @@ + + + + + + {{ t('button.enterLottery') }} + + + + + {{ t('button.start') }} + + + + + + + + + + + + + {{ t('button.selectLucky') }} + + + + + + {{ t('button.continue') }} + + + + + + + + + + + + + + {{ t('button.cancel') }} + + + + + + + + + + + + + + + diff --git a/src/views/Home/components/PrizeList/index.vue b/src/views/Home/components/PrizeList/index.vue index ac745aa..6a15b1b 100644 --- a/src/views/Home/components/PrizeList/index.vue +++ b/src/views/Home/components/PrizeList/index.vue @@ -318,5 +318,5 @@ onMounted(() => { diff --git a/src/views/Home/components/StarsBackground/index.vue b/src/views/Home/components/StarsBackground/index.vue new file mode 100644 index 0000000..3da65bc --- /dev/null +++ b/src/views/Home/components/StarsBackground/index.vue @@ -0,0 +1,71 @@ + + + + + + + + + + diff --git a/src/views/Home/index.scss b/src/views/Home/index.scss deleted file mode 100644 index b882304..0000000 --- a/src/views/Home/index.scss +++ /dev/null @@ -1,341 +0,0 @@ -#menu { - position: absolute; - z-index: 100; - width: 100%; - bottom: 50px; - text-align: center; - margin: 0 auto; - font-size: 32px; -} - -.header-title { - -webkit-animation: tracking-in-expand-fwd 0.8s cubic-bezier(0.215, 0.610, 0.355, 1.000) both; - animation: tracking-in-expand-fwd 0.8s cubic-bezier(0.215, 0.610, 0.355, 1.000) both; -} - -.start { - // 居中 - display: flex; - justify-content: center; -} - -.btn-start { - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - width: 13rem; - overflow: hidden; - height: 3rem; - background-size: 300% 300%; - backdrop-filter: blur(1rem); - border-radius: 5rem; - transition: 0.5s; - animation: gradient_301 5s ease infinite; - border: double 4px transparent; - background-image: linear-gradient(#212121, #212121), linear-gradient(137.48deg, #ffdb3b 10%, #FE53BB 45%, #8F51EA 67%, #0044ff 87%); - background-origin: border-box; - background-clip: content-box, border-box; - -webkit-animation: pulsate-fwd 1.2s ease-in-out infinite both; - animation: pulsate-fwd 1.2s ease-in-out infinite both; -} - -.btn-cancel { - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - width: 13rem; - overflow: hidden; - height: 3rem; - background-size: 300% 300%; - backdrop-filter: blur(1rem); - border-radius: 5rem; - transition: 0.5s; - animation: gradient_301 5s ease infinite; - border: double 4px transparent; - background-image: linear-gradient(#212121, #212121), linear-gradient(137.48deg, #ffdb3b 10%, #FE53BB 45%, #8F51EA 67%, #0044ff 87%); - background-origin: border-box; - background-clip: content-box, border-box; -} - -#container-stars { - position: absolute; - z-index: -1; - width: 100%; - height: 100%; - overflow: hidden; - transition: 0.5s; - backdrop-filter: blur(1rem); - border-radius: 5rem; -} - -strong { - z-index: 2; - font-family: 'Avalors Personal Use'; - font-size: 12px; - letter-spacing: 5px; - color: #FFFFFF; - text-shadow: 0 0 4px white; -} - -#glow { - position: absolute; - display: flex; - width: 12rem; -} - -.circle { - width: 100%; - height: 30px; - filter: blur(2rem); - animation: pulse_3011 4s infinite; - z-index: -1; -} - -.circle:nth-of-type(1) { - background: rgba(254, 83, 186, 0.636); -} - -.circle:nth-of-type(2) { - background: rgba(142, 81, 234, 0.704); -} - -.btn-start:hover #container-stars { - z-index: 1; - background-color: #212121; -} - -.btn-start:hover { - transform: scale(1.1) -} - -.btn-start:active { - border: double 4px #FE53BB; - background-origin: border-box; - background-clip: content-box, border-box; - animation: none; -} - -.btn-start:active .circle { - background: #FE53BB; -} - -#stars { - position: relative; - background: transparent; - width: 200rem; - height: 200rem; -} - -#stars::after { - content: ""; - position: absolute; - top: -10rem; - left: -100rem; - width: 100%; - height: 100%; - animation: animStarRotate 90s linear infinite; -} - -#stars::after { - background-image: radial-gradient(#ffffff 1px, transparent 1%); - background-size: 50px 50px; -} - -#stars::before { - content: ""; - position: absolute; - top: 0; - left: -50%; - width: 170%; - height: 500%; - animation: animStar 60s linear infinite; -} - -#stars::before { - background-image: radial-gradient(#ffffff 1px, transparent 1%); - background-size: 50px 50px; - opacity: 0.5; -} - -@keyframes animStar { - from { - transform: translateY(0); - } - - to { - transform: translateY(-135rem); - } -} - -@keyframes animStarRotate { - from { - transform: rotate(360deg); - } - - to { - transform: rotate(0); - } -} - -@keyframes gradient_301 { - 0% { - background-position: 0% 50%; - } - - 50% { - background-position: 100% 50%; - } - - 100% { - background-position: 0% 50%; - } -} - -@keyframes pulse_3011 { - 0% { - transform: scale(0.75); - box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.7); - } - - 70% { - transform: scale(1); - box-shadow: 0 0 0 10px rgba(0, 0, 0, 0); - } - - 100% { - transform: scale(0.75); - box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); - } -} - -.btn-end { - -webkit-animation: pulsate-fwd 0.9s ease-in-out infinite both; - animation: pulsate-fwd 0.9s ease-in-out infinite both; - cursor: pointer; -} - -.btn-end { - --glow-color: rgb(217, 176, 255); - --glow-spread-color: rgba(191, 123, 255, 0.781); - --enhanced-glow-color: rgb(231, 206, 255); - --btn-color: rgb(100, 61, 136); - border: .25em solid var(--glow-color); - padding: 1em 3em; - color: var(--glow-color); - font-size: 15px; - font-weight: bold; - background-color: var(--btn-color); - border-radius: 1em; - outline: none; - box-shadow: 0 0 1em .25em var(--glow-color), - 0 0 4em 1em var(--glow-spread-color), - inset 0 0 .75em .25em var(--glow-color); - text-shadow: 0 0 .5em var(--glow-color); - position: relative; - transition: all 0.3s; - -webkit-animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both; - animation: swing-in-top-fwd 0.5s cubic-bezier(0.175, 0.885, 0.320, 1.275) both; -} - -.btn-end::after { - pointer-events: none; - content: ""; - position: absolute; - top: 120%; - left: 0; - height: 100%; - width: 100%; - background-color: var(--glow-spread-color); - filter: blur(2em); - opacity: .7; - transform: perspective(1.5em) rotateX(35deg) scale(1, .6); -} - -.btn-end:hover { - color: var(--btn-color); - background-color: var(--glow-color); - box-shadow: 0 0 1em .25em var(--glow-color), - 0 0 4em 2em var(--glow-spread-color), - inset 0 0 .75em .25em var(--glow-color); -} - -.btn-end:active { - box-shadow: 0 0 0.6em .25em var(--glow-color), - 0 0 2.5em 2em var(--glow-spread-color), - inset 0 0 .5em .25em var(--glow-color); -} - -// 按钮动画 -@-webkit-keyframes pulsate-fwd { - 0% { - -webkit-transform: scale(1); - transform: scale(1); - } - - 50% { - -webkit-transform: scale(1.1); - transform: scale(1.1); - } - - 100% { - -webkit-transform: scale(1); - transform: scale(1); - } -} - -@keyframes pulsate-fwd { - 0% { - -webkit-transform: scale(1); - transform: scale(1); - } - - 50% { - -webkit-transform: scale(1.2); - transform: scale(1.2); - } - - 100% { - -webkit-transform: scale(1); - transform: scale(1); - } -} - -@-webkit-keyframes tracking-in-expand-fwd { - 0% { - letter-spacing: -0.5em; - -webkit-transform: translateZ(-700px); - transform: translateZ(-700px); - opacity: 0; - } - - 40% { - opacity: 0.6; - } - - 100% { - -webkit-transform: translateZ(0); - transform: translateZ(0); - opacity: 1; - } -} - -@keyframes tracking-in-expand-fwd { - 0% { - letter-spacing: -0.5em; - -webkit-transform: translateZ(-700px); - transform: translateZ(-700px); - opacity: 0; - } - - 40% { - opacity: 0.6; - } - - 100% { - -webkit-transform: translateZ(0); - transform: translateZ(0); - opacity: 1; - } -} \ No newline at end of file diff --git a/src/views/Home/index.vue b/src/views/Home/index.vue index b3f9a93..70f295d 100644 --- a/src/views/Home/index.vue +++ b/src/views/Home/index.vue @@ -1,665 +1,42 @@ - - - {{ topTitle }} - - - - {{ t('button.noInfoAndImport') }} - - - {{ t('button.useDefault') }} - - - + - - - - {{ t('button.enterLottery') }} - - - - - {{ t('button.start') }} - - - - - - - - - - - - - {{ t('button.selectLucky') }} - - - - - - {{ t('button.continue') }} - - - - - - - - - - - - - - {{ t('button.cancel') }} - - - - - - - - - - - - - + diff --git a/src/views/Home/type.ts b/src/views/Home/type.ts new file mode 100644 index 0000000..1a2f9c3 --- /dev/null +++ b/src/views/Home/type.ts @@ -0,0 +1,12 @@ +export enum LotteryStatus { + init = 0, + ready = 1, + running = 2, + end = 3, +} +export interface TargetType { + grid: any[] + helix: any[] + table: any[] + sphere: any[] +} diff --git a/src/views/Home/useViewModel.ts b/src/views/Home/useViewModel.ts new file mode 100644 index 0000000..cebfdf3 --- /dev/null +++ b/src/views/Home/useViewModel.ts @@ -0,0 +1,606 @@ +import type { Material, Object3D } from 'three' +import type { TargetType } from './type' +import type { IPersonConfig } from '@/types/storeType' +import * as TWEEN from '@tweenjs/tween.js' +import { storeToRefs } from 'pinia' +import { PerspectiveCamera, Scene } from 'three' +import { CSS3DObject, CSS3DRenderer } from 'three-css3d' +import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js' +import { nextTick, onMounted, onUnmounted, ref } from 'vue' +import { useToast } from 'vue-toast-notification' +import { useElementPosition, useElementStyle } from '@/hooks/useElement' +import i18n from '@/locales/i18n' +import useStore from '@/store' +import { selectCard } from '@/utils' +import { rgba } from '@/utils/color' +import { LotteryStatus } from './type' +import { confettiFire, createSphereVertices, createTableVertices, initTableData } from './util' + +export function useViewModel() { + const toast = useToast() + // store里面存储的值 + const { personConfig, globalConfig, prizeConfig } = useStore() + const { + getAllPersonList: allPersonList, + getNotPersonList: notPersonList, + getNotThisPrizePersonList: notThisPrizePersonList, + } = storeToRefs(personConfig) + const { getCurrentPrize: currentPrize } = storeToRefs(prizeConfig) + const { getCardColor: cardColor, getPatterColor: patternColor, getPatternList: patternList, getTextColor: textColor, getLuckyColor: luckyColor, getCardSize: cardSize, getTextSize: textSize, getRowCount: rowCount, getIsShowAvatar: isShowAvatar } = storeToRefs(globalConfig) + // three初始值 + const ballRotationY = ref(0) + const containerRef = ref() + const canOperate = ref(true) + const cameraZ = ref(3000) + const scene = ref() + const camera = ref() + const renderer = ref() + const controls = ref() + const objects = ref([]) + const targets: TargetType = { + grid: [], + helix: [], + table: [], + sphere: [], + } + // 页面数据初始值 + const currentStatus = ref(LotteryStatus.init) // 0为初始状态, 1为抽奖准备状态,2为抽奖中状态,3为抽奖结束状态 + const tableData = ref([]) + const luckyTargets = ref([]) + const luckyCardList = ref([]) + const luckyCount = ref(10) + const personPool = ref([]) + const intervalTimer = ref(null) + + function init() { + const felidView = 40 + const width = window.innerWidth + const height = window.innerHeight + const aspect = width / height + const nearPlane = 1 + const farPlane = 10000 + const WebGLoutput = containerRef.value + + scene.value = new Scene() + camera.value = new PerspectiveCamera(felidView, aspect, nearPlane, farPlane) + camera.value.position.z = cameraZ.value + renderer.value = new CSS3DRenderer() + renderer.value.setSize(width, height * 0.9) + renderer.value.domElement.style.position = 'absolute' + // 垂直居中 + renderer.value.domElement.style.paddingTop = '50px' + renderer.value.domElement.style.top = '50%' + renderer.value.domElement.style.left = '50%' + renderer.value.domElement.style.transform = 'translate(-50%, -50%)' + WebGLoutput!.appendChild(renderer.value.domElement) + + controls.value = new TrackballControls(camera.value, renderer.value.domElement) + controls.value.rotateSpeed = 1 + controls.value.staticMoving = true + controls.value.minDistance = 500 + controls.value.maxDistance = 6000 + controls.value.addEventListener('change', render) + + const tableLen = tableData.value.length + for (let i = 0; i < tableLen; i++) { + let element = document.createElement('div') + element.className = 'element-card' + + const number = document.createElement('div') + number.className = 'card-id' + number.textContent = tableData.value[i].uid + if (isShowAvatar.value) + number.style.display = 'none' + element.appendChild(number) + + const symbol = document.createElement('div') + symbol.className = 'card-name' + symbol.textContent = tableData.value[i].name + if (isShowAvatar.value) + symbol.className = 'card-name card-avatar-name' + element.appendChild(symbol) + + const detail = document.createElement('div') + detail.className = 'card-detail' + detail.innerHTML = `${tableData.value[i].department}${tableData.value[i].identity}` + if (isShowAvatar.value) + detail.style.display = 'none' + element.appendChild(detail) + + const avatar = document.createElement('img') + avatar.className = 'card-avatar' + avatar.src = tableData.value[i].avatar + avatar.alt = 'avatar' + avatar.style.width = '140px' + avatar.style.height = '140px' + if (!isShowAvatar.value) + avatar.style.display = 'none' + element.appendChild(avatar) + + element = useElementStyle(element, tableData.value[i], i, patternList.value, patternColor.value, cardColor.value, cardSize.value, textSize.value) + const object = new CSS3DObject(element) + object.position.x = Math.random() * 4000 - 2000 + object.position.y = Math.random() * 4000 - 2000 + object.position.z = Math.random() * 4000 - 2000 + scene.value.add(object) + + objects.value.push(object) + } + // 创建横铺的界面 + const tableVertices = createTableVertices({ tableData: tableData.value, rowCount: rowCount.value, cardSize: cardSize.value }) + targets.table = tableVertices + // 创建球体 + const sphereVertices = createSphereVertices({ objectsLength: objects.value.length }) + targets.sphere = sphereVertices + window.addEventListener('resize', onWindowResize, false) + transform(targets.table, 1000) + render() + } + function render() { + if (renderer.value) { + renderer.value.render(scene.value, camera.value) + } + } + /** + * @description: 位置变换 + * @param targets 目标位置 + * @param duration 持续时间 + */ + function transform(targets: any[], duration: number) { + TWEEN.removeAll() + if (intervalTimer.value) { + clearInterval(intervalTimer.value) + intervalTimer.value = null + randomBallData('sphere') + } + + return new Promise((resolve) => { + const objLength = objects.value.length + for (let i = 0; i < objLength; ++i) { + const object = objects.value[i] + const target = targets[i] + // console.log('target', i, target, targets) + new TWEEN.Tween(object.position) + .to({ x: target.position.x, y: target.position.y, z: target.position.z }, Math.random() * duration + duration) + .easing(TWEEN.Easing.Exponential.InOut) + .start() + + new TWEEN.Tween(object.rotation) + .to({ x: target.rotation.x, y: target.rotation.y, z: target.rotation.z }, Math.random() * duration + duration) + .easing(TWEEN.Easing.Exponential.InOut) + .start() + .onComplete(() => { + if (luckyCardList.value.length) { + luckyCardList.value.forEach((cardIndex: any) => { + const item = objects.value[cardIndex] + useElementStyle(item.element, {} as any, i, patternList.value, patternColor.value, cardColor.value, cardSize.value, textSize.value, 'sphere') + }) + } + luckyTargets.value = [] + luckyCardList.value = [] + canOperate.value = true + }) + } + + // 这个补间用来在位置与旋转补间同步执行,通过onUpdate在每次更新数据后渲染scene和camera + new TWEEN.Tween({}) + .to({}, duration * 2) + .onUpdate(render) + .start() + .onComplete(() => { + canOperate.value = true + resolve('') + }) + }) + } + /** + * @description: 窗口大小改变时重新设置渲染器的大小 + */ + function onWindowResize() { + camera.value.aspect = window.innerWidth / window.innerHeight + camera.value.updateProjectionMatrix() + + renderer.value.setSize(window.innerWidth, window.innerHeight) + render() + } + + /** + * [animation update all tween && controls] + */ + function animation() { + TWEEN.update() + if (controls.value) { + controls.value.update() + } + // 设置自动旋转 + // 设置相机位置 + requestAnimationFrame(animation) + } + /** + * @description: 旋转的动画 + * @param rotateY 绕y轴旋转圈数 + * @param duration 持续时间,单位秒 + */ + function rollBall(rotateY: number, duration: number) { + TWEEN.removeAll() + + return new Promise((resolve) => { + scene.value.rotation.y = 0 + ballRotationY.value = Math.PI * rotateY * 1000 + const rotateObj = new TWEEN.Tween(scene.value.rotation) + rotateObj + .to( + { + // x: Math.PI * rotateX * 1000, + x: 0, + y: ballRotationY.value, + // z: Math.PI * rotateZ * 1000 + z: 0, + }, + duration * 1000, + ) + .onUpdate(render) + .start() + .onStop(() => { + resolve('') + }) + .onComplete(() => { + resolve('') + }) + }) + } + /** + * @description: 视野转回正面 + */ + function resetCamera() { + new TWEEN.Tween(camera.value.position) + .to( + { + x: 0, + y: 0, + z: 3000, + }, + 1000, + ) + .onUpdate(render) + .start() + .onComplete(() => { + new TWEEN.Tween(camera.value.rotation) + .to( + { + x: 0, + y: 0, + z: 0, + }, + 1000, + ) + .onUpdate(render) + .start() + .onComplete(() => { + canOperate.value = true + // camera.value.lookAt(scene.value.position) + camera.value.position.y = 0 + camera.value.position.x = 0 + camera.value.position.z = 3000 + camera.value.rotation.x = 0 + camera.value.rotation.y = 0 + camera.value.rotation.z = -0 + controls.value.reset() + }) + }) + } + + /** + * @description: 开始抽奖,由横铺变换为球体(或其他图形) + * @returns 随机抽取球数据 + */ + /// description 进入抽奖准备状态 + async function enterLottery() { + if (!canOperate.value) { + return + } + if (!intervalTimer.value) { + randomBallData() + } + if (patternList.value.length) { + for (let i = 0; i < patternList.value.length; i++) { + if (i < rowCount.value * 7) { + objects.value[patternList.value[i] - 1].element.style.backgroundColor = rgba(cardColor.value, Math.random() * 0.5 + 0.25) + } + } + } + canOperate.value = false + await transform(targets.sphere, 1000) + currentStatus.value = LotteryStatus.ready + rollBall(0.1, 2000) + } + /** + * @description 开始抽奖 + */ + function startLottery() { + if (!canOperate.value) { + return + } + // 验证是否已抽完全部奖项 + if (currentPrize.value.isUsed || !currentPrize.value) { + toast.open({ + message: i18n.global.t('error.personIsAllDone'), + type: 'warning', + position: 'top-right', + duration: 10000, + }) + + return + } + personPool.value = currentPrize.value.isAll ? notThisPrizePersonList.value : notPersonList.value + // 验证抽奖人数是否还够 + if (personPool.value.length < currentPrize.value.count - currentPrize.value.isUsedCount) { + toast.open({ + message: i18n.global.t('error.personNotEnough'), + type: 'warning', + position: 'top-right', + duration: 10000, + }) + + return + } + luckyCount.value = 10 + // 自定义抽奖个数 + + let leftover = currentPrize.value.count - currentPrize.value.isUsedCount + const customCount = currentPrize.value.separateCount + if (customCount && customCount.enable && customCount.countList.length > 0) { + for (let i = 0; i < customCount.countList.length; i++) { + if (customCount.countList[i].isUsedCount < customCount.countList[i].count) { + leftover = customCount.countList[i].count - customCount.countList[i].isUsedCount + break + } + } + } + luckyCount.value = leftover < luckyCount.value ? leftover : luckyCount.value + for (let i = 0; i < luckyCount.value; i++) { + if (personPool.value.length > 0) { + // 解决随机元素概率过于不均等问题 + const randomIndex = Math.floor(Math.random() * (personPool.value.length - 1)) + luckyTargets.value.push(personPool.value[randomIndex]) + personPool.value.splice(randomIndex, 1) + } + } + + toast.open({ + // message: `现在抽取${currentPrize.value.name} ${leftover}人`, + message: i18n.global.t('error.startDraw', { count: currentPrize.value.name, leftover }), + type: 'default', + position: 'top-right', + duration: 8000, + }) + currentStatus.value = LotteryStatus.running + rollBall(10, 3000) + } + /** + * @description: 停止抽奖,抽出幸运人 + */ + async function stopLottery() { + if (!canOperate.value) { + return + } + // clearInterval(intervalTimer.value) + // intervalTimer.value = null + canOperate.value = false + rollBall(0, 1) + + const windowSize = { width: window.innerWidth, height: window.innerHeight } + luckyTargets.value.forEach((person: IPersonConfig, index: number) => { + const cardIndex = selectCard(luckyCardList.value, tableData.value.length, person.id) + luckyCardList.value.push(cardIndex) + const totalLuckyCount = luckyTargets.value.length + const item = objects.value[cardIndex] + const { xTable, yTable } = useElementPosition(item, rowCount.value, totalLuckyCount, { width: cardSize.value.width * 2, height: cardSize.value.height * 2 }, windowSize, index) + new TWEEN.Tween(item.position) + .to({ + x: xTable, + y: yTable, + z: 1000, + }, 1200) + .easing(TWEEN.Easing.Exponential.InOut) + .onStart(() => { + item.element = useElementStyle(item.element, person, cardIndex, patternList.value, patternColor.value, luckyColor.value, { width: cardSize.value.width * 2, height: cardSize.value.height * 2 }, textSize.value * 2, 'lucky') + }) + .start() + .onComplete(() => { + canOperate.value = true + currentStatus.value = LotteryStatus.end + }) + new TWEEN.Tween(item.rotation) + .to({ + x: 0, + y: 0, + z: 0, + }, 900) + .easing(TWEEN.Easing.Exponential.InOut) + .start() + .onComplete(() => { + confettiFire() + resetCamera() + }) + }) + } + /** + * @description: 继续,意味着这抽奖作数,计入数据库 + */ + async function continueLottery() { + if (!canOperate.value) { + return + } + + const customCount = currentPrize.value.separateCount + if (customCount && customCount.enable && customCount.countList.length > 0) { + for (let i = 0; i < customCount.countList.length; i++) { + if (customCount.countList[i].isUsedCount < customCount.countList[i].count) { + customCount.countList[i].isUsedCount += luckyCount.value + break + } + } + } + currentPrize.value.isUsedCount += luckyCount.value + luckyCount.value = 0 + if (currentPrize.value.isUsedCount >= currentPrize.value.count) { + currentPrize.value.isUsed = true + currentPrize.value.isUsedCount = currentPrize.value.count + } + personConfig.addAlreadyPersonList(luckyTargets.value, currentPrize.value) + prizeConfig.updatePrizeConfig(currentPrize.value) + await enterLottery() + } + /** + * @description: 放弃本次抽奖,回到初始状态 + */ + function quitLottery() { + enterLottery() + currentStatus.value = LotteryStatus.init + } + + /** + * @description: 随机替换卡片中的数据(不改变原有的值,只是显示) + * @param {string} mod 模式 + */ + function randomBallData(mod: 'default' | 'lucky' | 'sphere' = 'default') { + // 两秒执行一次 + intervalTimer.value = setInterval(() => { + // 产生随机数数组 + const indexLength = 4 + const cardRandomIndexArr: number[] = [] + const personRandomIndexArr: number[] = [] + for (let i = 0; i < indexLength; i++) { + // 解决随机元素概率过于不均等问题 + const randomCardIndex = Math.floor(Math.random() * (tableData.value.length - 1)) + const randomPersonIndex = Math.floor(Math.random() * (allPersonList.value.length - 1)) + if (luckyCardList.value.includes(randomCardIndex)) { + continue + } + cardRandomIndexArr.push(randomCardIndex) + personRandomIndexArr.push(randomPersonIndex) + } + for (let i = 0; i < cardRandomIndexArr.length; i++) { + if (!objects.value[cardRandomIndexArr[i]]) { + continue + } + objects.value[cardRandomIndexArr[i]].element = useElementStyle(objects.value[cardRandomIndexArr[i]].element, allPersonList.value[personRandomIndexArr[i]], cardRandomIndexArr[i], patternList.value, patternColor.value, cardColor.value, { width: cardSize.value.width, height: cardSize.value.height }, textSize.value, mod, 'change') + } + }, 200) + } + /** + * @description: 键盘监听,快捷键操作 + */ + function listenKeyboard(e: any) { + if ((e.keyCode !== 32 || e.keyCode !== 27) && !canOperate.value) { + return + } + if (e.keyCode === 27 && currentStatus.value === LotteryStatus.running) { + quitLottery() + } + if (e.keyCode !== 32) { + return + } + switch (currentStatus.value) { + case LotteryStatus.init: + enterLottery() + break + case LotteryStatus.ready: + startLottery() + break + case LotteryStatus.running: + stopLottery() + break + case LotteryStatus.end: + continueLottery() + break + default: + break + } + } + /** + * @description: 清理资源,避免内存溢出 + */ + function cleanup() { + clearInterval(intervalTimer.value) + intervalTimer.value = null + if (scene.value) { + scene.value.traverse((object: Object3D) => { + if ((object as any).material) { + if (Array.isArray((object as any).material)) { + (object as any).material.forEach((material: Material) => { + material.dispose() + }) + } + else { + (object as any).material.dispose() + } + } + if ((object as any).geometry) { + (object as any).geometry.dispose() + } + if ((object as any).texture) { + (object as any).texture.dispose() + } + }) + scene.value.clear() + } + + if (objects.value) { + objects.value.forEach((object) => { + if (object.element) { + object.element.remove() + } + }) + objects.value = [] + } + + if (controls.value) { + controls.value.removeEventListener('change') + controls.value.dispose() + } + // 移除所有事件监听 + window.removeEventListener('resize', onWindowResize) + scene.value = null + camera.value = null + renderer.value = null + controls.value = null + } + /** + * @description: 设置默认人员列表 + */ + function setDefaultPersonList() { + personConfig.setDefaultPersonList() + // 刷新页面 + window.location.reload() + } + onMounted(() => { + tableData.value = initTableData({ allPersonList: allPersonList.value, rowCount: rowCount.value }) + init() + animation() + containerRef.value!.style.color = `${textColor}` + randomBallData() + window.addEventListener('keydown', listenKeyboard) + }) + onUnmounted(() => { + nextTick(() => { + cleanup() + }) + clearInterval(intervalTimer.value) + intervalTimer.value = null + window.removeEventListener('keydown', listenKeyboard) + }) + + return { + setDefaultPersonList, + startLottery, + continueLottery, + quitLottery, + containerRef, + stopLottery, + enterLottery, + tableData, + currentStatus, + } +} diff --git a/src/views/Home/index.ts b/src/views/Home/util.ts similarity index 100% rename from src/views/Home/index.ts rename to src/views/Home/util.ts From 2b920557a1b529ff9c73848ce2cc18a0a93c5063 Mon Sep 17 00:00:00 2001 From: LOG1997 <2694233102@qq.com> Date: Wed, 1 Oct 2025 11:02:33 +0800 Subject: [PATCH 05/16] =?UTF-8?q?refactor:=20=E4=BA=BA=E5=91=98=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=E9=87=8D=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components.d.ts | 1 + src/components/Dialog/index.vue | 56 ++++ src/router/index.ts | 240 +++++++++--------- .../Person/PersonAll/importExcel.worker.ts | 32 +++ src/views/Config/Person/PersonAll/index.vue | 73 ++++++ .../Config/Person/PersonAll/useViewModel.ts | 153 +++++++++++ 6 files changed, 435 insertions(+), 120 deletions(-) create mode 100644 src/components/Dialog/index.vue create mode 100644 src/views/Config/Person/PersonAll/importExcel.worker.ts create mode 100644 src/views/Config/Person/PersonAll/index.vue create mode 100644 src/views/Config/Person/PersonAll/useViewModel.ts diff --git a/src/components.d.ts b/src/components.d.ts index 04d884e..7b0c306 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -9,6 +9,7 @@ export {} declare module 'vue' { export interface GlobalComponents { DaiysuiTable: typeof import('./components/DaiysuiTable/index.vue')['default'] + Dialog: typeof import('./components/Dialog/index.vue')['default'] EditSeparateDialog: typeof import('./components/NumberSeparate/EditSeparateDialog.vue')['default'] HelloWorld: typeof import('./components/HelloWorld.vue')['default'] ImageSync: typeof import('./components/ImageSync/index.vue')['default'] diff --git a/src/components/Dialog/index.vue b/src/components/Dialog/index.vue new file mode 100644 index 0000000..a004158 --- /dev/null +++ b/src/components/Dialog/index.vue @@ -0,0 +1,56 @@ + + + + + + + {{ title }} + + + {{ desc }} + + + + + + {{ cancelText }} + + + {{ submitText }} + + + + + + + + diff --git a/src/router/index.ts b/src/router/index.ts index 1a37f75..ef753a3 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,142 +1,142 @@ +import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router' import Layout from '@/layout/index.vue' import i18n from '@/locales/i18n' import Home from '@/views/Home/index.vue' -import { createRouter, createWebHistory,createWebHashHistory } from 'vue-router' export const configRoutes = { - path: '/log-lottery/config', - name: 'Config', - component: () => import('@/views/Config/index.vue'), - children: [ - { - path: '', - redirect: '/log-lottery/config/person', - }, - { - path: '/log-lottery/config/person', - name: 'PersonConfig', - component: () => import('@/views/Config/Person/PersonConfig.vue'), - meta: { - title: i18n.global.t('sidebar.personConfiguration'), - icon: 'person', - }, - children: [ + path: '/log-lottery/config', + name: 'Config', + component: () => import('@/views/Config/index.vue'), + children: [ { - path: '', - redirect: '/log-lottery/config/person/all', + path: '', + redirect: '/log-lottery/config/person', }, { - path: '/log-lottery/config/person/all', - name: 'AllPersonConfig', - component: () => import('@/views/Config/Person/PersonAll.vue'), - meta: { - title: i18n.global.t('sidebar.personList'), - icon: 'all', - }, + path: '/log-lottery/config/person', + name: 'PersonConfig', + component: () => import('@/views/Config/Person/PersonConfig.vue'), + meta: { + title: i18n.global.t('sidebar.personConfiguration'), + icon: 'person', + }, + children: [ + { + path: '', + redirect: '/log-lottery/config/person/all', + }, + { + path: '/log-lottery/config/person/all', + name: 'AllPersonConfig', + component: () => import('@/views/Config/Person/PersonAll/index.vue'), + meta: { + title: i18n.global.t('sidebar.personList'), + icon: 'all', + }, + }, + { + path: '/log-lottery/config/person/already', + name: 'AlreadyPerson', + component: () => import('@/views/Config/Person/PersonAlready.vue'), + meta: { + title: i18n.global.t('sidebar.winnerList'), + icon: 'already', + }, + }, + // { + // path:'other', + // name:'OtherPersonConfig', + // component:()=>import('@/views/Config/Person/OtherPersonConfig.vue'), + // meta:{ + // title:'其他配置', + // icon:'other' + // } + // } + ], }, { - path: '/log-lottery/config/person/already', - name: 'AlreadyPerson', - component: () => import('@/views/Config/Person/PersonAlready.vue'), - meta: { - title: i18n.global.t('sidebar.winnerList'), - icon: 'already', - }, - }, - // { - // path:'other', - // name:'OtherPersonConfig', - // component:()=>import('@/views/Config/Person/OtherPersonConfig.vue'), - // meta:{ - // title:'其他配置', - // icon:'other' - // } - // } - ], - }, - { - path: '/log-lottery/config/prize', - name: 'PrizeConfig', - component: () => import('@/views/Config/Prize/PrizeConfig.vue'), - meta: { - title: i18n.global.t('sidebar.prizeConfiguration'), - icon: 'prize', - }, - }, - { - path: '/log-lottery/config/global', - name: 'GlobalConfig', - redirect: '/log-lottery/config/global/all', - meta: { - title: i18n.global.t('sidebar.globalSetting'), - icon: 'global', - }, - children: [ - { - path: '/log-lottery/config/global/face', - name: 'FaceConfig', - component: () => import('@/views/Config/Global/FaceConfig.vue'), - meta: { - title: i18n.global.t('sidebar.viewSetting'), - icon: 'face', - }, + path: '/log-lottery/config/prize', + name: 'PrizeConfig', + component: () => import('@/views/Config/Prize/PrizeConfig.vue'), + meta: { + title: i18n.global.t('sidebar.prizeConfiguration'), + icon: 'prize', + }, }, { - path: '/log-lottery/config/global/image', - name: 'ImageConfig', - component: () => import('@/views/Config/Global/ImageConfig.vue'), - meta: { - title: i18n.global.t('sidebar.imagesManagement'), - icon: 'image', - }, + path: '/log-lottery/config/global', + name: 'GlobalConfig', + redirect: '/log-lottery/config/global/all', + meta: { + title: i18n.global.t('sidebar.globalSetting'), + icon: 'global', + }, + children: [ + { + path: '/log-lottery/config/global/face', + name: 'FaceConfig', + component: () => import('@/views/Config/Global/FaceConfig.vue'), + meta: { + title: i18n.global.t('sidebar.viewSetting'), + icon: 'face', + }, + }, + { + path: '/log-lottery/config/global/image', + name: 'ImageConfig', + component: () => import('@/views/Config/Global/ImageConfig.vue'), + meta: { + title: i18n.global.t('sidebar.imagesManagement'), + icon: 'image', + }, + }, + { + path: '/log-lottery/config/global/music', + name: 'MusicConfig', + component: () => import('@/views/Config/Global/MusicConfig.vue'), + meta: { + title: i18n.global.t('sidebar.musicManagement'), + icon: 'music', + }, + }, + ], }, { - path: '/log-lottery/config/global/music', - name: 'MusicConfig', - component: () => import('@/views/Config/Global/MusicConfig.vue'), - meta: { - title: i18n.global.t('sidebar.musicManagement'), - icon: 'music', - }, + path: '/log-lottery/config/readme', + name: 'Readme', + component: () => import('@/views/Config/Readme/index.vue'), + meta: { + title: i18n.global.t('sidebar.operatingInstructions'), + icon: 'readme', + }, }, - ], - }, - { - path: '/log-lottery/config/readme', - name: 'Readme', - component: () => import('@/views/Config/Readme/index.vue'), - meta: { - title: i18n.global.t('sidebar.operatingInstructions'), - icon: 'readme', - }, - }, - ], + ], } const routes = [ - { - path: '/log-lottery', - component: Layout, - redirect: '/log-lottery/home', - children: [ - { - path: '/log-lottery/home', - name: 'Home', - component: Home, - }, - { - path: '/log-lottery/demo', - name: 'Demo', - component: () => import('@/views/Demo/index.vue'), - }, - configRoutes, - ], - }, -]; -const envMode=import.meta.env.MODE; + { + path: '/log-lottery', + component: Layout, + redirect: '/log-lottery/home', + children: [ + { + path: '/log-lottery/home', + name: 'Home', + component: Home, + }, + { + path: '/log-lottery/demo', + name: 'Demo', + component: () => import('@/views/Demo/index.vue'), + }, + configRoutes, + ], + }, +] +const envMode = import.meta.env.MODE const router = createRouter({ // 读取环境变量 - history: envMode==='file'?createWebHashHistory():createWebHistory(), - routes, + history: envMode === 'file' ? createWebHashHistory() : createWebHistory(), + routes, }) export default router diff --git a/src/views/Config/Person/PersonAll/importExcel.worker.ts b/src/views/Config/Person/PersonAll/importExcel.worker.ts new file mode 100644 index 0000000..6b9a39e --- /dev/null +++ b/src/views/Config/Person/PersonAll/importExcel.worker.ts @@ -0,0 +1,32 @@ +import * as XLSX from 'xlsx' +import { addOtherInfo } from '@/utils' +// 定义消息类型 +interface WorkerMessage { + type: 'start' | 'stop' | 'reset' + data: any +} + +let allData: any[] = [] + +// 接收主线程消息 +globalThis.onmessage = async (e: MessageEvent) => { + switch (e.data.type) { + case 'start': + { + const fileData = e.data.data + // const dataBinary = await readFileBinary(((fileEvent.target as HTMLInputElement).files as FileList)[0]!) + const workBook = XLSX.read(fileData, { type: 'binary', cellDates: true }) + const workSheet = workBook.Sheets[workBook.SheetNames[0]] + const excelData = XLSX.utils.sheet_to_json(workSheet) + allData = addOtherInfo(excelData) + globalThis.postMessage({ + type: 'done', + data: allData, + message: '读取完成', + }) + break + } + default: + break + } +} diff --git a/src/views/Config/Person/PersonAll/index.vue b/src/views/Config/Person/PersonAll/index.vue new file mode 100644 index 0000000..7d52530 --- /dev/null +++ b/src/views/Config/Person/PersonAll/index.vue @@ -0,0 +1,73 @@ + + + + + + + + + {{ t('viewTitle.personManagement') }} + + + {{ t('button.allDelete') }} + + + {{ t('button.downloadTemplate') }} + + + + + + + + {{ t('button.importData') }} + + + + + {{ t('button.resetData') }} + + + {{ t('button.exportResult') }} + + + {{ t('table.luckyPeopleNumber') }}: + {{ alreadyPersonList.length }} + / + {{ allPersonList.length }} + + + + + + + diff --git a/src/views/Config/Person/PersonAll/useViewModel.ts b/src/views/Config/Person/PersonAll/useViewModel.ts new file mode 100644 index 0000000..3a1de31 --- /dev/null +++ b/src/views/Config/Person/PersonAll/useViewModel.ts @@ -0,0 +1,153 @@ +import type { IPersonConfig } from '@/types/storeType' +import { storeToRefs } from 'pinia' +import * as XLSX from 'xlsx' +import i18n from '@/locales/i18n' +import useStore from '@/store' +import { readFileBinary } from '@/utils/file' +import ImportExcelWorker from './importExcel.worker?worker' + +export function useViewModel() { + const worker: Worker | null = new ImportExcelWorker() + const personConfig = useStore().personConfig + const { getAllPersonList: allPersonList, getAlreadyPersonList: alreadyPersonList } = storeToRefs(personConfig) + const tableColumns = [ + { + label: i18n.global.t('data.number'), + props: 'uid', + }, + { + label: i18n.global.t('data.name'), + props: 'name', + }, + { + label: i18n.global.t('data.department'), + props: 'department', + }, + { + label: i18n.global.t('data.avatar'), + props: 'avatar', + formatValue(row: any) { + return row.avatar ? `` : '-' + }, + }, + { + label: i18n.global.t('data.identity'), + props: 'identity', + }, + { + label: i18n.global.t('data.isWin'), + props: 'isWin', + formatValue(row: IPersonConfig) { + return row.isWin ? i18n.global.t('data.yes') : i18n.global.t('data.no') + }, + }, + { + label: i18n.global.t('data.operation'), + actions: [ + // { + // label: '编辑', + // type: 'btn-info', + // onClick: (row: any) => { + // delPersonItem(row) + // } + // }, + { + label: i18n.global.t('data.delete'), + type: 'btn-error', + onClick: (row: IPersonConfig) => { + delPersonItem(row) + }, + }, + + ], + }, + ] + /// 向worker发送消息 + function sendWorkerMessage(message: any) { + if (worker) { + worker.postMessage(message) + } + } + /// 开始导入 + function startWorker(data: Event) { + sendWorkerMessage({ type: 'start', data }) + } + /** + * 获取用户数据 + */ + async function handleFileChange(e: Event) { + // worker = new ImportExcelWorker() + if (worker) { + worker.onmessage = (e) => { + if (e.data.type === 'done') { + personConfig.resetPerson() + personConfig.addNotPersonList(e.data.data) + } + } + } + const dataBinary = await readFileBinary(((e.target as HTMLInputElement).files as FileList)[0]!) + startWorker(dataBinary) + } + /// 导出数据 + function exportData() { + let data = JSON.parse(JSON.stringify(allPersonList.value)) + // 排除一些字段 + for (let i = 0; i < data.length; i++) { + delete data[i].x + delete data[i].y + delete data[i].id + delete data[i].createTime + delete data[i].updateTime + delete data[i].prizeId + // 修改字段名称 + if (data[i].isWin) { + data[i].isWin = i18n.global.t('data.yes') + } + else { + data[i].isWin = i18n.global.t('data.no') + } + // 格式化数组为 + data[i].prizeTime = data[i].prizeTime.join(',') + data[i].prizeName = data[i].prizeName.join(',') + } + let dataString = JSON.stringify(data) + dataString = dataString + .replaceAll(/uid/g, i18n.global.t('data.number')) + .replaceAll(/isWin/g, i18n.global.t('data.isWin')) + .replaceAll(/department/g, i18n.global.t('data.department')) + .replaceAll(/name/g, i18n.global.t('data.name')) + .replaceAll(/identity/g, i18n.global.t('data.identity')) + .replaceAll(/prizeName/g, i18n.global.t('data.prizeName')) + .replaceAll(/prizeTime/g, i18n.global.t('data.prizeTime')) + + data = JSON.parse(dataString) + + if (data.length > 0) { + const dataBinary = XLSX.utils.json_to_sheet(data) + const dataBinaryBinary = XLSX.utils.book_new() + XLSX.utils.book_append_sheet(dataBinaryBinary, dataBinary, 'Sheet1') + XLSX.writeFile(dataBinaryBinary, 'data.xlsx') + } + } + + function resetData() { + personConfig.resetAlreadyPerson() + } + + function deleteAll() { + personConfig.deleteAllPerson() + } + + function delPersonItem(row: IPersonConfig) { + personConfig.deletePerson(row) + } + return { + resetData, + deleteAll, + handleFileChange, + exportData, + alreadyPersonList, + allPersonList, + tableColumns, + } +} From 173dd796a40c613f2ed73c1d6720ee860b08f008 Mon Sep 17 00:00:00 2001 From: log1997 <2694233102@qq.com> Date: Fri, 3 Oct 2025 11:39:20 +0800 Subject: [PATCH 06/16] =?UTF-8?q?feat:=20:sparkles:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components.d.ts | 1 + src/components/Dialog/index.vue | 59 +++++ src/router/index.ts | 240 +++++++++--------- .../Person/PersonAll/importExcel.worker.ts | 32 +++ src/views/Config/Person/PersonAll/index.vue | 73 ++++++ .../Config/Person/PersonAll/useViewModel.ts | 153 +++++++++++ 6 files changed, 438 insertions(+), 120 deletions(-) create mode 100644 src/components/Dialog/index.vue create mode 100644 src/views/Config/Person/PersonAll/importExcel.worker.ts create mode 100644 src/views/Config/Person/PersonAll/index.vue create mode 100644 src/views/Config/Person/PersonAll/useViewModel.ts diff --git a/src/components.d.ts b/src/components.d.ts index 04d884e..7b0c306 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -9,6 +9,7 @@ export {} declare module 'vue' { export interface GlobalComponents { DaiysuiTable: typeof import('./components/DaiysuiTable/index.vue')['default'] + Dialog: typeof import('./components/Dialog/index.vue')['default'] EditSeparateDialog: typeof import('./components/NumberSeparate/EditSeparateDialog.vue')['default'] HelloWorld: typeof import('./components/HelloWorld.vue')['default'] ImageSync: typeof import('./components/ImageSync/index.vue')['default'] diff --git a/src/components/Dialog/index.vue b/src/components/Dialog/index.vue new file mode 100644 index 0000000..a57bb97 --- /dev/null +++ b/src/components/Dialog/index.vue @@ -0,0 +1,59 @@ + + + + + + + {{ title }} + + + {{ desc }} + + + + + + {{ cancelText }} + + + {{ submitText }} + + + + + + + + diff --git a/src/router/index.ts b/src/router/index.ts index 1a37f75..ef753a3 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,142 +1,142 @@ +import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router' import Layout from '@/layout/index.vue' import i18n from '@/locales/i18n' import Home from '@/views/Home/index.vue' -import { createRouter, createWebHistory,createWebHashHistory } from 'vue-router' export const configRoutes = { - path: '/log-lottery/config', - name: 'Config', - component: () => import('@/views/Config/index.vue'), - children: [ - { - path: '', - redirect: '/log-lottery/config/person', - }, - { - path: '/log-lottery/config/person', - name: 'PersonConfig', - component: () => import('@/views/Config/Person/PersonConfig.vue'), - meta: { - title: i18n.global.t('sidebar.personConfiguration'), - icon: 'person', - }, - children: [ + path: '/log-lottery/config', + name: 'Config', + component: () => import('@/views/Config/index.vue'), + children: [ { - path: '', - redirect: '/log-lottery/config/person/all', + path: '', + redirect: '/log-lottery/config/person', }, { - path: '/log-lottery/config/person/all', - name: 'AllPersonConfig', - component: () => import('@/views/Config/Person/PersonAll.vue'), - meta: { - title: i18n.global.t('sidebar.personList'), - icon: 'all', - }, + path: '/log-lottery/config/person', + name: 'PersonConfig', + component: () => import('@/views/Config/Person/PersonConfig.vue'), + meta: { + title: i18n.global.t('sidebar.personConfiguration'), + icon: 'person', + }, + children: [ + { + path: '', + redirect: '/log-lottery/config/person/all', + }, + { + path: '/log-lottery/config/person/all', + name: 'AllPersonConfig', + component: () => import('@/views/Config/Person/PersonAll/index.vue'), + meta: { + title: i18n.global.t('sidebar.personList'), + icon: 'all', + }, + }, + { + path: '/log-lottery/config/person/already', + name: 'AlreadyPerson', + component: () => import('@/views/Config/Person/PersonAlready.vue'), + meta: { + title: i18n.global.t('sidebar.winnerList'), + icon: 'already', + }, + }, + // { + // path:'other', + // name:'OtherPersonConfig', + // component:()=>import('@/views/Config/Person/OtherPersonConfig.vue'), + // meta:{ + // title:'其他配置', + // icon:'other' + // } + // } + ], }, { - path: '/log-lottery/config/person/already', - name: 'AlreadyPerson', - component: () => import('@/views/Config/Person/PersonAlready.vue'), - meta: { - title: i18n.global.t('sidebar.winnerList'), - icon: 'already', - }, - }, - // { - // path:'other', - // name:'OtherPersonConfig', - // component:()=>import('@/views/Config/Person/OtherPersonConfig.vue'), - // meta:{ - // title:'其他配置', - // icon:'other' - // } - // } - ], - }, - { - path: '/log-lottery/config/prize', - name: 'PrizeConfig', - component: () => import('@/views/Config/Prize/PrizeConfig.vue'), - meta: { - title: i18n.global.t('sidebar.prizeConfiguration'), - icon: 'prize', - }, - }, - { - path: '/log-lottery/config/global', - name: 'GlobalConfig', - redirect: '/log-lottery/config/global/all', - meta: { - title: i18n.global.t('sidebar.globalSetting'), - icon: 'global', - }, - children: [ - { - path: '/log-lottery/config/global/face', - name: 'FaceConfig', - component: () => import('@/views/Config/Global/FaceConfig.vue'), - meta: { - title: i18n.global.t('sidebar.viewSetting'), - icon: 'face', - }, + path: '/log-lottery/config/prize', + name: 'PrizeConfig', + component: () => import('@/views/Config/Prize/PrizeConfig.vue'), + meta: { + title: i18n.global.t('sidebar.prizeConfiguration'), + icon: 'prize', + }, }, { - path: '/log-lottery/config/global/image', - name: 'ImageConfig', - component: () => import('@/views/Config/Global/ImageConfig.vue'), - meta: { - title: i18n.global.t('sidebar.imagesManagement'), - icon: 'image', - }, + path: '/log-lottery/config/global', + name: 'GlobalConfig', + redirect: '/log-lottery/config/global/all', + meta: { + title: i18n.global.t('sidebar.globalSetting'), + icon: 'global', + }, + children: [ + { + path: '/log-lottery/config/global/face', + name: 'FaceConfig', + component: () => import('@/views/Config/Global/FaceConfig.vue'), + meta: { + title: i18n.global.t('sidebar.viewSetting'), + icon: 'face', + }, + }, + { + path: '/log-lottery/config/global/image', + name: 'ImageConfig', + component: () => import('@/views/Config/Global/ImageConfig.vue'), + meta: { + title: i18n.global.t('sidebar.imagesManagement'), + icon: 'image', + }, + }, + { + path: '/log-lottery/config/global/music', + name: 'MusicConfig', + component: () => import('@/views/Config/Global/MusicConfig.vue'), + meta: { + title: i18n.global.t('sidebar.musicManagement'), + icon: 'music', + }, + }, + ], }, { - path: '/log-lottery/config/global/music', - name: 'MusicConfig', - component: () => import('@/views/Config/Global/MusicConfig.vue'), - meta: { - title: i18n.global.t('sidebar.musicManagement'), - icon: 'music', - }, + path: '/log-lottery/config/readme', + name: 'Readme', + component: () => import('@/views/Config/Readme/index.vue'), + meta: { + title: i18n.global.t('sidebar.operatingInstructions'), + icon: 'readme', + }, }, - ], - }, - { - path: '/log-lottery/config/readme', - name: 'Readme', - component: () => import('@/views/Config/Readme/index.vue'), - meta: { - title: i18n.global.t('sidebar.operatingInstructions'), - icon: 'readme', - }, - }, - ], + ], } const routes = [ - { - path: '/log-lottery', - component: Layout, - redirect: '/log-lottery/home', - children: [ - { - path: '/log-lottery/home', - name: 'Home', - component: Home, - }, - { - path: '/log-lottery/demo', - name: 'Demo', - component: () => import('@/views/Demo/index.vue'), - }, - configRoutes, - ], - }, -]; -const envMode=import.meta.env.MODE; + { + path: '/log-lottery', + component: Layout, + redirect: '/log-lottery/home', + children: [ + { + path: '/log-lottery/home', + name: 'Home', + component: Home, + }, + { + path: '/log-lottery/demo', + name: 'Demo', + component: () => import('@/views/Demo/index.vue'), + }, + configRoutes, + ], + }, +] +const envMode = import.meta.env.MODE const router = createRouter({ // 读取环境变量 - history: envMode==='file'?createWebHashHistory():createWebHistory(), - routes, + history: envMode === 'file' ? createWebHashHistory() : createWebHistory(), + routes, }) export default router diff --git a/src/views/Config/Person/PersonAll/importExcel.worker.ts b/src/views/Config/Person/PersonAll/importExcel.worker.ts new file mode 100644 index 0000000..6b9a39e --- /dev/null +++ b/src/views/Config/Person/PersonAll/importExcel.worker.ts @@ -0,0 +1,32 @@ +import * as XLSX from 'xlsx' +import { addOtherInfo } from '@/utils' +// 定义消息类型 +interface WorkerMessage { + type: 'start' | 'stop' | 'reset' + data: any +} + +let allData: any[] = [] + +// 接收主线程消息 +globalThis.onmessage = async (e: MessageEvent) => { + switch (e.data.type) { + case 'start': + { + const fileData = e.data.data + // const dataBinary = await readFileBinary(((fileEvent.target as HTMLInputElement).files as FileList)[0]!) + const workBook = XLSX.read(fileData, { type: 'binary', cellDates: true }) + const workSheet = workBook.Sheets[workBook.SheetNames[0]] + const excelData = XLSX.utils.sheet_to_json(workSheet) + allData = addOtherInfo(excelData) + globalThis.postMessage({ + type: 'done', + data: allData, + message: '读取完成', + }) + break + } + default: + break + } +} diff --git a/src/views/Config/Person/PersonAll/index.vue b/src/views/Config/Person/PersonAll/index.vue new file mode 100644 index 0000000..7d52530 --- /dev/null +++ b/src/views/Config/Person/PersonAll/index.vue @@ -0,0 +1,73 @@ + + + + + + + + + {{ t('viewTitle.personManagement') }} + + + {{ t('button.allDelete') }} + + + {{ t('button.downloadTemplate') }} + + + + + + + + {{ t('button.importData') }} + + + + + {{ t('button.resetData') }} + + + {{ t('button.exportResult') }} + + + {{ t('table.luckyPeopleNumber') }}: + {{ alreadyPersonList.length }} + / + {{ allPersonList.length }} + + + + + + + diff --git a/src/views/Config/Person/PersonAll/useViewModel.ts b/src/views/Config/Person/PersonAll/useViewModel.ts new file mode 100644 index 0000000..c116a96 --- /dev/null +++ b/src/views/Config/Person/PersonAll/useViewModel.ts @@ -0,0 +1,153 @@ +import type { IPersonConfig } from '@/types/storeType' +import { storeToRefs } from 'pinia' +import * as XLSX from 'xlsx' +import i18n from '@/locales/i18n' +import useStore from '@/store' +import { readFileBinary } from '@/utils/file' +import ImportExcelWorker from './importExcel.worker?worker' + +export function useViewModel() { + const worker: Worker | null = new ImportExcelWorker() + const personConfig = useStore().personConfig + const { getAllPersonList: allPersonList, getAlreadyPersonList: alreadyPersonList } = storeToRefs(personConfig) + const tableColumns = [ + { + label: i18n.global.t('data.number'), + props: 'uid', + }, + { + label: i18n.global.t('data.name'), + props: 'name', + }, + { + label: i18n.global.t('data.department'), + props: 'department', + }, + { + label: i18n.global.t('data.avatar'), + props: 'avatar', + formatValue(row: any) { + return row.avatar ? `` : '-' + }, + }, + { + label: i18n.global.t('data.identity'), + props: 'identity', + }, + { + label: i18n.global.t('data.isWin'), + props: 'isWin', + formatValue(row: IPersonConfig) { + return row.isWin ? i18n.global.t('data.yes') : i18n.global.t('data.no') + }, + }, + { + label: i18n.global.t('data.operation'), + actions: [ + // { + // label: '编辑', + // type: 'btn-info', + // onClick: (row: any) => { + // delPersonItem(row) + // } + // }, + { + label: i18n.global.t('data.delete'), + type: 'btn-error', + onClick: (row: IPersonConfig) => { + delPersonItem(row) + }, + }, + + ], + }, + ] + /// 向worker发送消息 + function sendWorkerMessage(message: any) { + if (worker) { + worker.postMessage(message) + } + } + /// 开始导入 + function startWorker(data: Event) { + sendWorkerMessage({ type: 'start', data }) + } + /** + * 获取用户数据 + */ + async function handleFileChange(e: Event) { + // worker = new ImportExcelWorker() + if (worker) { + worker.onmessage = (e) => { + if (e.data.type === 'done') { + personConfig.resetPerson() + personConfig.addNotPersonList(e.data.data) + } + } + } + const dataBinary = await readFileBinary(((e.target as HTMLInputElement).files as FileList)[0]!) + startWorker(dataBinary) + } + /// 导出数据 + function exportData() { + let data = JSON.parse(JSON.stringify(allPersonList.value)) + // 排除一些字段 + for (let i = 0; i < data.length; i++) { + delete data[i].x + delete data[i].y + delete data[i].id + delete data[i].createTime + delete data[i].updateTime + delete data[i].prizeId + // 修改字段名称 + if (data[i].isWin) { + data[i].isWin = i18n.global.t('data.yes') + } + else { + data[i].isWin = i18n.global.t('data.no') + } + // 格式化数组为 + data[i].prizeTime = data[i].prizeTime.join(',') + data[i].prizeName = data[i].prizeName.join(',') + } + let dataString = JSON.stringify(data) + dataString = dataString + .replaceAll(/uid/g, i18n.global.t('data.number')) + .replaceAll(/isWin/g, i18n.global.t('data.isWin')) + .replaceAll(/department/g, i18n.global.t('data.department')) + .replaceAll(/name/g, i18n.global.t('data.name')) + .replaceAll(/identity/g, i18n.global.t('data.identity')) + .replaceAll(/prizeName/g, i18n.global.t('data.prizeName')) + .replaceAll(/prizeTime/g, i18n.global.t('data.prizeTime')) + + data = JSON.parse(dataString) + + if (data.length > 0) { + const dataBinary = XLSX.utils.json_to_sheet(data) + const dataBinaryBinary = XLSX.utils.book_new() + XLSX.utils.book_append_sheet(dataBinaryBinary, dataBinary, 'Sheet1') + XLSX.writeFile(dataBinaryBinary, 'data.xlsx') + } + } + + function resetData() { + personConfig.resetAlreadyPerson() + } + + function deleteAll() { + personConfig.deleteAllPerson() + } + + function delPersonItem(row: IPersonConfig) { + personConfig.deletePerson(row) + } + return { + resetData, + deleteAll, + handleFileChange, + exportData, + alreadyPersonList, + allPersonList, + tableColumns, + } +} From 27fd0768c1e3213b456d19154d6d2774fe465410 Mon Sep 17 00:00:00 2001 From: LOG1997 <2694233102@qq.com> Date: Sun, 12 Oct 2025 22:30:54 +0800 Subject: [PATCH 07/16] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0Loading?= =?UTF-8?q?=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 5 +- src/components.d.ts | 1 + src/components/Loading/index.ts | 5 + src/components/Loading/index.vue | 22 ++ src/components/Loading/loading-context.ts | 61 ++++ src/layout/index.vue | 5 +- src/main.ts | 16 +- .../Person/PersonAll/importExcel.worker.ts | 5 + src/views/Config/Person/PersonAll/index.vue | 12 +- .../Config/Person/PersonAll/useViewModel.ts | 282 +++++++++--------- src/vite-env.d.ts | 7 +- 11 files changed, 266 insertions(+), 155 deletions(-) create mode 100644 src/components/Loading/index.ts create mode 100644 src/components/Loading/index.vue create mode 100644 src/components/Loading/loading-context.ts diff --git a/src/App.vue b/src/App.vue index 8512bab..96991db 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,11 +1,13 @@ + + + + + {{ text ? text : '加载中' }} + + + + diff --git a/src/components/Loading/loading-context.ts b/src/components/Loading/loading-context.ts new file mode 100644 index 0000000..6093acf --- /dev/null +++ b/src/components/Loading/loading-context.ts @@ -0,0 +1,61 @@ +// src/contexts/loading-context.ts +import type { InjectionKey, Ref } from 'vue' +import { ref } from 'vue' + +// 定义 Loading 配置类型 +export interface LoadingOptions { + visible: Ref + text: Ref + fullscreen: Ref + zIndex: Ref + count: Ref + show: (options?: Partial<{ text: string, fullscreen: boolean, zIndex: number }>) => void + hide: () => void +} + +// 注入密钥(Symbol 确保唯一性) +export const loadingKey: InjectionKey = Symbol('loading') + +// 全局状态(单例) +const visible = ref(false) +const text = ref('') +const fullscreen = ref(true) +const zIndex = ref(9999) +const count = ref(0) + +// 显示 Loading +function show(options?: Partial<{ text: string, fullscreen: boolean, zIndex: number }>) { + count.value++ + if (count.value > 1) + return + visible.value = true + if (options) { + text.value = options.text || '' + fullscreen.value = options.fullscreen ?? true + zIndex.value = options.zIndex || 9999 + } +} + +// 隐藏 Loading +function hide() { + if (count.value <= 0) + return + count.value-- + if (count.value === 0) { + visible.value = false + text.value = '' + fullscreen.value = true + zIndex.value = 9999 + } +} + +// 导出全局状态(供根组件提供) +export const loadingState: LoadingOptions = { + visible, + text, + fullscreen, + zIndex, + count, + show, + hide, +} diff --git a/src/layout/index.vue b/src/layout/index.vue index acd521c..10cc77e 100644 --- a/src/layout/index.vue +++ b/src/layout/index.vue @@ -1,12 +1,12 @@ @@ -45,8 +45,8 @@ const delAllDataDialogRef = ref() {{ t('button.importData') }} diff --git a/src/views/Config/Person/PersonAll/useViewModel.ts b/src/views/Config/Person/PersonAll/useViewModel.ts index c116a96..9464db9 100644 --- a/src/views/Config/Person/PersonAll/useViewModel.ts +++ b/src/views/Config/Person/PersonAll/useViewModel.ts @@ -1,153 +1,167 @@ +import type { Ref } from 'vue' import type { IPersonConfig } from '@/types/storeType' import { storeToRefs } from 'pinia' +import { inject } from 'vue' import * as XLSX from 'xlsx' +import { loadingKey } from '@/components/Loading' import i18n from '@/locales/i18n' import useStore from '@/store' import { readFileBinary } from '@/utils/file' import ImportExcelWorker from './importExcel.worker?worker' -export function useViewModel() { - const worker: Worker | null = new ImportExcelWorker() - const personConfig = useStore().personConfig - const { getAllPersonList: allPersonList, getAlreadyPersonList: alreadyPersonList } = storeToRefs(personConfig) - const tableColumns = [ - { - label: i18n.global.t('data.number'), - props: 'uid', - }, - { - label: i18n.global.t('data.name'), - props: 'name', - }, - { - label: i18n.global.t('data.department'), - props: 'department', - }, - { - label: i18n.global.t('data.avatar'), - props: 'avatar', - formatValue(row: any) { - return row.avatar ? `` : '-' - }, - }, - { - label: i18n.global.t('data.identity'), - props: 'identity', - }, - { - label: i18n.global.t('data.isWin'), - props: 'isWin', - formatValue(row: IPersonConfig) { - return row.isWin ? i18n.global.t('data.yes') : i18n.global.t('data.no') - }, - }, - { - label: i18n.global.t('data.operation'), - actions: [ - // { - // label: '编辑', - // type: 'btn-info', - // onClick: (row: any) => { - // delPersonItem(row) - // } - // }, +export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref }) { + const worker: Worker | null = new ImportExcelWorker() + const loading = inject(loadingKey) + const personConfig = useStore().personConfig + const { getAllPersonList: allPersonList, getAlreadyPersonList: alreadyPersonList } = storeToRefs(personConfig) + const tableColumns = [ { - label: i18n.global.t('data.delete'), - type: 'btn-error', - onClick: (row: IPersonConfig) => { - delPersonItem(row) - }, + label: i18n.global.t('data.number'), + props: 'uid', }, + { + label: i18n.global.t('data.name'), + props: 'name', + }, + { + label: i18n.global.t('data.department'), + props: 'department', + }, + { + label: i18n.global.t('data.avatar'), + props: 'avatar', + formatValue(row: any) { + return row.avatar ? `` : '-' + }, + }, + { + label: i18n.global.t('data.identity'), + props: 'identity', + }, + { + label: i18n.global.t('data.isWin'), + props: 'isWin', + formatValue(row: IPersonConfig) { + return row.isWin ? i18n.global.t('data.yes') : i18n.global.t('data.no') + }, + }, + { + label: i18n.global.t('data.operation'), + actions: [ + // { + // label: '编辑', + // type: 'btn-info', + // onClick: (row: any) => { + // delPersonItem(row) + // } + // }, + { + label: i18n.global.t('data.delete'), + type: 'btn-error', + onClick: (row: IPersonConfig) => { + delPersonItem(row) + }, + }, - ], - }, - ] - /// 向worker发送消息 - function sendWorkerMessage(message: any) { - if (worker) { - worker.postMessage(message) - } - } - /// 开始导入 - function startWorker(data: Event) { - sendWorkerMessage({ type: 'start', data }) - } - /** - * 获取用户数据 - */ - async function handleFileChange(e: Event) { - // worker = new ImportExcelWorker() - if (worker) { - worker.onmessage = (e) => { - if (e.data.type === 'done') { - personConfig.resetPerson() - personConfig.addNotPersonList(e.data.data) + ], + }, + ] + /// 向worker发送消息 + function sendWorkerMessage(message: any) { + if (worker) { + worker.postMessage(message) } - } } - const dataBinary = await readFileBinary(((e.target as HTMLInputElement).files as FileList)[0]!) - startWorker(dataBinary) - } - /// 导出数据 - function exportData() { - let data = JSON.parse(JSON.stringify(allPersonList.value)) - // 排除一些字段 - for (let i = 0; i < data.length; i++) { - delete data[i].x - delete data[i].y - delete data[i].id - delete data[i].createTime - delete data[i].updateTime - delete data[i].prizeId - // 修改字段名称 - if (data[i].isWin) { - data[i].isWin = i18n.global.t('data.yes') - } - else { - data[i].isWin = i18n.global.t('data.no') - } - // 格式化数组为 - data[i].prizeTime = data[i].prizeTime.join(',') - data[i].prizeName = data[i].prizeName.join(',') + /// 开始导入 + function startWorker(data: Event) { + loading?.show() + sendWorkerMessage({ type: 'start', data }) } - let dataString = JSON.stringify(data) - dataString = dataString - .replaceAll(/uid/g, i18n.global.t('data.number')) - .replaceAll(/isWin/g, i18n.global.t('data.isWin')) - .replaceAll(/department/g, i18n.global.t('data.department')) - .replaceAll(/name/g, i18n.global.t('data.name')) - .replaceAll(/identity/g, i18n.global.t('data.identity')) - .replaceAll(/prizeName/g, i18n.global.t('data.prizeName')) - .replaceAll(/prizeTime/g, i18n.global.t('data.prizeTime')) - - data = JSON.parse(dataString) - - if (data.length > 0) { - const dataBinary = XLSX.utils.json_to_sheet(data) - const dataBinaryBinary = XLSX.utils.book_new() - XLSX.utils.book_append_sheet(dataBinaryBinary, dataBinary, 'Sheet1') - XLSX.writeFile(dataBinaryBinary, 'data.xlsx') + /** + * 获取用户数据 + */ + async function handleFileChange(e: Event) { + // worker = new ImportExcelWorker() + if (worker) { + worker.onmessage = (e) => { + if (e.data.type === 'done') { + personConfig.resetPerson() + personConfig.addNotPersonList(e.data.data) + // 导入成功后清空file input + clearFileInput() + } + loading?.hide() + } + } + const dataBinary = await readFileBinary(((e.target as HTMLInputElement).files as FileList)[0]!) + startWorker(dataBinary) } - } + // 清空file input + function clearFileInput() { + if (exportInputFileRef.value) { + exportInputFileRef.value.value = '' + } + } + /// 导出数据 + function exportData() { + let data = JSON.parse(JSON.stringify(allPersonList.value)) + // 排除一些字段 + for (let i = 0; i < data.length; i++) { + delete data[i].x + delete data[i].y + delete data[i].id + delete data[i].createTime + delete data[i].updateTime + delete data[i].prizeId + // 修改字段名称 + if (data[i].isWin) { + data[i].isWin = i18n.global.t('data.yes') + } + else { + data[i].isWin = i18n.global.t('data.no') + } + // 格式化数组为 + data[i].prizeTime = data[i].prizeTime.join(',') + data[i].prizeName = data[i].prizeName.join(',') + } + let dataString = JSON.stringify(data) + dataString = dataString + .replaceAll(/uid/g, i18n.global.t('data.number')) + .replaceAll(/isWin/g, i18n.global.t('data.isWin')) + .replaceAll(/department/g, i18n.global.t('data.department')) + .replaceAll(/name/g, i18n.global.t('data.name')) + .replaceAll(/identity/g, i18n.global.t('data.identity')) + .replaceAll(/prizeName/g, i18n.global.t('data.prizeName')) + .replaceAll(/prizeTime/g, i18n.global.t('data.prizeTime')) - function resetData() { - personConfig.resetAlreadyPerson() - } + data = JSON.parse(dataString) - function deleteAll() { - personConfig.deleteAllPerson() - } + if (data.length > 0) { + const dataBinary = XLSX.utils.json_to_sheet(data) + const dataBinaryBinary = XLSX.utils.book_new() + XLSX.utils.book_append_sheet(dataBinaryBinary, dataBinary, 'Sheet1') + XLSX.writeFile(dataBinaryBinary, 'data.xlsx') + } + } - function delPersonItem(row: IPersonConfig) { - personConfig.deletePerson(row) - } - return { - resetData, - deleteAll, - handleFileChange, - exportData, - alreadyPersonList, - allPersonList, - tableColumns, - } + function resetData() { + personConfig.resetAlreadyPerson() + } + + function deleteAll() { + personConfig.deleteAllPerson() + } + + function delPersonItem(row: IPersonConfig) { + personConfig.deletePerson(row) + } + return { + resetData, + deleteAll, + handleFileChange, + exportData, + alreadyPersonList, + allPersonList, + tableColumns, + } } diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 85276f8..74dd46a 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,11 +1,12 @@ /// declare module '*.vue' { - import type { DefineComponent } from 'vue' + import type { DefineComponent } from 'vue' - const component: DefineComponent - export default component + const component: DefineComponent + export default component } declare module 'sparticles' declare module 'three-trackballcontrols' +declare module 'virtual:svg-icons-register' From 1eca9390106c92c242b652e3065fc57861beeeab Mon Sep 17 00:00:00 2001 From: LOG1997 <2694233102@qq.com> Date: Mon, 13 Oct 2025 22:31:12 +0800 Subject: [PATCH 08/16] =?UTF-8?q?feat:=20indexdb=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E4=BA=BA=E5=91=98=E5=90=8D=E5=8D=95=EF=BC=8C=E4=BD=BF=E7=94=A8?= =?UTF-8?q?dexie?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 8 + src/store/personConfig.ts | 302 +++++++++++++++++++++----------------- src/utils/dexie/index.ts | 108 ++++++++++++++ src/utils/dexie/type.ts | 3 + 5 files changed, 284 insertions(+), 138 deletions(-) create mode 100644 src/utils/dexie/index.ts create mode 100644 src/utils/dexie/type.ts diff --git a/package.json b/package.json index 5d5a7b7..9de49ae 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "axios": "^1.7.8", "canvas-confetti": "^1.9.3", "dayjs": "^1.11.13", + "dexie": "^4.2.1", "github-markdown-css": "^5.8.0", "localforage": "^1.10.0", "markdown-it": "^14.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9cda78..3584108 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 + dexie: + specifier: ^4.2.1 + version: 4.2.1 github-markdown-css: specifier: ^5.8.0 version: 5.8.0 @@ -2701,6 +2704,9 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dexie@4.2.1: + resolution: {integrity: sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==} + dom-accessibility-api@0.5.16: resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} @@ -8238,6 +8244,8 @@ snapshots: dependencies: dequal: 2.0.3 + dexie@4.2.1: {} + dom-accessibility-api@0.5.16: {} dom-serializer@0.2.2: diff --git a/src/store/personConfig.ts b/src/store/personConfig.ts index 06df7d1..b142649 100644 --- a/src/store/personConfig.ts +++ b/src/store/personConfig.ts @@ -1,159 +1,185 @@ import type { IPersonConfig, IPrizeConfig } from '@/types/storeType' - import dayjs from 'dayjs' import { defineStore } from 'pinia' +import { computed, ref, toRaw, watch } from 'vue' +import { IndexDb } from '@/utils/dexie' import { defaultPersonList } from './data' import { usePrizeConfig } from './prizeConfig' -export const usePersonConfig = defineStore('person', { - state() { - return { - personConfig: { +// 获取IPersonConfig的key组成数组 +export const personListKey = Object.keys(defaultPersonList[0]) +export const usePersonConfig = defineStore('person', () => { + const personDb = new IndexDb('person', ['allPersonList', 'alreadyPersonList'], 1, personListKey) + // NOTE: state + const personConfig = ref({ allPersonList: [] as IPersonConfig[], alreadyPersonList: [] as IPersonConfig[], - }, - } - }, - getters: { + }) + personDb.getAllData('allPersonList').then((data) => { + personConfig.value.allPersonList = data + }) + personDb.getAllData('alreadyPersonList').then((data) => { + console.log(data) + personConfig.value.alreadyPersonList = data + }) + + // NOTE: getter // 获取全部配置 - getPersonConfig(state) { - return state.personConfig - }, + const getPersonConfig = computed(() => personConfig.value) // 获取全部人员名单 - getAllPersonList(state) { - return state.personConfig.allPersonList.filter((item: IPersonConfig) => { - return item - }) - }, + const getAllPersonList = computed(() => personConfig.value.allPersonList) // 获取未获此奖的人员名单 - getNotThisPrizePersonList(state: any) { - const currentPrize = usePrizeConfig().prizeConfig.currentPrize - const data = state.personConfig.allPersonList.filter((item: IPersonConfig) => { - return !item.prizeId.includes(currentPrize.id as string) - }) - - return data - }, - // 获取已中奖人员名单 - getAlreadyPersonList(state) { - return state.personConfig.allPersonList.filter((item: IPersonConfig) => { - return item.isWin === true - }) - }, - // 获取中奖人员详情 - getAlreadyPersonDetail(state) { - return state.personConfig.alreadyPersonList - }, - // 获取未中奖人员名单 - getNotPersonList(state) { - return state.personConfig.allPersonList.filter((item: IPersonConfig) => { - return item.isWin === false - }) - }, - }, - actions: { - // 添加未中奖人员 - addNotPersonList(personList: IPersonConfig[]) { - if (personList.length <= 0) { - return - } - personList.forEach((item: IPersonConfig) => { - this.personConfig.allPersonList.push(item) - }) - }, - // 添加已中奖人员 - addAlreadyPersonList(personList: IPersonConfig[], prize: IPrizeConfig | null) { - if (personList.length <= 0) { - return - } - personList.forEach((person: IPersonConfig) => { - this.personConfig.allPersonList.map((item: IPersonConfig) => { - if (item.id === person.id && prize != null) { - item.isWin = true - // person.isWin = true - item.prizeName.push(prize.name) - // person.prizeName += prize.name - item.prizeTime.push(dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss')) - // person.prizeTime = new Date().toString() - item.prizeId.push(prize.id as string) - } - - return item + const getNotThisPrizePersonList = computed(() => { + const currentPrize = usePrizeConfig().prizeConfig.currentPrize + const data = personConfig.value.allPersonList.filter((item: IPersonConfig) => { + return !item.prizeId.includes(currentPrize.id as string) }) - this.personConfig.alreadyPersonList.push(person) - }) - }, - // 从已中奖移动到未中奖 - moveAlreadyToNot(person: IPersonConfig) { - if (person.id === undefined || person.id == null) { - return - } - const alreadyPersonListLength = this.personConfig.alreadyPersonList.length - for (let i = 0; i < this.personConfig.allPersonList.length; i++) { - if (person.id === this.personConfig.allPersonList[i].id) { - this.personConfig.allPersonList[i].isWin = false - this.personConfig.allPersonList[i].prizeName = [] - this.personConfig.allPersonList[i].prizeTime = [] - this.personConfig.allPersonList[i].prizeId = [] - break + return data + }) + + // 获取已中奖人员名单 + const getAlreadyPersonList = computed(() => { + return personConfig.value.allPersonList.filter((item: IPersonConfig) => { + return item.isWin === true + }) + }) + // 获取中奖人员详情 + const getAlreadyPersonDetail = computed(() => personConfig.value.alreadyPersonList) + // 获取未中奖人员名单 + const getNotPersonList = computed(() => personConfig.value.allPersonList.filter((item: IPersonConfig) => { + return item.isWin === false + })) + // NOTE: action + // 添加未中奖人员 + function addNotPersonList(personList: IPersonConfig[]) { + if (personList.length <= 0) { + return } - } - for (let i = 0; i < alreadyPersonListLength; i++) { - this.personConfig.alreadyPersonList = this.personConfig.alreadyPersonList.filter((item: IPersonConfig) => - item.id !== person.id, - ) - } - }, + personList.forEach((item: IPersonConfig) => { + personConfig.value.allPersonList.push(item) + }) + personDb.setAllData('allPersonList', personList) + } + // 添加已中奖人员 + function addAlreadyPersonList(personList: IPersonConfig[], prize: IPrizeConfig | null) { + if (personList.length <= 0) { + return + } + personList.forEach((person: IPersonConfig) => { + personConfig.value.allPersonList.map((item: IPersonConfig) => { + if (item.id === person.id && prize != null) { + item.isWin = true + // person.isWin = true + item.prizeName.push(prize.name) + // person.prizeName += prize.name + item.prizeTime.push(dayjs(new Date()).format('YYYY-MM-DD HH:mm:ss')) + // person.prizeTime = new Date().toString() + item.prizeId.push(prize.id as string) + } + return item + }) + personConfig.value.alreadyPersonList.push(person) + personDb.updateData('allPersonList', toRaw(person)) + personDb.setData('alreadyPersonList', toRaw(person)) + }) + } + // 从已中奖移动到未中奖 + function moveAlreadyToNot(person: IPersonConfig) { + if (person.id === undefined || person.id == null) { + return + } + const alreadyPersonListLength = personConfig.value.alreadyPersonList.length + for (let i = 0; i < personConfig.value.allPersonList.length; i++) { + if (person.id === personConfig.value.allPersonList[i].id) { + personConfig.value.allPersonList[i].isWin = false + personConfig.value.allPersonList[i].prizeName = [] + personConfig.value.allPersonList[i].prizeTime = [] + personConfig.value.allPersonList[i].prizeId = [] + personDb.updateData('allPersonList', toRaw(personConfig.value.allPersonList[i])) + break + } + } + const alreadyPersonListRaw = toRaw(personConfig.value.alreadyPersonList) + for (let i = 0; i < alreadyPersonListLength; i++) { + personConfig.value.alreadyPersonList = alreadyPersonListRaw.filter((item: IPersonConfig) => + item.id !== person.id, + ) + } + personDb.deleteData('alreadyPersonList', person) + } // 删除指定人员 - deletePerson(person: IPersonConfig) { - if (person.id !== undefined || person.id != null) { - this.personConfig.allPersonList = this.personConfig.allPersonList.filter((item: IPersonConfig) => item.id !== person.id) - this.personConfig.alreadyPersonList = this.personConfig.alreadyPersonList.filter((item: IPersonConfig) => item.id !== person.id) - } - }, + function deletePerson(person: IPersonConfig) { + if (person.id !== undefined || person.id != null) { + const allPersonListRaw = toRaw(personConfig.value.allPersonList) + const alreadyPersonListRaw = toRaw(personConfig.value.alreadyPersonList) + personConfig.value.allPersonList = allPersonListRaw.filter((item: IPersonConfig) => item.id !== person.id) + personConfig.value.alreadyPersonList = alreadyPersonListRaw.filter((item: IPersonConfig) => item.id !== person.id) + personDb.deleteData('allPersonList', person) + personDb.deleteData('alreadyPersonList', person) + } + } // 删除所有人员 - deleteAllPerson() { - this.personConfig.allPersonList = [] - this.personConfig.alreadyPersonList = [] - }, + function deleteAllPerson() { + personConfig.value.allPersonList = [] + personConfig.value.alreadyPersonList = [] + personDb.deleteAll('allPersonList') + personDb.deleteAll('alreadyPersonList') + } // 删除所有人员 - resetPerson() { - this.personConfig.allPersonList = [] - this.personConfig.alreadyPersonList = [] - }, + function resetPerson() { + personConfig.value.allPersonList = [] + personConfig.value.alreadyPersonList = [] + personDb.deleteAll('allPersonList') + personDb.deleteAll('alreadyPersonList') + } // 重置已中奖人员 - resetAlreadyPerson() { - // 把已中奖人员合并到未中奖人员,要验证是否已存在 - this.personConfig.allPersonList.forEach((item: IPersonConfig) => { - item.isWin = false - item.prizeName = [] - item.prizeTime = [] - item.prizeId = [] - }) - this.personConfig.alreadyPersonList = [] - }, - setDefaultPersonList() { - this.personConfig.allPersonList = defaultPersonList - this.personConfig.alreadyPersonList = [] - }, + function resetAlreadyPerson() { + // 把已中奖人员合并到未中奖人员,要验证是否已存在 + personConfig.value.allPersonList.forEach((item: IPersonConfig) => { + item.isWin = false + item.prizeName = [] + item.prizeTime = [] + item.prizeId = [] + }) + personConfig.value.alreadyPersonList = [] + const allPersonListRaw = toRaw(personConfig.value.allPersonList) + personDb.deleteAll('allPersonList') + personDb.setAllData('allPersonList', allPersonListRaw) + personDb.deleteAll('alreadyPersonList') + } + function setDefaultPersonList() { + personConfig.value.allPersonList = defaultPersonList + personConfig.value.alreadyPersonList = [] + personDb.setAllData('allPersonList', defaultPersonList) + personDb.deleteAll('alreadyPersonList') + } // 重置所有配置 - reset() { - this.personConfig = { - allPersonList: [] as IPersonConfig[], - alreadyPersonList: [] as IPersonConfig[], - } - }, - }, - persist: { - enabled: true, - strategies: [ - { - // 如果要存储在localStorage中 - storage: localStorage, - key: 'personConfig', - }, - ], - }, + function reset() { + personConfig.value = { + allPersonList: [] as IPersonConfig[], + alreadyPersonList: [] as IPersonConfig[], + } + personDb.deleteAll('allPersonList') + personDb.deleteAll('alreadyPersonList') + } + return { + personConfig, + getPersonConfig, + getAllPersonList, + getNotThisPrizePersonList, + getAlreadyPersonList, + getAlreadyPersonDetail, + getNotPersonList, + addNotPersonList, + addAlreadyPersonList, + moveAlreadyToNot, + deletePerson, + deleteAllPerson, + resetPerson, + resetAlreadyPerson, + setDefaultPersonList, + reset, + } }) diff --git a/src/utils/dexie/index.ts b/src/utils/dexie/index.ts new file mode 100644 index 0000000..704073b --- /dev/null +++ b/src/utils/dexie/index.ts @@ -0,0 +1,108 @@ +import type { EntityTable } from 'dexie' +import type { DbData } from './type' +import dayjs from 'dayjs' +import Dexie from 'dexie' + +class IndexDb { + name: string + dbStore: any + version: number + dbKeys: string[] + tableNames: string[] + constructor(name: string, tableNames: string[], version = 1, dbKeys: string[] = []) { + this.name = name // 数据库名称 + this.version = version // 数据库版本号 + this.dbKeys = dbKeys // 数据库key + this.tableNames = tableNames + this.dbStore = new Dexie(name) as Dexie & { [key: string]: EntityTable } + // 获取存在的key + const stores: Record = {} + for (const tableName of tableNames) { + stores[tableName] = 'id,dateTime,type,uid' // 根据需要调整字段 + } + this.dbStore.version(this.version).stores(stores) + } + + setAllData(tableName: string, data: DbData[]) { + this.dbStore[tableName].bulkAdd(data) + } + + /** + * @param data + * @description 添加单条数据,并为数据添加dataTime和type属性 + */ + setData(tableName: string, data: Partial) { + if (!data.dateTime) { + data.dateTime = dayjs().format('YYYY-MM-DD HH:mm:ss:SSS') + } + if (!data.type) { + data.type = 'info' + } + this.dbStore[tableName].add(data) + } + + // 更新单条数据 + updateData(tableName: string, data: Partial) { + this.dbStore[tableName].update(data.id, data) + } + + /** + * @returns 所有数据Array + * @description 删除所有数据并返回被删除的数据 + */ + deleteAll(tableName: string) { + return this.dbStore[tableName].clear() + } + + /** + * @param data + * @description 删除单条数据 + */ + deleteData(tableName: string, data: Partial) { + this.dbStore[tableName].delete(data.id) + } + + /** + * @returns 所有数据Array + * @description 获取所有数据 + */ + async getAllData(tableName: string, isAsc: boolean = true) { + const allData = await this.dbStore[tableName].toArray() + // return allData + return isAsc ? allData : allData.reverse() + } + + // 分页获取数据 + async getPageData(tableName: string, pageNum: number, pageSize: number, isAsc: boolean = true) { + const allData = await this.dbStore[tableName].toArray() + const start = (pageNum - 1) * pageSize + const end = pageNum * pageSize + return isAsc ? allData.slice(start, end) : allData.slice(end, start).reverse() + } + + /** + * @returns 数据库总长度 + * @description 获取所有数据的列表长度 + */ + getAllLength(tableName: string) { + return this.dbStore[tableName].count() + } + + /** + * + * @param filter 根据筛选条件返回数据 + * @returns + */ + getFilterData(tableName: string, filter: string) { + return this.dbStore[tableName].filter((item: any) => { + return item.content.includes(filter) + }).toArray() + } + + getKeys(tableName: string, key: string) { + // keys 方法获取所有主键 + return this.dbStore[tableName].orderBy(key).keys() + } +} + +export { IndexDb } diff --git a/src/utils/dexie/type.ts b/src/utils/dexie/type.ts new file mode 100644 index 0000000..e2e8ecc --- /dev/null +++ b/src/utils/dexie/type.ts @@ -0,0 +1,3 @@ +export interface DbData { + [key: string]: any +} From 005b93266ebc2c836a1a83dafbe0297ed1abe713 Mon Sep 17 00:00:00 2001 From: log1997 <2694233102@qq.com> Date: Tue, 2 Dec 2025 14:24:54 +0800 Subject: [PATCH 09/16] =?UTF-8?q?feat(config):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E9=85=8D=E7=BD=AE=E9=A1=B9=E5=B8=83=E5=B1=80?= =?UTF-8?q?=E4=B8=8E=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将原有的线性表单布局调整为分组的 fieldset 布局,提升视觉层次和可维护性。 新增文本设置、布局设置、主题设置、图案设置和其他设置等分类区域。 优化部分输入控件的结构和样式类名,增强用户体验和代码可读性。 --- package.json | 1 + pnpm-lock.yaml | 36 +-- src/views/Config/Global/FaceConfig.vue | 361 ++++++++++++++----------- 3 files changed, 208 insertions(+), 190 deletions(-) diff --git a/package.json b/package.json index 9de49ae..482e7f0 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@vitest/ui": "^3.2.4", "@vue/test-utils": "^2.4.6", "autoprefixer": "^10.4.20", + "baseline-browser-mapping": "^2.8.32", "daisyui": "^5.1.13", "eslint": "^9.15.0", "eslint-plugin-vue": "^10.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3584108..8799787 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,6 +132,9 @@ importers: autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.49) + baseline-browser-mapping: + specifier: ^2.8.32 + version: 2.8.32 daisyui: specifier: ^5.1.13 version: 5.1.13 @@ -1366,42 +1369,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.0': resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.0': resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.0': resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.0': resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.0': resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.0': resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==} @@ -1472,67 +1469,56 @@ packages: resolution: {integrity: sha512-aL6hRwu0k7MTUESgkg7QHY6CoqPgr6gdQXRJI1/VbFlUMwsSzPGSR7sG5d+MCbYnJmJwThc2ol3nixj1fvI/zQ==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.52.0': resolution: {integrity: sha512-BTs0M5s1EJejgIBJhCeiFo7GZZ2IXWkFGcyZhxX4+8usnIo5Mti57108vjXFIQmmJaRyDwmV59Tw64Ap1dkwMw==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.52.0': resolution: {integrity: sha512-uj672IVOU9m08DBGvoPKPi/J8jlVgjh12C9GmjjBxCTQc3XtVmRkRKyeHSmIKQpvJ7fIm1EJieBUcnGSzDVFyw==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.52.0': resolution: {integrity: sha512-/+IVbeDMDCtB/HP/wiWsSzduD10SEGzIZX2945KSgZRNi4TSkjHqRJtNTVtVb8IRwhJ65ssI56krlLik+zFWkw==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.52.0': resolution: {integrity: sha512-U1vVzvSWtSMWKKrGoROPBXMh3Vwn93TA9V35PldokHGqiUbF6erSzox/5qrSMKp6SzakvyjcPiVF8yB1xKr9Pg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.52.0': resolution: {integrity: sha512-X/4WfuBAdQRH8cK3DYl8zC00XEE6aM472W+QCycpQJeLWVnHfkv7RyBFVaTqNUMsTgIX8ihMjCvFF9OUgeABzw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.52.0': resolution: {integrity: sha512-xIRYc58HfWDBZoLmWfWXg2Sq8VCa2iJ32B7mqfWnkx5mekekl0tMe7FHpY8I72RXEcUkaWawRvl3qA55og+cwQ==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.52.0': resolution: {integrity: sha512-mbsoUey05WJIOz8U1WzNdf+6UMYGwE3fZZnQqsM22FZ3wh1N887HT6jAOjXs6CNEK3Ntu2OBsyQDXfIjouI4dw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.52.0': resolution: {integrity: sha512-qP6aP970bucEi5KKKR4AuPFd8aTx9EF6BvutvYxmZuWLJHmnq4LvBfp0U+yFDMGwJ+AIJEH5sIP+SNypauMWzg==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.52.0': resolution: {integrity: sha512-nmSVN+F2i1yKZ7rJNKO3G7ZzmxJgoQBQZ/6c4MuS553Grmr7WqR7LLDcYG53Z2m9409z3JLt4sCOhLdbKQ3HmA==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.52.0': resolution: {integrity: sha512-2d0qRo33G6TfQVjaMR71P+yJVGODrt5V6+T0BDYH4EMfGgdC/2HWDVjSSFw888GSzAZUwuska3+zxNUCDco6rQ==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openharmony-arm64@4.52.0': resolution: {integrity: sha512-A1JalX4MOaFAAyGgpO7XP5khquv/7xKzLIyLmhNrbiCxWpMlnsTYr8dnsWM7sEeotNmxvSOEL7F65j0HXFcFsw==} @@ -1610,28 +1596,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.13': resolution: {integrity: sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.13': resolution: {integrity: sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.13': resolution: {integrity: sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.13': resolution: {integrity: sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==} @@ -2284,8 +2266,8 @@ packages: resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} engines: {node: '>=0.10.0'} - baseline-browser-mapping@2.8.6: - resolution: {integrity: sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==} + baseline-browser-mapping@2.8.32: + resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==} hasBin: true bidi-js@1.0.3: @@ -3753,28 +3735,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -7829,7 +7807,7 @@ snapshots: mixin-deep: 1.3.2 pascalcase: 0.1.1 - baseline-browser-mapping@2.8.6: {} + baseline-browser-mapping@2.8.32: {} bidi-js@1.0.3: dependencies: @@ -7892,7 +7870,7 @@ snapshots: browserslist@4.26.2: dependencies: - baseline-browser-mapping: 2.8.6 + baseline-browser-mapping: 2.8.32 caniuse-lite: 1.0.30001743 electron-to-chromium: 1.5.222 node-releases: 2.0.21 diff --git a/src/views/Config/Global/FaceConfig.vue b/src/views/Config/Global/FaceConfig.vue index 712e676..88e4b3d 100644 --- a/src/views/Config/Global/FaceConfig.vue +++ b/src/views/Config/Global/FaceConfig.vue @@ -1,15 +1,15 @@ + + + + + {{ props.title }} + + + + + + + + + + + + diff --git a/src/views/Config/Global/ImageConfig.vue b/src/views/Config/Global/ImageConfig.vue index c8c6936..950bffa 100644 --- a/src/views/Config/Global/ImageConfig.vue +++ b/src/views/Config/Global/ImageConfig.vue @@ -1,12 +1,13 @@
+ {{ desc }} +