diff --git a/__test__/Button.test.ts b/__test__/Button.test.ts deleted file mode 100644 index e08d2ce..0000000 --- a/__test__/Button.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Button from '@/components/Button/index.vue' - -import { shallowMount } from '@vue/test-utils' -import { describe, expect, test } from 'vitest' -// 测试分组 -describe('Button', () => { - // mount - test('Buttons slot text', () => { - // @vue/test-utils - const wrapper = shallowMount(Button, { - slots: { - default: 'Button', - }, - }) - // 断言 - expect(wrapper.text()).toBe('Button') - }) - test('Button click', () => { - const wrapper = shallowMount(Button) - wrapper.trigger('click') - expect(wrapper.emitted('click')).toBeTruthy() - }) - test('Button disabled', () => { - const wrapper = shallowMount(Button, { - props: { - disabled: true, - }, - }) - wrapper.trigger('click') - expect(wrapper.emitted('click')).toBeFalsy() - }) - test('Button not disabled', () => { - const wrapper = shallowMount(Button, { - props: { - disabled: false, - }, - }) - wrapper.trigger('click') - expect(wrapper.emitted('click')).toBeTruthy() - }) -}) diff --git a/__test__/Lottery.test.ts b/__test__/Lottery.test.ts deleted file mode 100644 index e08d2ce..0000000 --- a/__test__/Lottery.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Button from '@/components/Button/index.vue' - -import { shallowMount } from '@vue/test-utils' -import { describe, expect, test } from 'vitest' -// 测试分组 -describe('Button', () => { - // mount - test('Buttons slot text', () => { - // @vue/test-utils - const wrapper = shallowMount(Button, { - slots: { - default: 'Button', - }, - }) - // 断言 - expect(wrapper.text()).toBe('Button') - }) - test('Button click', () => { - const wrapper = shallowMount(Button) - wrapper.trigger('click') - expect(wrapper.emitted('click')).toBeTruthy() - }) - test('Button disabled', () => { - const wrapper = shallowMount(Button, { - props: { - disabled: true, - }, - }) - wrapper.trigger('click') - expect(wrapper.emitted('click')).toBeFalsy() - }) - test('Button not disabled', () => { - const wrapper = shallowMount(Button, { - props: { - disabled: false, - }, - }) - wrapper.trigger('click') - expect(wrapper.emitted('click')).toBeTruthy() - }) -}) diff --git a/__test__/Random.test.ts b/__test__/Random.test.ts new file mode 100644 index 0000000..6741433 --- /dev/null +++ b/__test__/Random.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it } from 'vitest' +import { getRandomElements } from '@/views/Home/utils/random' + +describe('getRandomElements', () => { + // 测试基本功能:从数组中获取指定数量的元素 + it('should return specified number of elements', () => { + const sourceArray = [1, 2, 3, 4, 5] + const result = getRandomElements(sourceArray, 3) + + expect(result).toHaveLength(3) + result.forEach((element) => { + expect(sourceArray).toContain(element) + }) + }) + + // 测试边界情况:count为0 + it('should return empty array when count is 0', () => { + const sourceArray = [1, 2, 3] + const result = getRandomElements(sourceArray, 0) + + expect(result).toEqual([]) + }) + + // 测试边界情况:count为负数 + it('should return empty array when count is negative', () => { + const sourceArray = [1, 2, 3] + const result = getRandomElements(sourceArray, -1) + + expect(result).toEqual([]) + }) + + // 测试边界情况:count大于等于数组长度 + it('should return shuffled array when count equals or exceeds array length', () => { + const sourceArray = [1, 2, 3] + const result1 = getRandomElements(sourceArray, 3) + const result2 = getRandomElements(sourceArray, 5) + + expect(result1).toHaveLength(3) + expect(result2).toHaveLength(3) + + // 验证返回的元素与原数组相同 + expect(result1.sort()).toEqual(sourceArray.sort()) + expect(result2.sort()).toEqual(sourceArray.sort()) + }) + + // 测试空数组情况 + it('should return empty array when source array is empty', () => { + const sourceArray: number[] = [] + const result = getRandomElements(sourceArray, 3) + + expect(result).toEqual([]) + }) + + // 测试单元素数组 + it('should handle single element array', () => { + const sourceArray = [42] + const result = getRandomElements(sourceArray, 1) + + expect(result).toEqual([42]) + }) + + // 测试字符串数组 + it('should work with string arrays', () => { + const sourceArray = ['a', 'b', 'c', 'd', 'e'] + const result = getRandomElements(sourceArray, 2) + + expect(result).toHaveLength(2) + result.forEach((element) => { + expect(sourceArray).toContain(element) + }) + }) + + // 测试对象数组 + it('should work with object arrays', () => { + const sourceArray = [ + { id: 1, name: 'Alice' }, + { id: 2, name: 'Bob' }, + { id: 3, name: 'Charlie' }, + ] + const result = getRandomElements(sourceArray, 2) + + expect(result).toHaveLength(2) + result.forEach((element) => { + expect(sourceArray).toContain(element) + }) + }) + + // 测试多次调用应产生不同结果(概率性测试) + it('should produce different results on multiple calls', () => { + const sourceArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] + const results = new Set() + + // 多次调用并收集结果 + for (let i = 0; i < 10; i++) { + const result = getRandomElements(sourceArray, 5).sort().join(',') + results.add(result) + } + + // 虽然有极小概率会相同,但大多数情况下应该有不同的结果 + expect(results.size).toBeGreaterThan(1) + }) + // 多次调用,每个元素抽中的概率基本上相等 + it('should have approximately equal probabilities for each element', () => { + const sourceArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] + const times = 200000 // 次数 + const count = 5 // 抽奖个数 + const expectedProbability = count / sourceArray.length + const elementCounts = new Map() + + // 多次调用并统计元素出现的次数 + for (let i = 0; i < times; i++) { + const result = getRandomElements(sourceArray, count) + result.forEach((element) => { + const count = elementCounts.get(element) || 0 + elementCounts.set(element, count + 1) + }) + } + elementCounts.forEach((value) => { + // 验证每个元素出现的概率接近相等 + const probability = value / times + expect(probability).toBeCloseTo(expectedProbability, 2) + }) + }) +}) diff --git a/__test__/Request.test.ts b/__test__/Request.test.ts deleted file mode 100644 index 497f810..0000000 --- a/__test__/Request.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -// test axios request -import Request from '@/api/request'; -import { describe, it, expect, vi } from 'vitest'; -import { mount, flushPromises } from '@vue/test-utils'; -import axios from 'axios'; - -const fn = vi.fn(); -const mockRes = { - data: { - code: 200, - success: true, - message: 'success', - data: { - name: 'test', - age: 18, - }, - }, -}; -fn(mockRes); -fn.mock.calls[0] === [mockRes]; - -describe('Request', () => { - it('should return data when request success', async () => { - const request = new Request(); - const res = await request({ - url: '/test', - method: 'GET', - }); - expect(res).toEqual(mockRes.data); - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd00353..b7bd84d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1392,36 +1392,42 @@ 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==} @@ -1492,56 +1498,67 @@ 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==} @@ -1623,24 +1640,28 @@ 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==} @@ -3854,24 +3875,28 @@ 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==} diff --git a/src/types/storeType.ts b/src/types/storeType.ts index 799d4b8..9d0d49f 100644 --- a/src/types/storeType.ts +++ b/src/types/storeType.ts @@ -1,53 +1,54 @@ export interface IPersonConfig { - id: number; - uid: string; - name: string; - department: string; - identity: string; - avatar: string; - isWin: boolean; - x: number; + id: number + uid: string + uuid: string + name: string + department: string + identity: string + avatar: string + isWin: boolean + x: number y: number - createTime: string; - updateTime: string; - prizeName: string[]; - prizeId: string[]; - prizeTime: string[]; + createTime: string + updateTime: string + prizeName: string[] + prizeId: string[] + prizeTime: string[] } export interface Separate { - id: string - count: number - isUsedCount: number + id: string + count: number + isUsedCount: number } export interface IPrizeConfig { - id: number | string - name: string - sort: number - isAll: boolean - count: number - isUsedCount: number - picture: { - id: string | number + id: number | string name: string - url: string - } - separateCount: { - enable: boolean - countList: Separate[] - } - desc: string - isShow: boolean - isUsed: boolean - frequency: number + sort: number + isAll: boolean + count: number + isUsedCount: number + picture: { + id: string | number + name: string + url: string + } + separateCount: { + enable: boolean + countList: Separate[] + } + desc: string + isShow: boolean + isUsed: boolean + frequency: number } export interface IMusic { - id: string - name: string - url: string + id: string + name: string + url: string } export interface IImage { - id: string - name: string - url: string + id: string + name: string + url: string } diff --git a/src/utils/index.ts b/src/utils/index.ts index 046d246..9138357 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,5 @@ import dayjs from 'dayjs' - +import { v4 as uuidv4 } from 'uuid' /** * @description: 处理表格数据,添加x,y,id等信息 * @param tableData 表格数据 @@ -32,6 +32,7 @@ export function addOtherInfo(personList: any[]) { personList[i].prizeTime = [] as string[] personList[i].prizeId = [] personList[i].isWin = false + personList[i].uuid = uuidv4() } return personList diff --git a/src/views/Home/components/HeaderTitle/index.vue b/src/views/Home/components/HeaderTitle/index.vue index 03e6a4d..24e51a4 100644 --- a/src/views/Home/components/HeaderTitle/index.vue +++ b/src/views/Home/components/HeaderTitle/index.vue @@ -10,6 +10,7 @@ interface Props { topTitle: string tableData: any[] setDefaultPersonList: () => void + isInitialDone: boolean } const props = defineProps() @@ -26,7 +27,7 @@ const { t } = useI18n() > {{ topTitle }} -
+
+ +
+ + 加载中 +
diff --git a/src/views/Home/index.vue b/src/views/Home/index.vue index 70f295d..23fbe32 100644 --- a/src/views/Home/index.vue +++ b/src/views/Home/index.vue @@ -9,7 +9,7 @@ import { useViewModel } from './useViewModel' import 'vue-toast-notification/dist/theme-sugar.css' const viewModel = useViewModel() -const { setDefaultPersonList, tableData, currentStatus, enterLottery, stopLottery, containerRef, startLottery, continueLottery, quitLottery } = viewModel +const { setDefaultPersonList, tableData, currentStatus, enterLottery, stopLottery, containerRef, startLottery, continueLottery, quitLottery, isInitialDone } = viewModel const globalConfig = useStore().globalConfig const { getTopTitle: topTitle, getTextColor: textColor, getTextSize: textSize, getBackground: homeBackground } = storeToRefs(globalConfig) @@ -22,6 +22,7 @@ const { getTopTitle: topTitle, getTextColor: textColor, getTextSize: textSize, g :text-color="textColor" :top-title="topTitle" :set-default-person-list="setDefaultPersonList" + :is-initial-done="isInitialDone" />
([]) const intervalTimer = ref(null) + const isInitialDone = ref(false) - function init() { + function initThreeJs() { const felidView = 40 const width = window.innerWidth const height = window.innerHeight @@ -358,14 +359,22 @@ export function useViewModel() { } } 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) + // 重构抽奖函数 + luckyTargets.value = getRandomElements(personPool.value, luckyCount.value) + luckyTargets.value.forEach((item) => { + const index = personPool.value.findIndex(person => person.id === item.id) + if (index > -1) { + personPool.value.splice(index, 1) } - } + }) + // 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}人`, @@ -575,13 +584,33 @@ export function useViewModel() { // 刷新页面 window.location.reload() } + const init = () => { + const startTime = Date.now() + const maxWaitTime = 2000 // 2秒 + + const checkAndInit = () => { + // 如果人员列表有数据或者等待时间超过2秒,则执行初始化 + if (allPersonList.value.length > 0 || (Date.now() - startTime) >= maxWaitTime) { + console.log('初始化完成') + tableData.value = initTableData({ allPersonList: allPersonList.value, rowCount: rowCount.value }) + initThreeJs() + animation() + containerRef.value!.style.color = `${textColor}` + randomBallData() + window.addEventListener('keydown', listenKeyboard) + isInitialDone.value = true + } + else { + console.log('等待人员列表数据...') + // 继续等待 + setTimeout(checkAndInit, 100) // 每100毫秒检查一次 + } + } + + checkAndInit() + } onMounted(() => { - tableData.value = initTableData({ allPersonList: allPersonList.value, rowCount: rowCount.value }) init() - animation() - containerRef.value!.style.color = `${textColor}` - randomBallData() - window.addEventListener('keydown', listenKeyboard) }) onUnmounted(() => { nextTick(() => { @@ -591,6 +620,11 @@ export function useViewModel() { intervalTimer.value = null window.removeEventListener('keydown', listenKeyboard) }) + // watch(() => allPersonList.value, (newVal) => { + // if (newVal.length) { + // init() + // } + // }) return { setDefaultPersonList, @@ -602,5 +636,6 @@ export function useViewModel() { enterLottery, tableData, currentStatus, + isInitialDone, } } diff --git a/src/views/Home/utils/index.ts b/src/views/Home/utils/index.ts new file mode 100644 index 0000000..4f2e586 --- /dev/null +++ b/src/views/Home/utils/index.ts @@ -0,0 +1,2 @@ +export * from './random' +export * from './table' diff --git a/src/views/Home/utils/random.ts b/src/views/Home/utils/random.ts new file mode 100644 index 0000000..acb2dd1 --- /dev/null +++ b/src/views/Home/utils/random.ts @@ -0,0 +1,53 @@ +/** + * 浏览器端加密安全洗牌(无需指定抽取数量) + * @param array 要洗牌的数组 + * @returns 洗牌后的新数组 + */ +function shuffleBrowserCrypto(array: T[]): T[] { + const newArray = [...array] + if (newArray.length <= 1) + return newArray + + // 遍历数组,每轮生成一个随机索引 + for (let i = newArray.length - 1; i > 0; i--) { + // 步骤1:生成一个 32 位无符号加密随机数(仅需1个) + const randomBuffer = new Uint32Array(1) // 长度1表示只生成1个随机数 + crypto.getRandomValues(randomBuffer) + + // 步骤2:将随机数映射到 [0, i] 范围(核心:动态适配当前i的范围) + const randomIndex = randomBuffer[0] % (i + 1); + + // 步骤3:交换元素 + [newArray[i], newArray[randomIndex]] = [newArray[randomIndex], newArray[i]] + } + return newArray +} + +/** + * @description 从源数组中随机获取指定数量的元素 + * @param {Array} sourceArray 源数组 + * @param {number} count 要获取的元素数量 + * @returns {Array} 随机获取的元素 + */ + +export function getRandomElements(sourceArray: T[], count: number): T[] { + if (count <= 0) + return [] + if (count >= sourceArray.length) { + return shuffleBrowserCrypto([...sourceArray]) + } // 抽全部=洗牌 + + const newArray = [...sourceArray] + const result: T[] = [] + + // 抽取 count 个元素,每轮选一个随机索引加入结果,然后从原数组移除 + for (let i = 0; i < count; i++) { + const randomBuffer = new Uint32Array(1) + crypto.getRandomValues(randomBuffer) + const randomIndex = randomBuffer[0] % newArray.length + + result.push(newArray[randomIndex]) + } + + return result +} diff --git a/src/views/Home/utils/table.ts b/src/views/Home/utils/table.ts new file mode 100644 index 0000000..1c3cea1 --- /dev/null +++ b/src/views/Home/utils/table.ts @@ -0,0 +1,131 @@ +import type { IPersonConfig } from '@/types/storeType' +import confetti from 'canvas-confetti' +import { Object3D, Vector3 } from 'three' +import { filterData } from '@/utils' +/** + * @description 初始化表格数据 + * @param0 allPersonList 所有人的列表 + * @param1 rowCount 行数,默认是7行 + * @returns 表格数据 + */ +export function initTableData({ allPersonList, rowCount }: { allPersonList: IPersonConfig[], rowCount: number }): IPersonConfig[] { + let tableData: IPersonConfig[] = [] + if (allPersonList.length <= 0) { + return [] + } + const totalCount = rowCount * 7 + const allPersonLength = allPersonList.length + if (allPersonLength < totalCount) { + tableData = Array.from({ length: totalCount }, () => JSON.parse(JSON.stringify(allPersonList))).flat() + } + else { + tableData = allPersonList.slice(0, totalCount) + } + tableData = filterData(tableData.slice(0, totalCount), rowCount) + return tableData +} + +/** + * @description 横铺图形:处理数据,把每个卡片在界面的位置写入 + * @param0 tableData 表格数据 + * @param1 rowCount 每行有多少个元素 + * @param2 cardSize 卡片的大小 + * @returns Object3D[] + */ +export function createTableVertices({ tableData, rowCount, cardSize }: { tableData: IPersonConfig[], rowCount: number, cardSize: { width: number, height: number } }): Object3D[] { + const tableLen = tableData.length + const objects: Object3D[] = [] + for (let i = 0; i < tableLen; i++) { + const object = new Object3D() + + object.position.x = tableData[i].x * (cardSize.width + 40) - rowCount * 90 + object.position.y = -tableData[i].y * (cardSize.height + 20) + 1000 + object.position.z = 0 + objects.push(object) + // targets.table.push(object) + } + return objects +} +/** + * @description 创建球体 + * @param0 objectsLength 物体的个数 + * @returns Object3D[] + */ +export function createSphereVertices({ objectsLength }: { objectsLength: number }): Object3D[] { + let i = 0 + const resObjects: Object3D[] = [] + // const objLength = objects.value.length + const vector = new Vector3() + + for (; i < objectsLength; ++i) { + const phi = Math.acos(-1 + (2 * i) / objectsLength) + const theta = Math.sqrt(objectsLength * Math.PI) * phi + const object = new Object3D() + + object.position.x = 800 * Math.cos(theta) * Math.sin(phi) + object.position.y = 800 * Math.sin(theta) * Math.sin(phi) + object.position.z = -800 * Math.cos(phi) + + // rotation object + vector.copy(object.position).multiplyScalar(2) + object.lookAt(vector) + resObjects.push(object) + } + return resObjects +} + +export function confettiFire() { + const duration = 3 * 1000 + const end = Date.now() + duration; + (function frame() { + // launch a few confetti from the left edge + confetti({ + particleCount: 2, + angle: 60, + spread: 55, + origin: { x: 0 }, + }) + // and launch a few from the right edge + confetti({ + particleCount: 2, + angle: 120, + spread: 55, + origin: { x: 1 }, + }) + + // keep going until we are out of time + if (Date.now() < end) { + requestAnimationFrame(frame) + } + }()) + centerFire(0.25, { + spread: 26, + startVelocity: 55, + }) + centerFire(0.2, { + spread: 60, + }) + centerFire(0.35, { + spread: 100, + decay: 0.91, + scalar: 0.8, + }) + centerFire(0.1, { + spread: 120, + startVelocity: 25, + decay: 0.92, + scalar: 1.2, + }) + centerFire(0.1, { + spread: 120, + startVelocity: 45, + }) +} +function centerFire(particleRatio: number, opts: any) { + const count = 200 + confetti({ + origin: { y: 0.7 }, + ...opts, + particleCount: Math.floor(count * particleRatio), + }) +}