feat: 国际化

This commit is contained in:
LOG1997
2025-01-08 00:19:29 +08:00
64 changed files with 5792 additions and 3520 deletions

View File

@@ -1,12 +0,0 @@
.md
node_modules
public
package.json
*.yaml
.gitignore
.eslintrc*
.babelrc
.eslintignore
.commitlintrc*
.env*
tsconfig*

View File

@@ -1,57 +0,0 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:vue/vue3-essential",
"plugin:@typescript-eslint/recommended",
],
overrides: [],
parser: "vue-eslint-parser",
parserOptions: {
ecmaVersion: "latest",
parser: "@typescript-eslint/parser",
sourceType: "module",
},
plugins: ["vue", "@typescript-eslint"],
rules: {
"@typescript-eslint/ban-types": [
"error",
{
extendDefaults: true,
types: {
"{}": false,
},
},
],
// 关闭typescript类型为any的警告
"@typescript-eslint/no-explicit-any": ["off"],
// 驼峰命名但忽略index
"vue/multi-word-component-names": [
"error",
{
ignores: ["index"], //需要忽略的组件名
},
],
"no-console": "warn",
"no-debugger": "warn",
// complexity: ["warn", { max: 5 }],
// 禁止使用多个空格
"no-multi-spaces": "error",
// 最大连续空行数
"no-multiple-empty-lines": ["error", { max: 2, maxEOF: 1, maxBOF: 0 }],
// 代码块中去除前后空行
"padded-blocks": ["error", "never"],
// 使用单引号,字符串中包含了一个其它引号 允许"a string containing 'single' quotes"
quotes: ["error", "single", { avoidEscape: true }],
// return之前必须空行
"newline-before-return": "error",
//文件末尾强制换行
"eol-last": ["error", "always"],
//禁止空格和 tab 的混合缩进
"no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
},
};

View File

@@ -4,8 +4,6 @@
name: Node.js CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

3
.gitignore vendored
View File

@@ -6,7 +6,7 @@ yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
pnpm-lock.yaml
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
@@ -128,3 +128,4 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
target

View File

@@ -46,6 +46,7 @@ or
- [x] 🧨 国际化多语言
- [x] 🍃 更换背景图片
- [x] 🚅 添加docker构建
- [x] 😘 弹幕(开发中)
- [ ] 🧵 卡片组成多种形状
...

7
eslint.config.js Normal file
View File

@@ -0,0 +1,7 @@
import antfu from '@antfu/eslint-config'
export default antfu(
{
ignores: ['**/node_modules', '**/public', '**/dist', '**/package.json', '**/*.yaml', '**/.gitignore', '**/.env*', '**/tsconfig*'],
},
)

View File

@@ -12,7 +12,8 @@
"test": "vitest",
"test:ui": "vitest --ui",
"preview": "vite preview",
"lint": "eslint ./src --ext .vue,.js,.ts,.jsx,.tsx --fix"
"lint": "eslint ./src",
"lint:fix": "eslint ./src --fix"
},
"dependencies": {
"@tweenjs/tween.js": "^23.1.2",
@@ -31,6 +32,7 @@
"three-css3d": "^1.0.6",
"vue": "^3.5.13",
"vue-dompurify-html": "^5.2.0",
"vue-i18n": "^10.0.4",
"vue-router": "^4.5.0",
"vue-toast-notification": "^3",
"vue3-colorpicker": "^2.3.0",
@@ -38,6 +40,9 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@antfu/eslint-config": "^3.9.2",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.15.0",
"@iconify-json/ep": "^1.2.1",
"@iconify-json/fluent": "^1.2.8",
"@tailwindcss/typography": "^0.5.15",
@@ -56,6 +61,7 @@
"daisyui": "^4.12.14",
"eslint": "^9.15.0",
"eslint-plugin-vue": "^9.31.0",
"globals": "^15.12.0",
"happy-dom": "^15.11.6",
"husky": "^9.1.7",
"jsdom": "^25.0.1",

1707
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1705479543818" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4421" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M341.333333 618.666667l-179.2 174.933333C185.6 834.133333 149.333333 853.333333 149.333333 853.333333s-27.733333-34.133333-53.333333-64c-14.933333-17.066667-14.933333-40.533333 10.666667-64l149.333333-149.333333 106.666667 42.666667h-21.333334z m554.666667-296.533334s-44.8-12.8-64 17.066667v2.133333l-196.266667 21.333334 8.533334 85.333333 166.4-21.333333s27.733333 2.133333 42.666666-21.333334 42.666667-83.2 42.666667-83.2zM253.866667 541.866667c4.266667-10.666667-2.133333-23.466667-12.8-27.733334-4.266667-2.133333-93.866667-36.266667-93.866667-132.266666 0-12.8-8.533333-21.333333-21.333333-21.333334s-21.333333 8.533333-21.333334 21.333334c0 125.866667 106.666667 174.933333 113.066667 177.066666 2.133333 0 14.933333-2.133333 17.066667-2.133333 8.533333-2.133333 17.066667-6.4 19.2-14.933333zM597.333333 149.333333s-57.6-44.8-106.666666 0c0 0 42.666667 44.8 64 85.333334 93.866667-44.8 42.666667-85.333333 42.666666-85.333334z" fill="#844419" p-id="4422"></path><path d="M896 128m-42.666667 0a42.666667 42.666667 0 1 0 85.333334 0 42.666667 42.666667 0 1 0-85.333334 0Z" fill="#E91E63" p-id="4423"></path><path d="M917.333333 469.333333s-46.933333 53.333333-66.133333 76.8c-17.066667 17.066667-46.933333 4.266667-46.933333 4.266667l-147.2-46.933333c-12.8 17.066667-32 34.133333-59.733334 51.2-40.533333 27.733333-106.666667 51.2-172.8 66.133333L341.333333 746.666667l-42.666666 128c38.4 2.133333 42.666667 42.666667 42.666666 42.666666s-61.866667 4.266667-85.333333 0-51.2-32-42.666667-64l55.466667-183.466666c-6.4-12.8-17.066667-29.866667-34.133333-51.2-59.733333-74.666667 21.333333-128 21.333333-128s206.933333-134.4 256-170.666667c32-23.466667 40.533333-61.866667 42.666667-85.333333 93.866667-44.8 42.666667-85.333333 42.666666-85.333334s-2.133333-2.133333-4.266666-2.133333c12.8-8.533333 27.733333-14.933333 46.933333-19.2 49.066667-12.8 49.066667 21.333333 149.333333 21.333333 49.066667 0 10.666667 83.2-42.666666 106.666667-36.266667 14.933333-85.333333 49.066667-85.333334 85.333333 0 17.066667 17.066667 44.8 21.333334 81.066667l168.533333 49.066667s2.133333-4.266667 4.266667-4.266667c34.133333-25.6 61.866667 2.133333 61.866666 2.133333zM522.666667 185.6c23.466667-19.2 49.066667-40.533333 70.4-40.533333l-12.8-6.4c-21.333333-10.666667-57.6-19.2-89.6 10.666666 0 0-34.133333 44.8-17.066667 61.866667 8.533333 8.533333 27.733333-6.4 49.066667-25.6z" fill="#CC6C25" p-id="4424"></path><path d="M789.333333 149.333333h-21.333333s10.666667 34.133333 38.4 42.666667c8.533333-23.466667 6.4-42.666667-17.066667-42.666667z" fill="#5D4037" p-id="4425"></path><path d="M704 170.666667c0 12.8-8.533333 21.333333-21.333333 21.333333s-21.333333-21.333333-21.333334-21.333333 8.533333-21.333333 21.333334-21.333334 21.333333 8.533333 21.333333 21.333334z" fill="#3E2723" p-id="4426"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 256 256"><path fill="#FFF" d="M128 249.018C61.09 249.018 6.982 194.91 6.982 128S61.09 6.982 128 6.982S249.018 61.091 249.018 128c0 66.91-54.109 121.018-121.018 121.018Z"/><path fill="#3F3F3F" d="M128 14.545c62.836 0 113.455 51.2 113.455 113.455c0 62.255-50.619 113.455-113.455 113.455S14.545 190.836 14.545 128S65.164 14.545 128 14.545ZM128 0C57.018 0 0 57.018 0 128s57.018 128 128 128s128-57.018 128-128S197.818 0 128 0Z"/><path fill="#0AB3E5" d="m74.473 119.273l23.272 18.618l32-52.364l40.728 65.164l22.109-17.455l24.436 36.073c5.818-12.8 10.473-27.927 10.473-42.473c0-55.272-43.636-98.909-98.91-98.909c-55.272 0-100.654 44.8-100.654 98.91c0 17.454 4.655 32 11.637 46.545l34.909-54.11Z"/></svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

31
public/readme-en.md Normal file
View File

@@ -0,0 +1,31 @@
# Operation Guide
## Steps
1. Upon first entry, no data will be displayed. You can choose to use default data to view the overall display effect. It is recommended to import your own data for operation. The steps are as follows:
a. Personnel Configuration - Personnel List - Download Template, download the data template and modify it with your data (please note that the header cannot be modified).
b. After modification, click 'Upload File' on the same page to upload the modified Excel table.
2. Enter the Prize Configuration to modify your prize information. Try to keep the name short for better display; "All Participants" indicates whether this award will be drawn from all participants (those who have already won can still participate); "Winners" refers to the number of people to be drawn for this award; "Already Won" cannot be edited; "Selected" means this award has been used, unselecting it will reset the award but not the winners; "Image" is the prize image displayed on the home page (you can upload images in the image list); "Left Icon" is used to adjust the order of prizes.
Completing the above two steps allows normal use.
## Function Description
1. Add Temporary Draw: There is a '+' button in the prize list on the draw page. Clicking it allows you to add a temporary draw. Note: Only one temporary draw can be added at a time. After adding successfully, the current prize will be set to the temporary prize, and after drawing, it will return to the normal prize list.
2. Music and Image List: You can upload files yourself for use. After uploading images successfully, you can select them in the prize configuration for display. After uploading music successfully, it will be added to the play list.
3. Music Playback: Left-click with the mouse to play/pause, right-click to play the next song.
4. Interface Configuration - Pattern Settings: You can use the mouse to click and customize the highlighted patterns on the home page.
5. If you do not want to display the prize list on the home page, uncheck 'Always Show Prize List' in the interface configuration.
6. When clicking buttons on the home page, the button value will not update immediately but will only update after the animation ends. This is a normal phenomenon.
## Shortcuts
Shortcuts are set up on the draw page.
| Shortcut | Description |
| --- | --- |
| Space | Enter Draw / Start / Draw Lucky Winner / Continue |
| Esc | Cancel |

View File

@@ -1,26 +1,26 @@
<script setup lang="ts">
import { onMounted,ref } from 'vue'
import PlayMusic from '@/components/PlayMusic/index.vue'
import useStore from '@/store'
import { storeToRefs } from 'pinia'
import PlayMusic from '@/components/PlayMusic/index.vue'
import { themeChange } from 'theme-change'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const globalConfig = useStore().globalConfig
const prizeConfig = useStore().prizeConfig
const system=useStore().system
const system = useStore().system
const { getTheme: localTheme } = storeToRefs(globalConfig)
const { getPrizeConfig: prizeList } = storeToRefs(prizeConfig)
// const { getIsMobile: isMobile } = storeToRefs(system)
const tipDialog=ref()
const tipDialog = ref()
// const isMobileValue = ref(structuredClone(isMobile.value))
const setLocalTheme = (theme: any) => {
function setLocalTheme(theme: any) {
themeChange(theme.name)
}
// 设置当前奖列表
const setCurrentPrize = () => {
function setCurrentPrize() {
if (prizeList.value.length <= 0) {
return
}
@@ -31,33 +31,31 @@ const setCurrentPrize = () => {
break
}
}
return
}
// 判断是否手机端访问
const judgeMobile=()=>{
function judgeMobile() {
const ua = navigator.userAgent
const isAndroid = ua.indexOf('Android') > -1 || ua.indexOf('Adr') > -1
const isIOS =!!ua.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
const isAndroid = ua.includes('Android') || ua.includes('Adr')
const isIOS = !!ua.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/)
system.setIsMobile(isAndroid||isIOS)
system.setIsMobile(isAndroid || isIOS)
return isAndroid||isIOS
return isAndroid || isIOS
}
// 判断是否chrome或者edge访问
const judgeChromeOrEdge=()=>{
function judgeChromeOrEdge() {
const ua = navigator.userAgent
const isChrome = ua.indexOf('Chrome') > -1
const isEdge = ua.indexOf('Edg') > -1
const isChrome = ua.includes('Chrome')
const isEdge = ua.includes('Edg')
system.setIsChrome(isChrome)
return isChrome||isEdge
return isChrome || isEdge
}
onMounted(() => {
setLocalTheme(localTheme.value)
setCurrentPrize()
if(judgeMobile()||!judgeChromeOrEdge()){
if (judgeMobile() || !judgeChromeOrEdge()) {
tipDialog.value.showModal()
}
})
@@ -66,19 +64,27 @@ onMounted(() => {
<template>
<dialog id="my_modal_1" ref="tipDialog" class="border-none modal">
<div class="modal-box">
<h3 class="text-lg font-bold">提示!</h3>
<p class="py-4" v-if="judgeMobile()">请使用PC进行访问以获得最佳显示效果</p>
<p class="py-4" v-if=" !judgeChromeOrEdge()">请使用最新版Chrome或者Edge浏览器</p>
<h3 class="text-lg font-bold">
{{ t('dialog.titleTip') }}
</h3>
<p v-if="judgeMobile()" class="py-4">
{{ t('dialog.dialogPCWeb') }}
</p>
<p v-if=" !judgeChromeOrEdge()" class="py-4">
{{ t('dialog.dialogLatestBrowser') }}
</p>
<div class="modal-action">
<form method="dialog" class="flex justify-start w-full gap-3">
<!-- if there is a button in form, it will close the modal -->
<button class="btn">确定</button>
<button class="btn">
{{ t('button.confirm') }}
</button>
</form>
</div>
</div>
</dialog>
<router-view></router-view>
<PlayMusic class="absolute right-0 bottom-1/2"></PlayMusic>
<router-view />
<PlayMusic class="absolute right-0 bottom-1/2" />
</template>
<style scoped lang="scss"></style>

View File

@@ -1,61 +1,62 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import type { InternalAxiosRequestConfig } from 'axios';
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import axios from 'axios'
class Request {
private instance: AxiosInstance;
private instance: AxiosInstance
constructor(config: AxiosRequestConfig) {
this.instance = axios.create({
baseURL: '/api',
timeout: 10000,
...config,
});
})
// 添加请求拦截器
this.instance.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
// 在发送请求之前做些什么
console.log('请求拦截器被触发');
console.log('请求拦截器被触发')
return config;
return config
},
(error: any) => {
// 对请求错误做些什么
console.error('请求拦截器发生错误:', error);
console.error('请求拦截器发生错误:', error)
return Promise.reject(error);
}
);
return Promise.reject(error)
},
)
// 添加响应拦截器
this.instance.interceptors.response.use(
(response: AxiosResponse) => {
// 对响应数据做些什么
console.log('响应拦截器被触发');
const reponseData = response.data;
console.log('响应拦截器被触发')
const responseData = response.data
return reponseData;
return responseData
},
(error: any) => {
// 对响应错误做些什么
console.error('响应拦截器发生错误:', error);
console.error('响应拦截器发生错误:', error)
return Promise.reject(error);
}
);
return Promise.reject(error)
},
)
}
public async request<T>(config: AxiosRequestConfig): Promise<T> {
const response: AxiosResponse<T> = await this.instance.request(config);
const response: AxiosResponse<T> = await this.instance.request(config)
return response.data;
return response.data
}
}
// 函数
function request<T>(config: AxiosRequestConfig): Promise<T> {
const instance = new Request(config);
const instance = new Request(config)
return instance.request(config);
return instance.request(config)
}
export default request;
export default request

View File

@@ -1,16 +1,18 @@
<script setup lang='ts'>
import { computed } from 'vue';
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps({
data: {
type: Array as any,
default: [] as any[]
default: [] as any[],
},
tableColumns: {
type: Array,
default: [] as any[]
default: [] as any[],
},
})
const { t } = useI18n()
const dataColumns = computed<any[]>(() => {
// 不带有actions的列
const columns = props.tableColumns.filter((item: any) => !item.actions)
@@ -24,7 +26,6 @@ const actionsColumns = computed<any[]>(() => {
return columns
})
</script>
<template>
@@ -33,15 +34,19 @@ const actionsColumns = computed<any[]>(() => {
<!-- head -->
<thead>
<tr>
<th></th>
<th v-for="(item, index) in dataColumns" :key="index">{{ item.label }}</th>
<th v-for="(item, index) in actionsColumns" :key="index">操作</th>
<th></th>
<th />
<th v-for="(item, index) in dataColumns" :key="index">
{{ item.label }}
</th>
<th v-for="(item, index) in actionsColumns" :key="index">
{{ t('table.operation') }}
</th>
<th />
</tr>
</thead>
<tbody v-if="data.length > 0">
<!-- row -->
<tr class="hover" v-for="item in data" :key="item.id">
<tr v-for="item in data" :key="item.id" class="hover">
<th>{{ item.id }}</th>
<td v-for="(column, index) in dataColumns" :key="index">
<span v-if="column.formatValue">{{ column.formatValue(item) }}</span>
@@ -49,19 +54,23 @@ const actionsColumns = computed<any[]>(() => {
</td>
<!-- action -->
<td v-for="(column, index) in actionsColumns" :key="index" class="flex gap-2">
<button class="btn btn-xs" v-for="action in column.actions" :key="action.name" :class="action.type"
@click="action.onClick(item)">{{ action.label }}</button>
<button
v-for="action in column.actions" :key="action.name" class="btn btn-xs" :class="action.type"
@click="action.onClick(item)"
>
{{ action.label }}
</button>
</td>
</tr>
</tbody>
<tbody v-else>
<tr>
<td colspan="5" class="text-center">暂无数据</td>
<td colspan="5" class="text-center">
{{ t('table.noneData') }}
</td>
</tr>
</tbody>
<!-- foot -->
</table>
</div>
</template>

View File

@@ -1,17 +1,19 @@
<script setup lang="ts">
import { ref } from 'vue';
import { ref } from 'vue'
defineProps<{ msg: string }>();
defineProps<{ msg: string }>()
const count = ref(0);
const addCount = () => {
count.value++;
};
const count = ref(0)
function addCount() {
count.value++
}
</script>
<template>
<div>
<h1 class="text-4xl font-bold py-6">{{ msg }}</h1>
<h1 class="text-4xl font-bold py-6">
{{ msg }}
</h1>
<div class="card w-1200px">
<button
@@ -29,16 +31,16 @@ const addCount = () => {
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank">create-vue</a>, the official Vue + Vite starter
</p>
<p>
Install
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
in your IDE for a better DX
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
<p class="read-the-docs">
Click on the Vite and Vue logos to learn more
</p>
</div>
</template>

View File

@@ -1,41 +1,40 @@
<script setup lang='ts'>
import {ref,onMounted} from 'vue'
import localforage from 'localforage'
const props=defineProps({
import { onMounted, ref } from 'vue'
const props = defineProps({
imgItem: {
type:Object,
default:()=>({})
type: Object,
default: () => ({}),
},
})
const imageDbStore = localforage.createInstance({
name: 'imgStore'
name: 'imgStore',
})
const imgUrl=ref('')
const imgUrl = ref('')
const getImageStoreItem=async (item:any):Promise<string>=>{
let image=''
if(item.url=='Storage'){
const key=item.id;
image=await imageDbStore.getItem(key) as string
async function getImageStoreItem(item: any): Promise<string> {
let image = ''
if (item.url === 'Storage') {
const key = item.id
image = await imageDbStore.getItem(key) as string
}
else{
image=item.url
else {
image = item.url
}
return image
return image
}
onMounted(async ()=>{
const image=await getImageStoreItem(props.imgItem)
imgUrl.value=image
onMounted(async () => {
const image = await getImageStoreItem(props.imgItem)
imgUrl.value = image
})
</script>
<template>
<img :src="imgUrl" alt="Image" class="object-cover h-full rounded-xl"/>
<img :src="imgUrl" alt="Image" class="object-cover h-full rounded-xl">
</template>
<style lang='scss' scoped>

View File

@@ -1,25 +1,25 @@
<script setup lang='ts'>
import { ref, watch, onMounted, toRefs } from 'vue'
import { Separate } from '@/types/storeType'
import type { Separate } from '@/types/storeType'
import { onMounted, ref, toRefs, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const props = defineProps({
totalNumber: {
type: Number,
default: 0
default: 0,
},
separatedNumber: {
type: Array<Separate>,
default: []
}
default: [],
},
})
const emits = defineEmits(['submitData'])
const { t } = useI18n()
const separatedNumberRef = ref()
const { separatedNumber, totalNumber } = toRefs(props)
const scaleList = ref<number[]>([])
const editScale = (item: number) => {
if (item == totalNumber.value) {
function editScale(item: number) {
if (item === totalNumber.value) {
return
}
if (scaleList.value.includes(item)) {
@@ -32,12 +32,12 @@ const editScale = (item: number) => {
scaleList.value.sort((a, b) => a - b)
}
}
const clearData = () => {
function clearData() {
emits('submitData', separatedNumber.value)
separatedNumberRef.value.close()
}
watch(scaleList, (val: number[]) => {
separatedNumber.value.length=0
separatedNumber.value.length = 0
for (let i = 1; i < scaleList.value.length; i++) {
separatedNumber.value[i - 1] = {
id: i.toString(),
@@ -53,11 +53,11 @@ watch(totalNumber, (val) => {
}
separatedNumberRef.value.showModal()
// scaleList.value = [0, val]
scaleList.value = new Array(separatedNumber.value.length + 1).fill(totalNumber.value)
scaleList.value = Array.from({ length: separatedNumber.value.length + 1 }).fill(totalNumber.value) as number[]
for (let i = separatedNumber.value.length - 1; i >= 0; i--) {
scaleList.value[i] = scaleList.value[i + 1] - separatedNumber.value[i].count
}
if(scaleList.value[0]!==0){
if (scaleList.value[0] !== 0) {
scaleList.value.unshift(0)
}
})
@@ -74,22 +74,34 @@ onMounted(() => {
<template>
<dialog id="my_modal_1" ref="separatedNumberRef" class="z-50 overflow-hidden border-none modal">
<div class="overflow-hidden modal-box">
<h3 class="pb-6 text-lg font-bold">提示!</h3>
<p class="pb-8">单次抽取只能抽取10位</p>
<h3 class="pb-6 text-lg font-bold">
{{ t('dialog.titleTip') }}
</h3>
<p class="pb-8">
{{ t('dialog.dialogSingleDrawLimit') }}
</p>
<div class="flex justify-between px-3 text-center separated-number">
<div v-for="item in props.totalNumber" :key="item"
class="relative flex flex-col items-center cursor-pointer">
<div class="absolute mb-12 text-center tooltip -top-5 hover:text-lg" data-tip="左键切割"
@click.left="editScale(item)">
<div
v-for="item in props.totalNumber" :key="item"
class="relative flex flex-col items-center cursor-pointer"
>
<div
class="absolute mb-12 text-center tooltip -top-5 hover:text-lg" :data-tip="t('tooltip.leftClick')"
@click.left="editScale(item)"
>
<span> {{ item }}</span>
</div>
<div class="text-center" :class="scaleList.includes(item) ? 'text-red-500 font-extrabold' : ''">|</div>
<div class="text-center" :class="scaleList.includes(item) ? 'text-red-500 font-extrabold' : ''">
|
</div>
</div>
</div>
<div class="modal-action">
<form method="dialog">
<!-- if there is a button in form, it will close the modal -->
<button class="btn" @click="clearData">关闭</button>
<button class="btn" @click="clearData">
{{ t('button.close') }}
</button>
</form>
</div>
</div>

View File

@@ -1,23 +1,26 @@
<script setup lang='ts'>
import { ref, onMounted, onUnmounted,watch } from 'vue'
import useStore from '@/store';
import { storeToRefs } from 'pinia';
import useStore from '@/store'
import localforage from 'localforage'
import { useRouter, useRoute } from 'vue-router';
import { storeToRefs } from 'pinia'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const audioDbStore = localforage.createInstance({
name: 'audioStore'
name: 'audioStore',
})
const audio=ref(new Audio())
const audio = ref(new Audio())
const settingRef = ref()
// const audio = ref(new Audio())
const globalConfig = useStore().globalConfig
const { getMusicList: localMusicList,getCurrentMusic:currentMusic } = storeToRefs(globalConfig);
const { getMusicList: localMusicList, getCurrentMusic: currentMusic } = storeToRefs(globalConfig)
// const localMusicListValue = ref(localMusicList)
const play = async (item: any) => {
if(!item){
async function play(item: any) {
if (!item) {
return
}
// if (!audio.value.paused && !skip) {
@@ -29,7 +32,7 @@ const play = async (item: any) => {
if (!item.url) {
return
}
if (item.url == 'Storage') {
if (item.url === 'Storage') {
audioUrl = await audioDbStore.getItem(item.name) as string
}
else {
@@ -39,42 +42,42 @@ const play = async (item: any) => {
audio.value.src = audioUrl
audio.value.play()
}
const playMusic=(item:any,skip = false)=>{
if(!item){
function playMusic(item: any, skip = false) {
if (!item) {
return
}
if(!currentMusic.value.paused&&!skip){
globalConfig.setCurrentMusic(item,true)
if (!currentMusic.value.paused && !skip) {
globalConfig.setCurrentMusic(item, true)
return
return
}
globalConfig.setCurrentMusic(item,false)
globalConfig.setCurrentMusic(item, false)
}
const nextPlay = () => {
function nextPlay() {
// 播放下一首
if (localMusicList.value.length >= 1) {
let index = localMusicList.value.findIndex((item: any) => item.name == currentMusic.value.item.name)
let index = localMusicList.value.findIndex((item: any) => item.name === currentMusic.value.item.name)
index++
if (index >= localMusicList.value.length) {
index = 0
}
globalConfig.setCurrentMusic(localMusicList.value[index],false)
globalConfig.setCurrentMusic(localMusicList.value[index], false)
}
}
// 监听播放成后开始下一首
const onPlayEnd = () => {
function onPlayEnd() {
audio.value.addEventListener('ended', nextPlay)
}
const enterConfig = () => {
function enterConfig() {
router.push('/log-lottery/config')
}
const enterHome = () => {
function enterHome() {
router.push('/log-lottery')
}
onMounted(() => {
globalConfig.setCurrentMusic(localMusicList.value[0],true)
globalConfig.setCurrentMusic(localMusicList.value[0], true)
onPlayEnd()
// 不使用空格控制audio
})
@@ -82,38 +85,42 @@ onUnmounted(() => {
audio.value.removeEventListener('ended', nextPlay)
})
watch(currentMusic, (val: any) => {
if(!val.paused&&audio.value){
if (!val.paused && audio.value) {
play(val.item)
}
else{
else {
audio.value.pause()
}
},{deep:true})
}, { deep: true })
</script>
<template>
<div class="flex flex-col gap-3" ref="settingRef">
<div v-if="route.path.includes('/config')" class="tooltip tooltip-left" data-tip="主页">
<div class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
@click="enterHome">
<svg-icon name="home"></svg-icon>
<div ref="settingRef" class="flex flex-col gap-3">
<div v-if="route.path.includes('/config')" class="tooltip tooltip-left" :data-tip="t('tooltip.toHome')">
<div
class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
@click="enterHome"
>
<svg-icon name="home" />
</div>
</div>
<div v-else class="tooltip tooltip-left" data-tip="设置/配置">
<div class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
@click="enterConfig">
<svg-icon name="setting"></svg-icon>
<div v-else class="tooltip tooltip-left" :data-tip="t('tooltip.settingConfiguration')">
<div
class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
@click="enterConfig"
>
<svg-icon name="setting" />
</div>
</div>
<div class="tooltip tooltip-left" :data-tip="currentMusic.item ? currentMusic.item.name+'\n\r &nbsp; 右键下一曲' : '没有音乐可以播放'">
<div class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
@click="playMusic(currentMusic.item)" @click.right.prevent="nextPlay">
<svg-icon :name="currentMusic.paused ? 'play' : 'pause'"></svg-icon>
<div class="tooltip tooltip-left" :data-tip="currentMusic.item ? `${currentMusic.item.name}\n\r ${t('tooltip.nextSong')}` : t('tooltip.noSongPlay')">
<div
class="flex items-center justify-center w-10 h-10 p-0 m-0 cursor-pointer setting-container bg-slate-500/50 rounded-l-xl hover:bg-slate-500/80 hover:text-blue-400/90"
@click="playMusic(currentMusic.item)" @click.right.prevent="nextPlay"
>
<svg-icon :name="currentMusic.paused ? 'play' : 'pause'" />
</div>
</div>
<!-- <div class="bg-blue-300 cursor-pointer" @click="nextPlay">下一首</div> -->
</div>
</template>

View File

@@ -56,7 +56,6 @@ onMounted(() => {
onUnmounted(() => {
window.removeEventListener('resize', listenWindowSize)
})
</script>
<template>

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { computed } from 'vue'
const props = defineProps({
prefix: {
type: String,
@@ -17,10 +18,11 @@ const props = defineProps({
type: String,
default: '24px',
},
});
})
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
const symbolId = computed(() => `#${props.prefix}-${props.name}`)
</script>
<template>
<svg
aria-hidden="true"
@@ -31,6 +33,7 @@ const symbolId = computed(() => `#${props.prefix}-${props.name}`);
<use :xlink:href="symbolId" />
</svg>
</template>
<style scoped>
.svg-icon {
width: 24px;

View File

@@ -3,9 +3,9 @@
</script>
<template>
<div class="fixed z-50 flex items-center justify-center w-10 h-10 rounded-full shadow-lg cursor-pointer right-12 bottom-12 bg-slate-700 hover:bg-slate-600">
<svg-icon name="totop"></svg-icon>
</div>
<div class="fixed z-50 flex items-center justify-center w-10 h-10 rounded-full shadow-lg cursor-pointer right-12 bottom-12 bg-slate-700 hover:bg-slate-600">
<svg-icon name="toTop" />
</div>
</template>
<style lang='scss' scoped>

View File

@@ -1,5 +1,5 @@
export { default as Footer } from './Footer/index.vue'
/**
*title: 自动导出组件
*/
export { default as Header } from './Header/index.vue';
export { default as Footer } from './Footer/index.vue';
export { default as Header } from './Header/index.vue'

View File

@@ -1,5 +1,5 @@
import type { IPersonConfig } from '@/types/storeType'
import { rgba } from '@/utils/color'
import { IPersonConfig } from '@/types/storeType'
export const useElementStyle = (element: any, person: IPersonConfig, index: number, patternList: number[], patternColor: string, cardColor: string, cardSize: { width: number, height: number }, textSize: number, mod: 'default' | 'lucky' | 'sphere' = 'default') => {
if (patternList.includes(index + 1) && mod == 'default') {
@@ -37,14 +37,14 @@ export const useElementStyle = (element: any, person: IPersonConfig, index: numb
element.children[0].textContent = person.uid;
}
element.children[1].style.fontSize = textSize + 'px'
element.children[1].style.lineHeight = textSize * 3 + 'px'
element.children[1].style.fontSize = `${textSize}px`
element.children[1].style.lineHeight = `${textSize * 3}px`
element.children[1].style.textShadow = `0 0 12px ${rgba(cardColor, 0.95)}`
if (person.name) {
element.children[1].textContent = person.name
}
element.children[2].style.fontSize = textSize * 0.5 + 'px'
element.children[2].style.fontSize = `${textSize * 0.5}px`
if (person.department || person.identity) {
element.children[2].innerHTML = `${person.department ? person.department : ''}<br/>${person.identity ? person.identity : ''}`
}

View File

@@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1704702652534" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4194" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.1953125" height="200"><path d="M1013.64736 885.56544H11.13088c-6.12352 0-11.13088 5.00736-11.13088 11.14112v83.53792c0 6.12352 5.00736 11.14112 11.13088 11.14112h1002.50624c6.13376 0 11.14112-5.0176 11.14112-11.14112v-83.53792c0.01024-6.12352-4.99712-11.14112-11.13088-11.14112zM338.33984 253.00992h102.90176v471.04c0 6.12352 5.00736 11.14112 11.14112 11.14112h83.53792c6.12352 0 11.13088-5.0176 11.13088-11.14112v-471.04h103.17824c9.32864 0 14.47936-10.72128 8.76544-17.96096L503.06048 37.61152c-4.4544-5.71392-13.08672-5.71392-17.54112 0L329.5744 234.91584c-5.71392 7.3728-0.5632 18.09408 8.76544 18.09408z m0 0" p-id="4195"></path></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 1024 1024"><path fill="currentColor" d="M572.235 205.282v600.365a30.118 30.118 0 1 1-60.235 0V205.282L292.382 438.633a28.913 28.913 0 0 1-42.646 0a33.43 33.43 0 0 1 0-45.236l271.058-288.045a28.913 28.913 0 0 1 42.647 0L834.5 393.397a33.43 33.43 0 0 1 0 45.176a28.913 28.913 0 0 1-42.647 0l-219.618-233.23z"/></svg>

Before

Width:  |  Height:  |  Size: 951 B

After

Width:  |  Height:  |  Size: 390 B

View File

@@ -7,4 +7,4 @@ export const footerList = {
icon: 'github',
},
],
};
}

View File

@@ -1,19 +1,21 @@
<script setup lang="ts">
import { footerList } from './config';
const skip = (url: string) => {
window.open(url);
};
import { footerList } from './config'
function skip(url: string) {
window.open(url)
}
</script>
<template>
<div class="footer-container">
<ul class="flex justify-center">
<li
v-for="item in footerList.data"
:key="item.id"
@click="skip(item.url)"
class="flex items-center gap-1 cursor-pointer"
@click="skip(item.url)"
>
<svg-icon :name="item.icon"></svg-icon>
<svg-icon :name="item.icon" />
<p>{{ item.name }}</p>
</li>
</ul>

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import { navList } from './config';
const skip = (url: string) => {
window.open(url, '_self');
};
import { navList } from './config'
function skip(url: string) {
window.open(url, '_self')
}
</script>
<template>
@@ -11,24 +12,34 @@ const skip = (url: string) => {
<div class="navbar-start max-lg:w-full">
<div class="dropdown">
<label tabindex="0" class="btn btn-ghost lg:hidden">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h8m-8 6h16" />
<svg
xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h8m-8 6h16"
/>
</svg>
</label>
<ul tabindex="0"
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52 text-lg flex flex-col gap-2">
<li class="cursor-pointer hover:text-gray-100 hover:bg-base-200" v-for="item in navList" :key="item.id" @click="skip(item.url)">{{ item.name }}</li>
<ul
tabindex="0"
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52 text-lg flex flex-col gap-2"
>
<li v-for="item in navList" :key="item.id" class="cursor-pointer hover:text-gray-100 hover:bg-base-200" @click="skip(item.url)">
{{ item.name }}
</li>
</ul>
</div>
<a class="text-xl lg:pl-12 max-lg:mx-auto" href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
<img src="/vite.svg" class="logo" alt="Vite logo">
</a>
</div>
<div class="hidden navbar-center lg:flex">
<ul class="flex gap-10 px-1 text-lg cursor-pointer menu menu-horizontal">
<li class="hover:text-gray-100" v-for="item in navList" :key="item.id" @click="skip(item.url)">{{ item.name }}</li>
<li v-for="item in navList" :key="item.id" class="hover:text-gray-100" @click="skip(item.url)">
{{ item.name }}
</li>
</ul>
</div>
<div class="navbar-end max-lg:w-0">

View File

@@ -1,16 +1,16 @@
<script setup lang="ts">
// import Header from './Header/index.vue';
// import Footer from './Footer/index.vue';
import {ref} from 'vue';
import ToTop from '@/components/ToTop/index.vue'
import { useScroll } from '@vueuse/core'
// import Header from './Header/index.vue';
// import Footer from './Footer/index.vue';
import { ref } from 'vue'
const mainContainer = ref<HTMLElement | null>(null)
const { y} = useScroll(mainContainer)
const { y } = useScroll(mainContainer)
const scrollToTop=()=>{
y.value=0
function scrollToTop() {
y.value = 0
}
</script>
@@ -19,16 +19,16 @@ const scrollToTop=()=>{
<!-- <header class="shadow-2xl head-container h-14">
<Header></Header>
</header> -->
<ToTop @click="scrollToTop" v-if="y>400"></ToTop>
<ToTop v-if="y > 400" @click="scrollToTop" />
<main ref="mainContainer" class="box-content w-screen h-screen overflow-x-hidden overflow-y-auto main-container">
<router-view class="h-full main-container-content"></router-view>
<router-view class="h-full main-container-content" />
</main>
<!-- <footer class="w-screen footer-container">
<Footer></Footer>
</footer> -->
</div>
</template>
<style scoped lang="scss">
</style>

151
src/locales/en.ts Normal file
View File

@@ -0,0 +1,151 @@
export default {
button: {
enterLottery: 'Enter Lottery',
start: 'Start',
selectLucky: 'Draw the Lucky',
continue: 'Continue',
confirm: 'Confirm',
cancel: 'Cancel',
setting: 'Setting',
delete: 'Delete',
allDelete: 'Delete All',
downloadTemplate: 'Download Template',
importData: 'Import Data',
resetData: 'Reset Data',
exportResult: 'Export Result',
add: 'Add',
resetDefault: 'Reset Default',
resetAllData: 'Reset All Data',
clearPattern: 'Clear Pattern',
DefaultPattern: 'Default Pattern',
upload: 'Upload',
reset: 'Reset',
play: 'Play',
setLayout: 'Set Layout',
close: 'Close',
noInfoAndImport: 'No Info and import it',
useDefault: 'Use Default Data',
},
sidebar: {
personConfiguration: 'Person Configuration',
personList: 'Person List',
winnerList: 'Winner List',
prizeConfiguration: 'Prize Configuration',
globalSetting: 'Global Configuration',
viewSetting: 'View Setting',
imagesManagement: 'Images Management',
musicManagement: 'Music Management',
operatingInstructions: 'Operating Instructions',
},
viewTitle: {
personManagement: 'Person Management',
winnerManagement: 'Winner Management',
prizeManagement: 'Prize Management',
globalSetting: 'Global Setting',
operatingInstructions: 'Operating Instructions',
},
table: {
// person configuration
number: 'Number',
name: 'Name',
prizeName: 'Name',
department: 'Department',
identity: 'Identity',
isLucky: 'Is Lucky',
operation: 'Operation',
setLuckyNumber: 'Set Lucky Number',
luckyPeopleNumber: 'Lucky People Number',
detail: 'Detail',
noneData: 'No Data',
// prize configuration
fullParticipation: 'FullParticipation',
numberParticipants: 'NumberParticipants',
isDone: 'is Done',
image: 'Image',
onceNumber: 'Once Number',
time: 'Time',
// view setting
title: 'Main Title',
columnNumber: 'Column Number',
theme: 'Theme',
language: 'Language',
cardColor: 'Card Color',
winnerColor: 'Winner Color',
textColor: 'Text Color',
cardWidth: 'Card Width',
cardHeight: 'Card Height',
textSize: 'Text Size',
highlightColor: 'HighLight Color',
patternSetting: 'Pattern Setting',
alwaysDisplay: 'Always Display Prize List',
selectPicture: 'Select a Picture',
backgroundImage: 'Select Background Image',
},
dialog: {
titleTip: 'Tip!',
titleTemporary: 'Add Temporary Activity',
dialogPCWeb: 'Please use a PC browser to access for optimal display performance',
dialogDelAllPerson: 'This operation will delete all personnel list data. Do you want to continue?',
dialogResetWinner: 'This operation will clear the winning information of personnel. Do you want to continue?',
dialogResetAllData: 'This operation will reset all data. Do you want to continue?',
dialogSingleDrawLimit: 'Only 10 characters can be extracted in a single draw',
dialogLatestBrowser: 'Please use the latest version of Chrome or Edge browser',
tipResetPrize: 'Performing operations may reset data, please proceed with caution',
},
tooltip: {
settingConfiguration: 'Setting/Configuration',
nextSong: 'Right Click to Next Song',
noSongPlay: 'No Song to Play',
prizeList: 'Prize List',
addActivity: 'Add Activity',
downloadTemplateTip: 'After downloading the file, please fill in the data in Excel and save it in xlsx format',
uploadExcelTip: 'Upload the modified Excel file',
leftClick: 'Left Click to Slice',
toHome: 'to Home',
resetLayout: 'This item is time-consuming and performance intensive',
defaultLayout: 'The default pattern setting is valid for 17 columns, please set the number of other columns yourself',
doneCount: 'Number of winners',
edit: 'Edit',
delete: 'Delete',
},
error: {
require: 'required field',
requireNumber: 'please enter a number',
minNumber1: 'the minimum is 1',
maxNumber100: 'the maximum is 100',
uploadSuccess: 'Upload Success',
uploadFail: 'Upload Failed',
notImage: 'Not Image',
personIsAllDone: 'All Person Is Done',
personNotEnough: 'Person Is Not Enough',
startDraw: 'Now Draw {count} {leftover} people',
completeInformation: 'Please provide complete information',
},
placeHolder: {
enterTitle: 'Enter Title',
name: 'Name',
winnerCount: 'Lucky Person Count',
},
data: {
yes: 'Yes',
no: 'No',
number: 'Number',
isWin: 'isWin',
department: 'Department',
name: 'Name',
identity: 'Identity',
prizeName: 'Prize Name',
prizeTime: 'Prize Time',
operation: 'Operation',
delete: 'Delete',
removePerson: 'Remove the Person',
defaultTitle: 'The Prelude to the Six Ministries of the Ming Dynasty Cabinet',
xlsxName: 'personListTemplate-en.xlsx',
readmeName: 'readme-en.md',
},
footer: {
'self-reflection': 'Turn inward and examine yourself when you encounter difficulties in life.',
'thiefEasy': 'Thief difficult mountain thief easily, breaking heart.',
},
}

32
src/locales/i18n.ts Normal file
View File

@@ -0,0 +1,32 @@
// i18n配置
import { createI18n } from 'vue-i18n'
import en from './en'
import zhCn from './zhCn'
export type Language = 'en' | 'zhCn'
export const languageList = [
{
key: 'zhCn',
name: '中文',
flag: 'zh-cn',
},
{
key: 'en',
name: 'English',
flag: 'en-us',
},
]
export const browserLanguage = navigator.language.toLowerCase().includes('zh') ? 'zhCn' : 'en'
const globalConfig = JSON.parse(localStorage.getItem('globalConfig') || '{}').globalConfig || {}
// 创建i18n
const i18n = createI18n({
locale: globalConfig.language || browserLanguage,
legacy: false,
messages: {
zhCn,
en,
},
})
export default i18n

151
src/locales/zhCn.ts Normal file
View File

@@ -0,0 +1,151 @@
export default {
button: {
enterLottery: '进入抽奖',
start: '开始',
selectLucky: '抽取幸运儿',
continue: '继续',
confirm: '确认',
cancel: '取消',
setting: '设置',
delete: '删除',
allDelete: '删除全部',
downloadTemplate: '下载模板',
importData: '导入数据',
resetData: '重置数据',
exportResult: '导出结果',
add: '添加',
resetDefault: '重置为默认',
resetAllData: '重置所有数据',
clearPattern: '清除图案',
DefaultPattern: '默认图案',
upload: '上传',
reset: '重置',
play: '播放',
setLayout: '重设布局',
close: '关闭',
noInfoAndImport: '暂无人员信息,前往导入',
useDefault: '使用默认数据',
},
sidebar: {
personConfiguration: '人员配置',
personList: '人员列表',
winnerList: '中奖人员',
prizeConfiguration: '奖品配置',
globalSetting: '全局配置',
viewSetting: '界面设置',
imagesManagement: '图片管理',
musicManagement: '音乐管理',
operatingInstructions: '操作说明',
},
viewTitle: {
personManagement: '人员管理',
winnerManagement: '已中奖人员管理',
prizeManagement: '奖项配置',
globalSetting: '全局配置',
operatingInstructions: '操作说明',
},
table: {
// person configuration
number: '编号',
name: '姓名',
prizeName: '名称',
department: '部门',
identity: '身份',
isLucky: '是否中奖',
operation: '操作',
setLuckyNumber: '设置中奖人数',
luckyPeopleNumber: '中奖人数',
detail: '详细信息',
noneData: '暂无数据',
// prize configuration
fullParticipation: '全员参加',
numberParticipants: '抽奖人数',
isDone: '已抽取',
image: '图片',
onceNumber: '单次抽取个数',
time: '时间',
// view setting
title: '主标题',
columnNumber: '列数',
theme: '主题',
language: '语言',
cardColor: '卡片颜色',
winnerColor: '中奖卡片颜色',
textColor: '文字颜色',
cardWidth: '卡片宽度',
cardHeight: '卡片高度',
textSize: '文字大小',
highlightColor: '高亮颜色',
patternSetting: '图案设置',
alwaysDisplay: '常显奖项列表',
selectPicture: '选择一张图片',
backgroundImage: '选择背景图片',
},
dialog: {
titleTip: '提示!',
titleTemporary: '增加临时抽奖',
dialogPCWeb: '请使用PC进行访问以获得最佳显示效果',
dialogDelAllPerson: '该操作会删除所有人员数据,是否继续?',
dialogResetWinner: '该操作会清空人员中奖信息,是否继续?',
dialogResetAllData: '该操作会重置所有数据,是否继续?',
dialogSingleDrawLimit: '单次抽取只能抽取10位',
dialogLatestBrowser: '请使用最新版Chrome或者Edge浏览器',
tipResetPrize: '进行操作可能会重置数据,请谨慎操作',
},
tooltip: {
settingConfiguration: '设置/配置',
nextSong: '右键点击下一首',
noSongPlay: '没有音乐可以播放',
prizeList: '奖项列表',
addActivity: '添加抽奖',
downloadTemplateTip: '下载文件后请在excel中填写数据并保存为xlsx格式',
uploadExcelTip: '上传修改好的excel文件',
leftClick: '左键切割',
toHome: '主页',
resetLayout: '该项比较耗费时间和性能',
defaultLayout: '默认图案设置针对17列时有效其他列数请自行设置',
doneCount: '已抽取',
edit: '编辑',
delete: '删除',
},
error: {
require: '必填项',
requireNumber: '请输入数字',
minNumber1: '最小为1',
maxNumber100: '最大为100',
uploadSuccess: '上传成功',
uploadFail: '上传失败',
notImage: '不是图片',
personIsAllDone: '抽奖抽完了',
personNotEnough: '抽奖人数不足',
startDraw: '现在抽取{count}{leftover}人',
completeInformation: '请填写完整信息',
},
placeHolder: {
enterTitle: '输入标题',
name: '名称',
winnerCount: '中奖人数',
},
data: {
yes: '是',
no: '否',
number: '编号',
isWin: '是否中奖',
department: '部门',
name: '姓名',
identity: '身份',
prizeName: '获奖',
prizeTime: '获奖时间',
operation: '操作',
delete: '删除',
removePerson: '移入未中奖名单',
defaultTitle: '大明内阁六部御前奏对',
xlsxName: '人口登记表-zhCn.xlsx',
readmeName: 'readme-zhCn.md',
},
footer: {
'self-reflection': '行有不得,反求诸己',
'thiefEasy': '破山中贼易,破心中贼难',
},
}

View File

@@ -1,24 +1,25 @@
import { createApp } from 'vue';
import './style.css';
import svgIcon from '@/components/SvgIcon/index.vue'
import i18n from '@/locales/i18n'
import * as THREE from 'three'
import { createApp } from 'vue'
import VueDOMPurifyHTML from 'vue-dompurify-html'
import App from './App.vue'
import './style.css'
import './style/markdown.css'
import './style/style.scss'
import * as THREE from 'three';
import App from './App.vue';
import VueDOMPurifyHTML from 'vue-dompurify-html'
const app = createApp(App);
// 全局svg组件
import 'virtual:svg-icons-register';
import svgIcon from '@/components/SvgIcon/index.vue';
import 'virtual:svg-icons-register'
// svg全局组件// 路由
import router from '@/router';
import router from '@/router'
// pinia
import { createPinia } from 'pinia';
import { createPinia } from 'pinia'
// pinia持久化
import piniaPluginPersist from 'pinia-plugin-persist';
const pinia = createPinia();
pinia.use(piniaPluginPersist);
import piniaPluginPersist from 'pinia-plugin-persist'
app.config.globalProperties.$THREE = THREE; //挂载到原型
app.component('svg-icon', svgIcon);
app.use(router).use(VueDOMPurifyHTML).use(pinia).mount('#app');
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersist)
app.config.globalProperties.$THREE = THREE // 挂载到原型
app.component('svg-icon', svgIcon)
app.use(router).use(VueDOMPurifyHTML).use(pinia).use(i18n).mount('#app')

View File

@@ -1,7 +1,9 @@
import { createRouter, createWebHistory,createWebHashHistory } from 'vue-router';
import Layout from '@/layout/index.vue';
import Home from '@/views/Home/index.vue';
export const configRoutes={
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'),
@@ -15,31 +17,31 @@ export const configRoutes={
name: 'PersonConfig',
component: () => import('@/views/Config/Person/PersonConfig.vue'),
meta: {
title: '人员配置',
title: i18n.global.t('sidebar.personConfiguration'),
icon: 'person',
},
children:[
children: [
{
path:'',
path: '',
redirect: '/log-lottery/config/person/all',
},
{
path:'/log-lottery/config/person/all',
name:'AllPersonConfig',
component:()=>import('@/views/Config/Person/PersonAll.vue'),
meta:{
title:'人员名单',
icon:'all'
}
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/already',
name:'AlreadyPerson',
component:()=>import('@/views/Config/Person/PersonAlready.vue'),
meta:{
title:'中奖名单人员',
icon:'already'
}
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',
@@ -50,66 +52,66 @@ export const configRoutes={
// icon:'other'
// }
// }
]
],
},
{
path: '/log-lottery/config/prize',
name: 'PrizeConfig',
component: () => import('@/views/Config/Prize/PrizeConfig.vue'),
meta:{
title: '奖品配置',
icon: 'prize'
}
meta: {
title: i18n.global.t('sidebar.prizeConfiguration'),
icon: 'prize',
},
},
{
path:'/log-lottery/config/global',
name:'GlobalConfig',
path: '/log-lottery/config/global',
name: 'GlobalConfig',
redirect: '/log-lottery/config/global/all',
meta:{
title:'全局配置',
icon:'global'
meta: {
title: i18n.global.t('sidebar.globalSetting'),
icon: 'global',
},
children:[
children: [
{
path:'/log-lottery/config/global/face',
name:'FaceConfig',
component:()=>import('@/views/Config/Global/FaceConfig.vue'),
meta:{
title:'界面配置',
icon:'face'
}
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:'图片列表',
icon:'image'
}
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:'音乐列表',
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: '操作说明',
icon: 'readme'
}
meta: {
title: i18n.global.t('sidebar.operatingInstructions'),
icon: 'readme',
},
]
}
},
],
}
const routes = [
{
path: '/log-lottery',
@@ -122,9 +124,9 @@ const routes = [
component: Home,
},
{
path:'/log-lottery/demo',
name:'Demo',
component:()=>import('@/views/Demo/index.vue')
path: '/log-lottery/demo',
name: 'Demo',
component: () => import('@/views/Demo/index.vue'),
},
configRoutes,
],
@@ -135,6 +137,6 @@ const router = createRouter({
// 读取环境变量
history: envMode==='file'?createWebHashHistory():createWebHistory(),
routes,
});
})
export default router;
export default router

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia';
import { defaultMusicList, defaultImageList, defaultPatternList } from './data'
import { IMusic, IImage } from '@/types/storeType';
import type { IImage, IMusic } from '@/types/storeType'
import i18n, { browserLanguage } from '@/locales/i18n'
import { defineStore } from 'pinia'
import { defaultImageList, defaultMusicList, defaultPatternList } from './data'
// import { IPrizeConfig } from '@/types/storeType';
export const useGlobalConfig = defineStore('global', {
state() {
@@ -8,7 +9,8 @@ export const useGlobalConfig = defineStore('global', {
globalConfig: {
rowCount: 17,
isSHowPrizeList: true,
topTitle: '大明内阁六部御前奏对',
topTitle: i18n.global.t('data.defaultTitle'),
language: browserLanguage,
theme: {
name: 'dracula',
detail: { primary: '#0f5fd3' },
@@ -20,7 +22,7 @@ export const useGlobalConfig = defineStore('global', {
textSize: 30,
patternColor: '#1b66c9',
patternList: defaultPatternList as number[],
background:{}, // 背景颜色或图片
background: {}, // 背景颜色或图片
},
musicList: defaultMusicList as IMusic[],
imageList: defaultImageList as IImage[],
@@ -29,141 +31,145 @@ export const useGlobalConfig = defineStore('global', {
item: defaultMusicList[0],
paused: true,
},
};
}
},
getters: {
// 获取全部配置
getGlobalConfig(state) {
return state.globalConfig;
return state.globalConfig
},
// 获取标题
getTopTitle(state) {
return state.globalConfig.topTitle;
return state.globalConfig.topTitle
},
// 获取行数
getRowCount(state) {
return state.globalConfig.rowCount;
return state.globalConfig.rowCount
},
// 获取主题
getTheme(state) {
return state.globalConfig.theme;
return state.globalConfig.theme
},
// 获取卡片颜色
getCardColor(state) {
return state.globalConfig.theme.cardColor;
return state.globalConfig.theme.cardColor
},
// 获取中奖颜色
getLuckyColor(state) {
return state.globalConfig.theme.luckyCardColor;
return state.globalConfig.theme.luckyCardColor
},
// 获取文字颜色
getTextColor(state) {
return state.globalConfig.theme.textColor;
return state.globalConfig.theme.textColor
},
// 获取卡片宽高
getCardSize(state) {
return {
width: state.globalConfig.theme.cardWidth,
height: state.globalConfig.theme.cardHeight
height: state.globalConfig.theme.cardHeight,
}
},
// 获取文字大小
getTextSize(state) {
return state.globalConfig.theme.textSize;
return state.globalConfig.theme.textSize
},
// 获取图案颜色
getPatterColor(state) {
return state.globalConfig.theme.patternColor;
return state.globalConfig.theme.patternColor
},
// 获取图案列表
getPatternList(state) {
return state.globalConfig.theme.patternList;
return state.globalConfig.theme.patternList
},
// 获取音乐列表
getMusicList(state) {
return state.globalConfig.musicList;
return state.globalConfig.musicList
},
// 获取当前音乐
getCurrentMusic(state) {
return state.currentMusic;
return state.currentMusic
},
// 获取图片列表
getImageList(state) {
return state.globalConfig.imageList;
return state.globalConfig.imageList
},
// 获取是否显示奖品列表
getIsShowPrizeList(state) {
return state.globalConfig.isSHowPrizeList;
return state.globalConfig.isSHowPrizeList
},
// 获取当前语言
getLanguage(state) {
return state.globalConfig.language
},
// 获取背景图片设置
getBackground(state){
getBackground(state) {
return state.globalConfig.theme.background
},
},
actions: {
// 设置rowCount
setRowCount(rowCount: number) {
this.globalConfig.rowCount = rowCount;
this.globalConfig.rowCount = rowCount
},
// 设置标题
setTopTitle(topTitle: string) {
this.globalConfig.topTitle = topTitle;
this.globalConfig.topTitle = topTitle
},
// 设置主题
setTheme(theme: any) {
const { name, detail } = theme;
this.globalConfig.theme.name = name;
this.globalConfig.theme.detail = detail;
const { name, detail } = theme
this.globalConfig.theme.name = name
this.globalConfig.theme.detail = detail
},
// 设置卡片颜色
setCardColor(cardColor: string) {
this.globalConfig.theme.cardColor = cardColor;
this.globalConfig.theme.cardColor = cardColor
},
// 设置中奖颜色
setLuckyCardColor(luckyCardColor: string) {
this.globalConfig.theme.luckyCardColor = luckyCardColor;
this.globalConfig.theme.luckyCardColor = luckyCardColor
},
// 设置文字颜色
setTextColor(textColor: string) {
this.globalConfig.theme.textColor = textColor;
this.globalConfig.theme.textColor = textColor
},
// 设置卡片宽高
setCardSize(cardSize: { width: number, height: number }) {
this.globalConfig.theme.cardWidth = cardSize.width;
this.globalConfig.theme.cardHeight = cardSize.height;
this.globalConfig.theme.cardWidth = cardSize.width
this.globalConfig.theme.cardHeight = cardSize.height
},
// 设置文字大小
setTextSize(textSize: number) {
this.globalConfig.theme.textSize = textSize;
this.globalConfig.theme.textSize = textSize
},
// 设置图案颜色
setPatterColor(patterColor: string) {
this.globalConfig.theme.patternColor = patterColor;
this.globalConfig.theme.patternColor = patterColor
},
// 设置图案列表
setPatternList(patternList: number[]) {
this.globalConfig.theme.patternList = patternList;
this.globalConfig.theme.patternList = patternList
},
// 重置图案列表
resetPatternList() {
this.globalConfig.theme.patternList = defaultPatternList;
this.globalConfig.theme.patternList = defaultPatternList
},
// 添加音乐
addMusic(music: IMusic) {
// 验证音乐是否已存在看name字段
for (let i = 0; i < this.globalConfig.musicList.length; i++) {
if (this.globalConfig.musicList[i].name === music.name) {
return;
return
}
}
this.globalConfig.musicList.push(music);
this.globalConfig.musicList.push(music)
},
// 删除音乐
removeMusic(musicId: string) {
for (let i = 0; i < this.globalConfig.musicList.length; i++) {
if (this.globalConfig.musicList[i].id === musicId) {
this.globalConfig.musicList.splice(i, 1);
break;
this.globalConfig.musicList.splice(i, 1)
break
}
}
},
@@ -171,38 +177,38 @@ export const useGlobalConfig = defineStore('global', {
setCurrentMusic(musicItem: IMusic, paused: boolean = true) {
this.currentMusic = {
item: musicItem,
paused: paused,
paused,
}
},
// 重置音乐列表
resetMusicList() {
this.globalConfig.musicList = defaultMusicList as IMusic[];
this.globalConfig.musicList = defaultMusicList as IMusic[]
},
// 清空音乐列表
clearMusicList() {
this.globalConfig.musicList = [] as IMusic[];
this.globalConfig.musicList = [] as IMusic[]
},
// 添加图片
addImage(image: IImage) {
for (let i = 0; i < this.globalConfig.imageList.length; i++) {
if (this.globalConfig.imageList[i].name === image.name) {
return;
return
}
}
this.globalConfig.imageList.push(image);
this.globalConfig.imageList.push(image)
},
// 删除图片
removeImage(imageId: string) {
for (let i = 0; i < this.globalConfig.imageList.length; i++) {
if (this.globalConfig.imageList[i].id === imageId) {
this.globalConfig.imageList.splice(i, 1);
break;
this.globalConfig.imageList.splice(i, 1)
break
}
}
},
// 重置图片列表
resetImageList() {
this.globalConfig.imageList = defaultImageList as IImage[];
this.globalConfig.imageList = defaultImageList as IImage[]
},
// 清空图片列表
clearImageList() {
@@ -210,10 +216,15 @@ export const useGlobalConfig = defineStore('global', {
},
// 设置是否显示奖品列表
setIsShowPrizeList(isShowPrizeList: boolean) {
this.globalConfig.isSHowPrizeList = isShowPrizeList;
this.globalConfig.isSHowPrizeList = isShowPrizeList
},
// 设置
setLanguage(language: string) {
this.globalConfig.language = language
i18n.global.locale.value = language
},
// 设置背景图片
setBackground(background:{}){
setBackground(background: any) {
this.globalConfig.theme.background = background
},
// 重置所有配置
@@ -221,7 +232,8 @@ export const useGlobalConfig = defineStore('global', {
this.globalConfig = {
rowCount: 17,
isSHowPrizeList: true,
topTitle: '大明内阁六部御前奏对',
topTitle: i18n.global.t('data.defaultTitle'),
language: browserLanguage,
theme: {
name: 'dracula',
detail: { primary: '#0f5fd3' },
@@ -233,16 +245,16 @@ export const useGlobalConfig = defineStore('global', {
textSize: 30,
patternColor: '#1b66c9',
patternList: defaultPatternList as number[],
background:{}, // 背景图片
background: {}, // 背景颜色或图片
},
musicList: defaultMusicList as IMusic[],
imageList: defaultImageList as IImage[],
},
}
this.currentMusic = {
item: defaultMusicList[0],
paused: true,
}
}
},
},
persist: {
enabled: true,

View File

@@ -1,12 +1,13 @@
import {usePersonConfig} from './personConfig';
import { usePrizeConfig } from './prizeConfig';
import {useGlobalConfig} from './globalConfig';
import {useSystem} from './system';
import { useGlobalConfig } from './globalConfig'
import { usePersonConfig } from './personConfig'
import { usePrizeConfig } from './prizeConfig'
import { useSystem } from './system'
export default function useStore() {
return {
personConfig:usePersonConfig(),
prizeConfig:usePrizeConfig(),
globalConfig:useGlobalConfig(),
system:useSystem(),
};
personConfig: usePersonConfig(),
prizeConfig: usePrizeConfig(),
globalConfig: useGlobalConfig(),
system: useSystem(),
}
}

View File

@@ -1,43 +1,44 @@
import { defineStore } from 'pinia';
import { IPersonConfig } from '@/types/storeType';
import { IPrizeConfig } from '@/types/storeType';
import { defaultPersonList } from './data'
import { usePrizeConfig } from './prizeConfig';
import type { IPersonConfig, IPrizeConfig } from '@/types/storeType'
import dayjs from 'dayjs'
import { defineStore } from 'pinia'
import { defaultPersonList } from './data'
import { usePrizeConfig } from './prizeConfig'
export const usePersonConfig = defineStore('person', {
state() {
return {
personConfig: {
allPersonList: [] as IPersonConfig[],
alreadyPersonList: [] as IPersonConfig[],
},
}
};
},
getters: {
// 获取全部配置
getPersonConfig(state) {
return state.personConfig;
return state.personConfig
},
// 获取全部人员名单
getAllPersonList(state) {
return state.personConfig.allPersonList.filter((item: IPersonConfig) => {
return item
});
})
},
// 获取未获此奖的人员名单
getNotThisPrizePersonList(state: any) {
const currentPrize = usePrizeConfig().prizeConfig.currentPrize;
const currentPrize = usePrizeConfig().prizeConfig.currentPrize
const data = state.personConfig.allPersonList.filter((item: IPersonConfig) => {
return !item.prizeId.includes(currentPrize.id as string);
});
return !item.prizeId.includes(currentPrize.id as string)
})
return data
},
// 获取已中奖人员名单
getAlreadyPersonList(state) {
return state.personConfig.allPersonList.filter((item: IPersonConfig) => {
return item.isWin === true;
});
return item.isWin === true
})
},
// 获取中奖人员详情
getAlreadyPersonDetail(state) {
@@ -46,8 +47,8 @@ export const usePersonConfig = defineStore('person', {
// 获取未中奖人员名单
getNotPersonList(state) {
return state.personConfig.allPersonList.filter((item: IPersonConfig) => {
return item.isWin === false;
});
return item.isWin === false
})
},
},
actions: {
@@ -57,8 +58,8 @@ export const usePersonConfig = defineStore('person', {
return
}
personList.forEach((item: IPersonConfig) => {
this.personConfig.allPersonList.push(item);
});
this.personConfig.allPersonList.push(item)
})
},
// 添加已中奖人员
addAlreadyPersonList(personList: IPersonConfig[], prize: IPrizeConfig | null) {
@@ -78,13 +79,13 @@ export const usePersonConfig = defineStore('person', {
}
return item
});
this.personConfig.alreadyPersonList.push(person);
});
})
this.personConfig.alreadyPersonList.push(person)
})
},
// 从已中奖移动到未中奖
moveAlreadyToNot(person: IPersonConfig) {
if (person.id == undefined || person.id == null) {
if (person.id === undefined || person.id == null) {
return
}
const alreadyPersonListLength = this.personConfig.alreadyPersonList.length
@@ -100,42 +101,42 @@ export const usePersonConfig = defineStore('person', {
}
for (let i = 0; i < alreadyPersonListLength; i++) {
this.personConfig.alreadyPersonList = this.personConfig.alreadyPersonList.filter((item: IPersonConfig) =>
item.id !== person.id
item.id !== person.id,
)
}
},
// 删除指定人员
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);
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)
}
},
// 删除所有人员
deleteAllPerson() {
this.personConfig.allPersonList = [];
this.personConfig.alreadyPersonList = [];
this.personConfig.allPersonList = []
this.personConfig.alreadyPersonList = []
},
// 删除所有人员
resetPerson() {
this.personConfig.allPersonList = [];
this.personConfig.alreadyPersonList = [];
this.personConfig.allPersonList = []
this.personConfig.alreadyPersonList = []
},
// 重置已中奖人员
resetAlreadyPerson() {
// 把已中奖人员合并到未中奖人员,要验证是否已存在
this.personConfig.allPersonList.forEach((item: IPersonConfig) => {
item.isWin = false;
item.prizeName = [];
item.prizeTime = [];
item.isWin = false
item.prizeName = []
item.prizeTime = []
item.prizeId = []
});
this.personConfig.alreadyPersonList = [];
})
this.personConfig.alreadyPersonList = []
},
setDefaultPersonList() {
this.personConfig.allPersonList = defaultPersonList;
this.personConfig.alreadyPersonList = [];
this.personConfig.allPersonList = defaultPersonList
this.personConfig.alreadyPersonList = []
},
// 重置所有配置
reset() {
@@ -155,4 +156,4 @@ export const usePersonConfig = defineStore('person', {
},
],
},
});
})

View File

@@ -1,6 +1,7 @@
import { defineStore } from 'pinia';
import { IPrizeConfig } from '@/types/storeType';
import { defaultPrizeList, defaultCurrentPrize } from './data';
import type { IPrizeConfig } from '@/types/storeType'
import { defineStore } from 'pinia'
import { defaultCurrentPrize, defaultPrizeList } from './data'
export const usePrizeConfig = defineStore('prize', {
state() {
return {
@@ -17,66 +18,66 @@ export const usePrizeConfig = defineStore('prize', {
picture: {
id: '-1',
name: '',
url: ''
url: '',
},
separateCount: {
enable: true,
countList: []
countList: [],
},
desc: '',
isShow: false,
isUsed: false,
frequency: 1,
} as IPrizeConfig
} as IPrizeConfig,
},
}
};
},
getters: {
// 获取全部配置
getPrizeConfigAll(state) {
return state.prizeConfig;
return state.prizeConfig
},
// 获取奖品列表
getPrizeConfig(state) {
return state.prizeConfig.prizeList;
return state.prizeConfig.prizeList
},
// 根据id获取配置
getPrizeConfigById(state) {
return (id: number | string) => {
return state.prizeConfig.prizeList.find(item => item.id === id);
return state.prizeConfig.prizeList.find(item => item.id === id)
}
},
// 获取当前奖项
getCurrentPrize(state) {
return state.prizeConfig.currentPrize;
return state.prizeConfig.currentPrize
},
// 获取临时的奖项
getTemporaryPrize(state) {
return state.prizeConfig.temporaryPrize;
return state.prizeConfig.temporaryPrize
},
},
actions: {
// 设置奖项
setPrizeConfig(prizeList: IPrizeConfig[]) {
this.prizeConfig.prizeList = prizeList;
this.prizeConfig.prizeList = prizeList
},
// 添加奖项
addPrizeConfig(prizeConfigItem: IPrizeConfig) {
this.prizeConfig.prizeList.push(prizeConfigItem);
this.prizeConfig.prizeList.push(prizeConfigItem)
},
// 删除奖项
deletePrizeConfig(prizeConfigItemId: number | string) {
this.prizeConfig.prizeList = this.prizeConfig.prizeList.filter(item => item.id !== prizeConfigItemId);
this.prizeConfig.prizeList = this.prizeConfig.prizeList.filter(item => item.id !== prizeConfigItemId)
},
// 更新奖项数据
updatePrizeConfig(prizeConfigItem: IPrizeConfig) {
const prizeListLength = this.prizeConfig.prizeList.length;
const prizeListLength = this.prizeConfig.prizeList.length
if (prizeConfigItem.isUsed && prizeListLength) {
for (let i = 0; i < prizeListLength; i++) {
if (!this.prizeConfig.prizeList[i].isUsed) {
this.setCurrentPrize(this.prizeConfig.prizeList[i]);
break;
this.setCurrentPrize(this.prizeConfig.prizeList[i])
break
}
}
}
@@ -87,7 +88,7 @@ export const usePrizeConfig = defineStore('prize', {
},
// 删除全部奖项
deleteAllPrizeConfig() {
this.prizeConfig.prizeList = [] as IPrizeConfig[];
this.prizeConfig.prizeList = [] as IPrizeConfig[]
},
// 设置当前奖项
setCurrentPrize(prizeConfigItem: IPrizeConfig) {
@@ -95,10 +96,10 @@ export const usePrizeConfig = defineStore('prize', {
},
// 设置临时奖项
setTemporaryPrize(prizeItem: IPrizeConfig) {
if (prizeItem.isShow == false) {
if (prizeItem.isShow === false) {
for (let i = 0; i < this.prizeConfig.prizeList.length; i++) {
if (this.prizeConfig.prizeList[i].isUsed == false) {
this.setCurrentPrize(this.prizeConfig.prizeList[i]);
if (this.prizeConfig.prizeList[i].isUsed === false) {
this.setCurrentPrize(this.prizeConfig.prizeList[i])
break
}
@@ -122,17 +123,17 @@ export const usePrizeConfig = defineStore('prize', {
picture: {
id: '-1',
name: '',
url: ''
url: '',
},
separateCount: {
enable: true,
countList: []
countList: [],
},
desc: '',
isShow: false,
isUsed: false,
frequency: 1,
} as IPrizeConfig;
} as IPrizeConfig
},
// 重置所有配置
resetDefault() {
@@ -149,20 +150,20 @@ export const usePrizeConfig = defineStore('prize', {
picture: {
id: '-1',
name: '',
url: ''
url: '',
},
separateCount: {
enable: true,
countList: []
countList: [],
},
desc: '',
isShow: false,
isUsed: false,
frequency: 1,
} as IPrizeConfig
}
} as IPrizeConfig,
}
},
},
persist: {
enabled: true,
strategies: [
@@ -173,4 +174,4 @@ export const usePrizeConfig = defineStore('prize', {
},
],
},
});
})

View File

@@ -1,26 +1,26 @@
import { defineStore } from 'pinia';
import { defineStore } from 'pinia'
// import { IPrizeConfig } from '@/types/storeType';
export const useSystem = defineStore('system', {
state() {
return {
isMobile:false,
isChrome:true
};
isMobile: false,
isChrome: true,
}
},
getters: {
getIsMobile(state) {
return state.isMobile;
return state.isMobile
},
getIsChrome(state) {
return state.isChrome;
return state.isChrome
},
},
actions: {
setIsMobile(isMobile: boolean) {
this.isMobile = isMobile;
this.isMobile = isMobile
},
setIsChrome(isChrome: boolean) {
this.isChrome = isChrome;
this.isChrome = isChrome
},
},
persist: {

View File

@@ -1,52 +1,52 @@
export interface IPersonConfig {
id: number;
uid: string;
name: string;
department: string;
identity: string;
isWin: boolean;
x: number;
id: number
uid: string
name: string
department: string
identity: 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 type Separate = {
id: string,
count: number,
isUsedCount: number,
export interface Separate {
id: string
count: number
isUsedCount: number
}
export interface IPrizeConfig {
id: number | string;
name: string;
sort: number;
isAll: boolean;
count: number;
isUsedCount: number,
id: number | string
name: string
sort: number
isAll: boolean
count: number
isUsedCount: number
picture: {
id: string | number,
name: string,
id: string | number
name: string
url: string
};
}
separateCount: {
enable: boolean,
countList: Separate[],
};
desc: string;
isShow: boolean;
isUsed: boolean,
frequency: number;
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
}

View File

@@ -1,3 +1,3 @@
export function getToken() {
return window.localStorage.getItem('userToken');
return window.localStorage.getItem('userToken')
}

View File

@@ -1,43 +1,43 @@
// 判断颜色是否rgb或者rgba
export function isRgbOrRgba(color: string) {
return color.indexOf('rgb') > -1 || color.indexOf('rgba') > -1;
return color.includes('rgb') || color.includes('rgba')
}
// 判断是否hex形式
export function isHex(color: string) {
return color.indexOf('#') > -1;
return color.includes('#')
}
// 把hex颜色转成rgb数值类型
export function hexToRgba(hex: string) {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
const r = Number.parseInt(hex.slice(1, 3), 16)
const g = Number.parseInt(hex.slice(3, 5), 16)
const b = Number.parseInt(hex.slice(5, 7), 16)
return {r,g,b}
return { r, g, b }
}
// 把rgb数组转化成r g b 数值
export function rgbToRgba(rgb: string) {
const rgbArr = rgb.split('(')[1].split(')')[0].split(',');
const rgbArr = rgb.split('(')[1].split(')')[0].split(',')
return {r:rgbArr[0],g:rgbArr[1],b:rgbArr[2]}
return { r: rgbArr[0], g: rgbArr[1], b: rgbArr[2] }
}
// 组成rgb颜色添加透明度
export function rgba(color: string, opacity: number) {
opacity = opacity || 1;
let rgbaStr=''
opacity = opacity || 1
let rgbaStr = ''
// 判断是否是hex颜色
if (isHex(color)) {
const {r,g,b} = hexToRgba(color);
const { r, g, b } = hexToRgba(color)
rgbaStr = `rgba(${r},${g},${b},${opacity})`
}
else{
const {r,g,b} = rgbToRgba(color)
else {
const { r, g, b } = rgbToRgba(color)
rgbaStr = `rgba(${r},${g},${b},${opacity})`
}
return rgbaStr
return rgbaStr
}
export function rgbToHex(color:string) {

View File

@@ -1,5 +1,5 @@
export const readFileBinary = (file: any): Promise<any> => {
return new Promise(resolve => {
export function readFileBinary(file: any): Promise<any> {
return new Promise((resolve) => {
const reader = new FileReader()
reader.readAsBinaryString(file)
reader.onload = (ev: any) => {
@@ -8,12 +8,12 @@ export const readFileBinary = (file: any): Promise<any> => {
})
}
export const readFileData = (file: any): Promise<{dataUrl:string,fileName:string}> => {
return new Promise(resolve => {
export function readFileData(file: any): Promise<{ dataUrl: string, fileName: string }> {
return new Promise((resolve) => {
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = (ev: any) => {
resolve({dataUrl:ev.target.result,fileName:file.name})
resolve({ dataUrl: ev.target.result, fileName: file.name })
}
})
}

View File

@@ -1,38 +1,38 @@
import dayjs from 'dayjs';
import dayjs from 'dayjs'
// 筛选人员数据
export const filterData = (tableData: any[], localRowCount: number, startIndex = 0) => {
export function filterData(tableData: any[], localRowCount: number) {
const dataLength = tableData.length
let j = 0;
let j = 0
for (let i = 0; i < dataLength; i++) {
if (i % localRowCount === 0) {
j++;
j++
}
tableData[i].x = i % localRowCount + 1;
tableData[i].y = j;
tableData[i].id = i;
tableData[i].x = i % localRowCount + 1
tableData[i].y = j
tableData[i].id = i
// 是否中奖
}
return tableData
}
export const addOtherInfo = (personList: any[]) => {
const len = personList.length;
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].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
}
export const selectCard = (cardIndexArr: number[], tableLength: number, personId: number): number => {
const cardIndex = Math.round(Math.random() * (tableLength - 1));
export function selectCard(cardIndexArr: number[], tableLength: number, personId: number): number {
const cardIndex = Math.round(Math.random() * (tableLength - 1))
if (cardIndexArr.includes(cardIndex)) {
return selectCard(cardIndexArr, tableLength, personId)
}

View File

@@ -1,11 +1,10 @@
// 提取有哪些字段
export const extractFields = (data: any) => {
const item=data[0];
export function extractFields(data: any) {
const item = data[0]
// 排除id x y其他都加入数组
const keys = Object.keys(item).filter(key => key!== 'id' && key!== 'x' && key!== 'y');
if(keys.length>0){
const keys = Object.keys(item).filter(key => key !== 'id' && key !== 'x' && key !== 'y')
if (keys.length > 0) {
// 返回数组key value
return keys.map(key => ({label:key,value:true}));
return keys.map(key => ({ label: key, value: true }))
}
};
}

View File

@@ -1,31 +1,23 @@
<script setup lang='ts'>
import { ref, watch, onMounted } from 'vue'
import useStore from '@/store'
import { storeToRefs } from 'pinia'
import { themeChange } from 'theme-change';
import zod from 'zod';
import daisyuiThemes from 'daisyui/src/theming/themes'
import { ColorPicker } from 'vue3-colorpicker';
import 'vue3-colorpicker/style.css';
import { isRgbOrRgba, isHex } from '@/utils/color'
import PatternSetting from './components/PatternSetting.vue'
import i18n, { languageList } from '@/locales/i18n'
import useStore from '@/store'
import { isHex, isRgbOrRgba } from '@/utils/color'
import daisyuiThemes from 'daisyui/src/theming/themes'
import { storeToRefs } from 'pinia'
import { themeChange } from 'theme-change'
import { onMounted, ref, watch } from 'vue'
import { ColorPicker } from 'vue3-colorpicker'
import { useI18n } from 'vue-i18n'
import zod from 'zod'
import PatternSetting from './components/PatternSetting.vue'
import 'vue3-colorpicker/style.css'
const { t } = useI18n()
const globalConfig = useStore().globalConfig
const personConfig = useStore().personConfig
const prizeConfig = useStore().prizeConfig
const { getTopTitle: topTitle,
getTheme: localTheme,
getPatterColor: patternColor,
getPatternList: patternList,
getCardColor: cardColor,
getLuckyColor: luckyCardColor,
getTextColor: textColor,
getCardSize: cardSize,
getTextSize: textSize,
getRowCount: rowCount,
getIsShowPrizeList: isShowPrizeList,
getBackground: backgroundImage,
getImageList: imageList
const { getTopTitle: topTitle, getTheme: localTheme, getPatterColor: patternColor, getPatternList: patternList, getCardColor: cardColor, getLuckyColor: luckyCardColor, getTextColor: textColor, getCardSize: cardSize, getTextSize: textSize, getRowCount: rowCount, getIsShowPrizeList: isShowPrizeList, getLanguage: userLanguage, getBackground: backgroundImage, getImageList: imageList,
} = storeToRefs(globalConfig)
const { getAlreadyPersonList: alreadyPersonList, getNotPersonList: notPersonList } = storeToRefs(personConfig)
const colorPickerRef = ref()
@@ -33,7 +25,7 @@ const resetDataDialogRef = ref()
interface ThemeDaType {
[key: string]: any
}
const isRowCountChange = ref(0) //0未改变1改变,2加载中
const isRowCountChange = ref(0) // 0未改变1改变,2加载中
const themeValue = ref(localTheme.value.name)
const topTitleValue = ref(structuredClone(topTitle.value))
const cardColorValue = ref(structuredClone(cardColor.value))
@@ -42,6 +34,7 @@ const textColorValue = ref(structuredClone(textColor.value))
const cardSizeValue = ref(structuredClone(cardSize.value))
const textSizeValue = ref(structuredClone(textSize.value))
const rowCountValue = ref(structuredClone(rowCount.value))
const languageValue = ref(structuredClone(userLanguage.value))
const isShowPrizeListValue = ref(structuredClone(isShowPrizeList.value))
const patternColorValue = ref(structuredClone(patternColor.value))
const themeList = ref(Object.keys(daisyuiThemes))
@@ -55,24 +48,23 @@ const formErr = ref({
})
const schema = zod.object({
rowCount: zod.number({
required_error: '必填项',
invalid_type_error: '必须填入数字',
required_error: i18n.global.t('error.require'),
invalid_type_error: i18n.global.t('error.requireNumber'),
})
.min(1, '最小为1')
.max(100, '最大为100')
.min(1, i18n.global.t('error.minNumber1'))
.max(100, i18n.global.t('error.maxNumber100')),
// 格式化
})
type ValidatePayload = zod.infer<typeof schema>
const payload: ValidatePayload = {
rowCount: formData.value.rowCount,
}
const parseSchema = (props: ValidatePayload) => {
function parseSchema(props: ValidatePayload) {
return schema.parseAsync(props)
}
const resetPersonLayout = () => {
function resetPersonLayout() {
isRowCountChange.value = 2
setTimeout(() => {
const alreadyLen = alreadyPersonList.value.length
@@ -91,17 +83,17 @@ const resetPersonLayout = () => {
}, 1000)
}
const clearPattern = () => {
function clearPattern() {
globalConfig.setPatternList([] as number[])
}
const resetPattern = () => {
function resetPattern() {
globalConfig.resetPatternList()
}
const resetData = () => {
globalConfig.reset();
personConfig.reset();
prizeConfig.resetDefault();
function resetData() {
globalConfig.reset()
personConfig.reset()
prizeConfig.resetDefault()
// 刷新页面
window.location.reload()
}
@@ -116,28 +108,27 @@ const resetData = () => {
watch(() => formData.value.rowCount, () => {
payload.rowCount = formData.value.rowCount
parseSchema(payload).then(res => {
parseSchema(payload).then((res) => {
if (res.rowCount) {
isRowCountChange.value = 1
globalConfig.setRowCount(res.rowCount)
}
})
.catch(err => {
}).catch((err) => {
formErr.value.rowCount = err.issues[0].message
})
})
watch(topTitleValue, (val) => {
globalConfig.setTopTitle(val)
}),
watch(themeValue, (val: any) => {
})
watch(themeValue, (val: any) => {
const selectedThemeDetail = daisyuiThemeList.value[val]
globalConfig.setTheme({ name: val, detail: selectedThemeDetail })
themeChange(val)
if (selectedThemeDetail.primary && (isHex(selectedThemeDetail.primary) || isRgbOrRgba(selectedThemeDetail.primary))) {
globalConfig.setCardColor(selectedThemeDetail.primary)
}
}, { deep: true })
}, { deep: true })
watch(cardColorValue, (val: string) => {
globalConfig.setCardColor(val)
@@ -151,15 +142,20 @@ watch(patternColorValue, (val: string) => {
watch(textColorValue, (val: string) => {
globalConfig.setTextColor(val)
}, { deep: true })
watch(cardSizeValue, (val: { width: number; height: number; }) => {
watch(cardSizeValue, (val: { width: number, height: number }) => {
globalConfig.setCardSize(val)
}, { deep: true }),
watch(isShowPrizeListValue, () => {
}, { deep: true })
watch(isShowPrizeListValue, () => {
globalConfig.setIsShowPrizeList(isShowPrizeListValue.value)
})
watch(backgroundImageValue, (val: {}) => {
})
watch(backgroundImageValue, (val) => {
globalConfig.setBackground(val)
})
watch(languageValue, (val: string) => {
globalConfig.setLanguage(val)
})
onMounted(() => {
})
</script>
@@ -167,157 +163,187 @@ onMounted(() => {
<template>
<dialog id="my_modal_1" ref="resetDataDialogRef" class="border-none modal">
<div class="modal-box">
<h3 class="text-lg font-bold">提示!</h3>
<p class="py-4">该操作会重置所有数据是否继续</p>
<h3 class="text-lg font-bold">
{{ t('dialog.titleTip') }}
</h3>
<p class="py-4">
{{ t('dialog.dialogResetAllData') }}
</p>
<div class="modal-action">
<form method="dialog" class="flex gap-3">
<!-- if there is a button in form, it will close the modal -->
<button class="btn" @click="resetDataDialogRef.close()">取消</button>
<button class="btn" @click="resetData">确定</button>
<button class="btn" @click="resetDataDialogRef.close()">
{{ t(`button.cancel`) }}
</button>
<button class="btn" @click="resetData">
{{ t('button.confirm') }}
</button>
</form>
</div>
</div>
</dialog>
<div>
<h2>全局配置</h2>
<h2>{{ t('viewTitle.globalSetting') }}</h2>
<div class="mb-8">
<button class="btn btn-sm btn-primary" @click="resetDataDialogRef.showModal()">重置所有数据</button>
<button class="btn btn-sm btn-primary" @click="resetDataDialogRef.showModal()">
{{ t('button.resetAllData') }}
</button>
</div>
<label class="flex flex-row items-center w-full gap-24 mb-10 form-control">
<div class="">
<div class="label">
<span class="label-text">标题</span>
<span class="label-text">{{ t('table.title') }}</span>
</div>
<input type="text" v-model="topTitleValue" placeholder="输入标题"
class="w-full max-w-xs input input-bordered" />
<input
v-model="topTitleValue" type="text" :placeholder="t('placeHolder.enterTitle')"
class="w-full max-w-xs input input-bordered"
>
</div>
</label>
<label class="flex flex-row items-center w-full gap-24 mb-10 form-control">
<div class="">
<div class="label">
<span class="label-text">列数</span>
<span class="label-text">{{ t('table.columnNumber') }}</span>
</div>
<input type="number" v-model="formData.rowCount" placeholder="Type here"
class="w-full max-w-xs input input-bordered" />
<input
v-model="formData.rowCount" type="number" placeholder="Type here"
class="w-full max-w-xs input input-bordered"
>
<div class="help">
<span class="text-sm text-red-400 help-text" v-if="formErr.rowCount">
<span v-if="formErr.rowCount" class="text-sm text-red-400 help-text">
{{ formErr.rowCount }}
</span>
</div>
</div>
<div>
<div class="tooltip" data-tip="该项比较耗费时间和性能">
<button class="mt-5 btn btn-info btn-sm" :disabled="isRowCountChange != 1"
@click="resetPersonLayout">
<span>重设布局</span>
<span class="loading loading-ring loading-md" v-show="isRowCountChange == 2"></span>
<div class="tooltip" :data-tip="t('tooltip.resetLayout')">
<button class="mt-5 btn btn-info btn-sm" :disabled="isRowCountChange !== 1" @click="resetPersonLayout">
<span>{{ t('button.setLayout') }}</span>
<span v-show="isRowCountChange === 2" class="loading loading-ring loading-md" />
</button>
</div>
</div>
</label>
<label class="w-full max-w-xs form-control">
<div class="label">
<span class="label-text">选择主题</span>
<span class="label-text">{{ t('table.language') }}</span>
</div>
<select data-choose-theme class="w-full max-w-xs border-solid select border-1" v-model="themeValue">
<option disabled selected>选取主题</option>
<select v-model="languageValue" data-choose-theme class="w-full max-w-xs border-solid select border-1">
<option disabled selected>{{ t('table.language') }}</option>
<option v-for="item in languageList" :key="item.key" :value="item.key">{{ item.name }}</option>
</select>
</label>
<label class="w-full max-w-xs form-control">
<div class="label">
<span class="label-text">{{ t('table.theme') }}</span>
</div>
<select v-model="themeValue" data-choose-theme class="w-full max-w-xs border-solid select border-1">
<option disabled selected>{{ t('table.theme') }}</option>
<option v-for="(item, index) in themeList" :key="index" :value="item">{{ item }}</option>
</select>
</label>
<label class="w-full max-w-xs form-control">
<div class="label">
<span class="label-text">选择背景图片</span>
<span class="label-text">{{ t('table.backgroundImage') }}</span>
</div>
<select data-choose-theme class="w-full max-w-xs border-solid select border-1"
v-model="backgroundImageValue">
<option disabled selected>选取背景图片</option>
<option v-for="(item, index) in [{ name: '无', url: '', id: '' }, ...imageList]" :key="index"
:value="item">{{ item.name }}</option>
<select
v-model="backgroundImageValue" data-choose-theme
class="w-full max-w-xs border-solid select border-1"
>
<option disabled selected>{{ t('table.backgroundImage') }}</option>
<option
v-for="(item, index) in [{ name: '❌', url: '', id: '' }, ...imageList]" :key="index"
:value="item"
>{{ item.name }}</option>
</select>
</label>
<label class="w-full max-w-xs form-control">
<div class="label">
<span class="label-text">卡片颜色</span>
<span class="label-text">{{ t('table.cardColor') }}</span>
</div>
<ColorPicker ref="colorPickerRef" v-model="cardColorValue" v-model:pure-color="cardColorValue">
</ColorPicker>
<ColorPicker ref="colorPickerRef" v-model="cardColorValue" v-model:pure-color="cardColorValue" />
</label>
<label class="w-full max-w-xs form-control">
<div class="label">
<span class="label-text">中奖卡片颜色</span>
<span class="label-text">{{ t('table.winnerColor') }}</span>
</div>
<ColorPicker ref="colorPickerRef" v-model="luckyCardColorValue" v-model:pure-color="luckyCardColorValue">
</ColorPicker>
<ColorPicker ref="colorPickerRef" v-model="luckyCardColorValue" v-model:pure-color="luckyCardColorValue" />
</label>
<label class="w-full max-w-xs form-control">
<div class="label">
<span class="label-text">文字颜色</span>
<span class="label-text">{{ t('table.textColor') }}</span>
</div>
<ColorPicker ref="colorPickerRef" v-model="textColorValue" v-model:pure-color="textColorValue">
</ColorPicker>
<ColorPicker ref="colorPickerRef" v-model="textColorValue" v-model:pure-color="textColorValue" />
</label>
<label class="flex flex-row w-full max-w-xs gap-10 mb-10 form-control">
<div>
<div class="label">
<span class="label-text">卡片宽度</span>
<span class="label-text">{{ t('table.cardWidth') }}</span>
</div>
<input type="number" v-model="cardSizeValue.width" placeholder="Type here"
class="w-full max-w-xs input input-bordered" />
<input
v-model="cardSizeValue.width" type="number" placeholder="Type here"
class="w-full max-w-xs input input-bordered"
>
</div>
<div>
<div class="label">
<span class="label-text">卡片高度</span>
<span class="label-text">{{ t('table.cardHeight') }}</span>
</div>
<input type="number" v-model="cardSizeValue.height" placeholder="Type here"
class="w-full max-w-xs input input-bordered" />
<input
v-model="cardSizeValue.height" type="number" placeholder="Type here"
class="w-full max-w-xs input input-bordered"
>
</div>
</label>
<label class="w-full max-w-xs mb-10 form-control">
<div class="label">
<span class="label-text">文字大小</span>
<span class="label-text">{{ t('table.textSize') }}</span>
</div>
<input type="number" v-model="textSizeValue" placeholder="Type here"
class="w-full max-w-xs input input-bordered" />
<input
v-model="textSizeValue" type="number" placeholder="Type here"
class="w-full max-w-xs input input-bordered"
>
</label>
<label class="w-full max-w-xs form-control">
<div class="label">
<span class="label-text">高亮颜色</span>
<span class="label-text">{{ t('table.highlightColor') }}</span>
</div>
<ColorPicker ref="colorPickerRef" v-model="patternColorValue" v-model:pure-color="patternColorValue">
</ColorPicker>
<ColorPicker ref="colorPickerRef" v-model="patternColorValue" v-model:pure-color="patternColorValue" />
</label>
<label class="flex flex-row items-center w-full gap-24 mb-0 form-control">
<div>
<div class="label">
<span class="label-text">图案设置</span>
<span class="label-text">{{ t('table.patternSetting') }}</span>
</div>
<div class="h-auto">
<PatternSetting :rowCount="rowCount" :cardColor="cardColor" :patternColor="patternColor"
:patternList="patternList"></PatternSetting>
<PatternSetting
:row-count="rowCount" :card-color="cardColor" :pattern-color="patternColor"
:pattern-list="patternList"
/>
</div>
</div>
</label>
<div class="flex w-full h-24 gap-3 m-0">
<button class="mt-5 btn btn-info btn-sm" @click.stop="clearPattern">
<span>清空图案设置</span>
<span>{{ t('button.clearPattern') }}</span>
</button>
<div class="tooltip" data-tip="默认图案设置针对17列时有效其他列数请自行设置">
<div class="tooltip" :data-tip="t('tooltip.defaultLayout')">
<button class="mt-5 btn btn-info btn-sm" @click="resetPattern">
<span>默认图案设置</span>
<span>{{ t('button.DefaultPattern') }}</span>
</button>
</div>
</div>
<label class="w-full max-w-xs mb-10 form-control">
<div class="label">
<span class="label-text">是否常显奖品列表</span>
<span class="label-text">{{ t('table.alwaysDisplay') }}</span>
</div>
<input type="checkbox" :checked="isShowPrizeListValue"
<input
type="checkbox" :checked="isShowPrizeListValue" class="mt-2 border-solid checkbox checkbox-secondary border-1"
@change="isShowPrizeListValue = !isShowPrizeListValue"
class="mt-2 border-solid checkbox checkbox-secondary border-1" />
>
</label>
</div>
</template>

View File

@@ -1,29 +1,30 @@
<script setup lang='ts'>
import { ref, onMounted, watch } from 'vue'
import { IImage } from '@/types/storeType'
import type { IImage } from '@/types/storeType'
import ImageSync from '@/components/ImageSync/index.vue'
import useStore from '@/store'
import { readFileData } from '@/utils/file'
import localforage from 'localforage'
import useStore from '@/store'
import { storeToRefs } from 'pinia'
import ImageSync from '@/components/ImageSync/index.vue'
import { onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const globalConfig= useStore().globalConfig
const { getImageList:localImageList} = storeToRefs(globalConfig)
const { t } = useI18n()
const globalConfig = useStore().globalConfig
const { getImageList: localImageList } = storeToRefs(globalConfig)
const limitType = ref('image/*')
const imgUploadToast = ref(0) //0是不显示1是成功2是失败,3是不是图片
const imgUploadToast = ref(0) // 0是不显示1是成功2是失败,3是不是图片
const imageDbStore = localforage.createInstance({
name: 'imgStore'
name: 'imgStore',
})
const handleFileChange = async (e: Event) => {
const isImage= /image*/.test(((e.target as HTMLInputElement).files as FileList)[0].type)
async function handleFileChange(e: Event) {
const isImage = /image*/.test(((e.target as HTMLInputElement).files as FileList)[0].type)
if (!isImage) {
imgUploadToast.value = 3
return
}
let { dataUrl, fileName } = await readFileData(((e.target as HTMLInputElement).files as FileList)[0])
imageDbStore.setItem(new Date().getTime().toString() + '+' + fileName, dataUrl)
const { dataUrl, fileName } = await readFileData(((e.target as HTMLInputElement).files as FileList)[0])
imageDbStore.setItem(`${new Date().getTime().toString()}+${fileName}`, dataUrl)
.then(() => {
imgUploadToast.value = 1
getImageDbStore()
@@ -33,21 +34,21 @@ const handleFileChange = async (e: Event) => {
})
}
const getImageDbStore =async () => {
const keys =await imageDbStore.keys()
if(keys.length>0){
async function getImageDbStore() {
const keys = await imageDbStore.keys()
if (keys.length > 0) {
imageDbStore.iterate((value, key) => {
globalConfig.addImage({
id:key,
name:key,
url:'Storage'
id: key,
name: key,
url: 'Storage',
})
})
}
}
const removeImage=(item:IImage)=>{
if(item.url=='Storage'){
function removeImage(item: IImage) {
if (item.url === 'Storage') {
imageDbStore.removeItem(item.id).then(() => {
globalConfig.removeImage(item.id)
})
@@ -68,23 +69,25 @@ watch(() => imgUploadToast.value, (val) => {
<template>
<div class="toast toast-top toast-end">
<div class="alert alert-error" v-if="imgUploadToast == 2">
<span>上传失败</span>
<div v-if="imgUploadToast === 2" class="alert alert-error">
<span>{{ t('error.uploadFail') }}</span>
</div>
<div class="alert alert-success" v-if="imgUploadToast == 1">
<span>上传成功</span>
<div v-if="imgUploadToast === 1" class="alert alert-success">
<span>{{ t('error.uploadSuccess') }}</span>
</div>
<div class="alert alert-error" v-if="imgUploadToast == 3">
<span>不是图片</span>
<div v-if="imgUploadToast === 3" class="alert alert-error">
<span>{{ t('error.notImage') }}</span>
</div>
</div>
<div>
<div class="">
<label for="explore">
<input type="file" class="" id="explore" style="display: none" @change="handleFileChange"
:accept="limitType" />
<span class="btn btn-primary btn-sm">上传图片</span>
<input
id="explore" type="file" class="" style="display: none" :accept="limitType"
@change="handleFileChange"
>
<span class="btn btn-primary btn-sm">{{ t('button.upload') }}</span>
</label>
</div>
<ul class="p-0">
@@ -93,14 +96,18 @@ watch(() => imgUploadToast.value, (val) => {
<div class="avatar h-14">
<div class="w-12 h-12 mask mask-squircle hover:w-14 hover:h-14">
<!-- <img v-if="item.url!=='Storage'" :src="item.url" alt="Avatar Tailwind CSS Component" /> -->
<ImageSync :imgItem="item"></ImageSync>
<ImageSync :img-item="item" />
</div>
</div>
<div class="w-64">
<div class="overflow-hidden font-bold whitespace-nowrap text-ellipsis">{{ item.name}}</div>
<div class="overflow-hidden font-bold whitespace-nowrap text-ellipsis">
{{ item.name }}
</div>
</div>
<div>
<button class="btn btn-error btn-xs" @click="removeImage(item)">删除</button>
<button class="btn btn-error btn-xs" @click="removeImage(item)">
{{ t('button.upload') }}
</button>
</div>
</div>
</li>

View File

@@ -1,41 +1,43 @@
<script setup lang='ts'>
import { ref, onMounted } from 'vue'
import {storeToRefs } from 'pinia'
import { IMusic } from '@/types/storeType';
import type { IMusic } from '@/types/storeType'
import useStore from '@/store'
import { readFileData } from '@/utils/file'
import useStore from '@/store';
import localforage from 'localforage'
import { storeToRefs } from 'pinia'
const audioUploadToast = ref(0) //0是不显示1是成功2是失败,3是不是图片
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const audioUploadToast = ref(0) // 0是不显示1是成功2是失败,3是不是图片
const audioDbStore = localforage.createInstance({
name: 'audioStore'
name: 'audioStore',
})
const globalConfig = useStore().globalConfig
const { getMusicList: localMusicList } = storeToRefs(globalConfig);
const { getMusicList: localMusicList } = storeToRefs(globalConfig)
const limitType = ref('audio/*')
const localMusicListValue = ref(localMusicList)
const play = async (item: IMusic) => {
globalConfig.setCurrentMusic(item,false)
async function play(item: IMusic) {
globalConfig.setCurrentMusic(item, false)
}
const deleteMusic = (item: IMusic) => {
function deleteMusic(item: IMusic) {
globalConfig.removeMusic(item.id)
audioDbStore.removeItem(item.name)
// setTimeout(()=>{
// localMusicListValue.value=localMusicList
// },100)
}
const resetMusic = () => {
function resetMusic() {
globalConfig.resetMusicList()
audioDbStore.clear()
}
const deleteAll = () => {
function deleteAll() {
globalConfig.clearMusicList()
audioDbStore.clear()
}
const getMusicDbStore = async () => {
async function getMusicDbStore() {
const keys = await audioDbStore.keys()
if (keys.length > 0) {
audioDbStore.iterate((value: string, key: string) => {
@@ -47,15 +49,15 @@ const getMusicDbStore = async () => {
})
}
}
const handleFileChange = async (e: Event) => {
async function handleFileChange(e: Event) {
const isAudio = /audio*/.test(((e.target as HTMLInputElement).files as FileList)[0].type)
if (!isAudio) {
audioUploadToast.value = 3
return
}
let { dataUrl, fileName } = await readFileData(((e.target as HTMLInputElement).files as FileList)[0])
audioDbStore.setItem(new Date().getTime().toString() + '+' + fileName, dataUrl)
const { dataUrl, fileName } = await readFileData(((e.target as HTMLInputElement).files as FileList)[0])
audioDbStore.setItem(`${new Date().getTime().toString()}+${fileName}`, dataUrl)
.then(() => {
audioUploadToast.value = 1
getMusicDbStore()
@@ -65,7 +67,6 @@ const handleFileChange = async (e: Event) => {
})
}
onMounted(() => {
getMusicDbStore()
})
@@ -74,13 +75,19 @@ onMounted(() => {
<template>
<div>
<div class="flex gap-3">
<button class="btn btn-primary btn-sm" @click="resetMusic">重置音乐列表</button>
<button class="btn btn-primary btn-sm" @click="resetMusic">
{{ t('button.reset') }}
</button>
<label for="explore">
<input type="file" class="" id="explore" style="display: none" @change="handleFileChange"
:accept="limitType" />
<span class="btn btn-primary btn-sm">上传音乐</span>
<input
id="explore" type="file" class="" style="display: none" :accept="limitType"
@change="handleFileChange"
>
<span class="btn btn-primary btn-sm">{{ t('button.upload') }}</span>
</label>
<button class="btn btn-error btn-sm" @click="deleteAll">删除所有</button>
<button class="btn btn-error btn-sm" @click="deleteAll">
{{ t('button.allDelete') }}
</button>
</div>
<div>
<ul class="p-0">
@@ -90,8 +97,12 @@ onMounted(() => {
{{ item.name }}</span>
</div>
<div class="flex gap-3">
<button class="btn btn-primary btn-xs" @click="play(item)">播放</button>
<button class="btn btn-error btn-xs" @click="deleteMusic(item)">删除</button>
<button class="btn btn-primary btn-xs" @click="play(item)">
{{ t('button.play') }}
</button>
<button class="btn btn-error btn-xs" @click="deleteMusic(item)">
{{ t('button.delete') }}
</button>
</div>
</li>
</ul>

View File

@@ -1,32 +1,34 @@
<script setup lang='ts'>
import {computed} from 'vue';
const props=defineProps({
rowCount:{
type:Number,
default:17
import { computed } from 'vue'
const props = defineProps({
rowCount: {
type: Number,
default: 17,
},
cardColor:{
type:String,
default:'#fff'
cardColor: {
type: String,
default: '#fff',
},
patternColor:{
type:String,
default:'#000'
patternColor: {
type: String,
default: '#000',
},
patternList: {
type: Array,
default: () => [],
},
patternList:{
type:Array,
default:()=>[]
}
})
const data=computed(()=>{
const data = computed(() => {
return props
})
const updatePatternList=(event:Event,item:number)=>{
if(data.value.patternList.includes(item)){
const index=data.value.patternList.indexOf(item)
data.value.patternList.splice(index,1)
}else{
function updatePatternList(event: Event, item: number) {
if (data.value.patternList.includes(item)) {
const index = data.value.patternList.indexOf(item)
data.value.patternList.splice(index, 1)
}
else {
data.value.patternList.push(item)
}
// emits
@@ -34,12 +36,11 @@ const updatePatternList=(event:Event,item:number)=>{
</script>
<template>
<div class="w-full h-auto" >
<ul class="pattern-list" :style="{gridTemplateColumns:'repeat('+data.rowCount+',1fr)'}">
<li @click.stop="(event)=>updatePatternList(event,item)" class="w-5 h-5" v-for="item in data.rowCount*7" :key="item" :style="{backgroundColor:data.patternList.includes(item)?data.patternColor:data.cardColor}">
</li>
<div class="w-full h-auto">
<ul class="pattern-list" :style="{ gridTemplateColumns: `repeat(${data.rowCount},1fr)` }">
<li v-for="item in data.rowCount * 7" :key="item" class="w-5 h-5" :style="{ backgroundColor: data.patternList.includes(item) ? data.patternColor : data.cardColor }" @click.stop="(event) => updatePatternList(event, item)" />
</ul>
</div>
</div>
</template>
<style lang='scss' scoped>

View File

@@ -1,14 +1,17 @@
<!-- eslint-disable vue/no-parsing-error -->
<script setup lang='ts'>
import { ref, onMounted } from 'vue';
import useStore from '@/store'
import { IPersonConfig } from '@/types/storeType';
import { storeToRefs } from 'pinia'
import * as XLSX from 'xlsx'
import { readFileBinary } from '@/utils/file'
import { addOtherInfo } from '@/utils'
import type { IPersonConfig } from '@/types/storeType'
import DaiysuiTable from '@/components/DaiysuiTable/index.vue'
import i18n from '@/locales/i18n'
import useStore from '@/store'
import { addOtherInfo } from '@/utils'
import { readFileBinary } from '@/utils/file'
import { storeToRefs } from 'pinia'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import * as XLSX from 'xlsx'
const { t } = useI18n()
const personConfig = useStore().personConfig
const { getAllPersonList: allPersonList, getAlreadyPersonList: alreadyPersonList } = storeToRefs(personConfig)
const limitType = '.xlsx,.xls'
@@ -17,16 +20,16 @@ const limitType = '.xlsx,.xls'
const resetDataDialog = ref()
const delAllDataDialog = ref()
const handleFileChange = async (e: Event) => {
let dataBinary = await readFileBinary(((e.target as HTMLInputElement).files as FileList)[0]!)
let workBook = XLSX.read(dataBinary, { type: 'binary', cellDates: true })
let workSheet = workBook.Sheets[workBook.SheetNames[0]]
async function handleFileChange(e: Event) {
const dataBinary = await readFileBinary(((e.target as HTMLInputElement).files as FileList)[0]!)
const workBook = XLSX.read(dataBinary, { type: 'binary', cellDates: true })
const workSheet = workBook.Sheets[workBook.SheetNames[0]]
const excelData = XLSX.utils.sheet_to_json(workSheet)
const allData = addOtherInfo(excelData);
const allData = addOtherInfo(excelData)
personConfig.resetPerson()
personConfig.addNotPersonList(allData)
}
const exportData = () => {
function exportData() {
let data = JSON.parse(JSON.stringify(allPersonList.value))
// 排除一些字段
for (let i = 0; i < data.length; i++) {
@@ -38,9 +41,10 @@ const exportData = () => {
delete data[i].prizeId
// 修改字段名称
if (data[i].isWin) {
data[i].isWin = '是'
} else {
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(',')
@@ -48,13 +52,13 @@ const exportData = () => {
}
let dataString = JSON.stringify(data)
dataString = dataString
.replaceAll(/uid/g, '编号')
.replaceAll(/isWin/g, '是否中奖')
.replaceAll(/department/g, '部门')
.replaceAll(/name/g, '姓名')
.replaceAll(/identity/g, '身份')
.replaceAll(/prizeName/g, '获奖')
.replaceAll(/prizeTime/g, '获奖时间')
.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)
@@ -66,44 +70,44 @@ const exportData = () => {
}
}
const resetData = () => {
function resetData() {
personConfig.resetAlreadyPerson()
}
const deleteAll = () => {
function deleteAll() {
personConfig.deleteAllPerson()
}
const delPersonItem = (row: IPersonConfig) => {
function delPersonItem(row: IPersonConfig) {
personConfig.deletePerson(row)
}
const tableColumns = [
{
label: '编号',
label: i18n.global.t('data.number'),
props: 'uid',
},
{
label: '姓名',
label: i18n.global.t('data.name'),
props: 'name',
},
{
label: '部门',
label: i18n.global.t('data.department'),
props: 'department',
},
{
label: '身份',
label: i18n.global.t('data.identity'),
props: 'identity',
},
{
label: '是否已中奖',
label: i18n.global.t('data.isWin'),
props: 'isWin',
formatValue(row: IPersonConfig) {
return row.isWin ? '是' : '否'
}
return row.isWin ? i18n.global.t('data.yes') : i18n.global.t('data.no')
},
},
{
label: '操作',
label: i18n.global.t('data.operation'),
actions: [
// {
// label: '编辑',
@@ -113,14 +117,14 @@ const tableColumns = [
// }
// },
{
label: '删除',
label: i18n.global.t('data.delete'),
type: 'btn-error',
onClick: (row: IPersonConfig) => {
delPersonItem(row)
}
},
},
]
],
},
]
onMounted(() => {
@@ -130,62 +134,85 @@ onMounted(() => {
<template>
<dialog id="my_modal_1" ref="resetDataDialog" class="border-none modal">
<div class="modal-box">
<h3 class="text-lg font-bold">提示!</h3>
<p class="py-4">该操作会清空人员中奖信息是否继续</p>
<h3 class="text-lg font-bold">
{{ t('dialog.titleTip') }}
</h3>
<p class="py-4">
{{ t('dialog.dialogResetWinner') }}
</p>
<div class="modal-action">
<form method="dialog" class="flex gap-3">
<!-- if there is a button in form, it will close the modal -->
<button class="btn" @click="resetDataDialog.close()">取消</button>
<button class="btn" @click="resetData">确定</button>
<button class="btn" @click="resetDataDialog.close()">
{{ t('button.cancel') }}
</button>
<button class="btn" @click="resetData">
{{ t('button.confirm') }}
</button>
</form>
</div>
</div>
</dialog>
<dialog id="my_modal_1" ref="delAllDataDialog" class="border-none modal">
<div class="modal-box">
<h3 class="text-lg font-bold">提示!</h3>
<p class="py-4">该操作会删除所有人员数据是否继续</p>
<h3 class="text-lg font-bold">
{{ t('dialog.titleTip') }}
</h3>
<p class="py-4">
{{ t('dialog.dialogDelAllPerson') }}
</p>
<div class="modal-action">
<form method="dialog" class="flex gap-3">
<!-- if there is a button in form, it will close the modal -->
<button class="btn" @click="delAllDataDialog.close()">取消</button>
<button class="btn" @click="deleteAll">确定</button>
<button class="btn" @click="delAllDataDialog.close()">
{{ t('button.cancel') }}
</button>
<button class="btn" @click="deleteAll">
{{ t('button.confirm') }}
</button>
</form>
</div>
</div>
</dialog>
<div class="min-w-1000px">
<h2>人员管理</h2>
<h2>{{ t('viewTitle.personManagement') }}</h2>
<div class="flex gap-3">
<button class="btn btn-error btn-sm" @click="delAllDataDialog.showModal()">全部删除</button>
<div class="tooltip tooltip-bottom" data-tip="下载文件后请在excel中填写数据并保存为xlsx格式">
<a class="no-underline btn btn-secondary btn-sm" download="人口登记表.xlsx" target="_blank"
href="/log-lottery/人口登记表.xlsx">下载模板</a>
<button class="btn btn-error btn-sm" @click="delAllDataDialog.showModal()">
{{ t('button.allDelete') }}
</button>
<div class="tooltip tooltip-bottom" :data-tip="t('tooltip.downloadTemplateTip')">
<a
class="no-underline btn btn-secondary btn-sm" :download="t('data.xlsxName')" target="_blank"
:href="`/log-lottery/${t('data.xlsxName')}`"
>{{ t('button.downloadTemplate') }}</a>
</div>
<div class="">
<label for="explore">
<div class="tooltip tooltip-bottom" data-tip="上传修改好的excel文件">
<input type="file" class="" id="explore" style="display: none" @change="handleFileChange"
:accept="limitType" />
<div class="tooltip tooltip-bottom" :data-tip="t('tooltip.uploadExcelTip')">
<input
id="explore" type="file" class="" style="display: none" :accept="limitType"
@change="handleFileChange"
>
<span class="btn btn-primary btn-sm">导入人员数据</span>
<span class="btn btn-primary btn-sm">{{ t('button.importData') }}</span>
</div>
</label>
<!-- <button class="btn btn-primary btn-sm">上传excel</button> -->
</div>
<button class="btn btn-error btn-sm" @click="resetDataDialog.showModal()">重置人员数据</button>
<button class="btn btn-accent btn-sm" @click="exportData">导出结果</button>
<button class="btn btn-error btn-sm" @click="resetDataDialog.showModal()">
{{ t('button.resetData') }}
</button>
<button class="btn btn-accent btn-sm" @click="exportData">
{{ t('button.exportResult') }}
</button>
<div>
<span>中奖人数</span>
<span>{{ t('table.luckyPeopleNumber') }}:</span>
<span>{{ alreadyPersonList.length }}</span>
<span>&nbsp;/&nbsp;</span>
<span>{{ allPersonList.length }}</span>
</div>
</div>
<DaiysuiTable :tableColumns="tableColumns" :data="allPersonList"></DaiysuiTable>
<DaiysuiTable :table-columns="tableColumns" :data="allPersonList" />
</div>
</template>

View File

@@ -1,11 +1,14 @@
<!-- eslint-disable vue/no-parsing-error -->
<script setup lang='ts'>
import { ref } from 'vue';
import useStore from '@/store'
import { IPersonConfig } from '@/types/storeType';
import { storeToRefs } from 'pinia';
import type { IPersonConfig } from '@/types/storeType'
import DaiysuiTable from '@/components/DaiysuiTable/index.vue'
import i18n from '@/locales/i18n'
import useStore from '@/store'
import { storeToRefs } from 'pinia'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const personConfig = useStore().personConfig
const { getAlreadyPersonList: alreadyPersonList, getAlreadyPersonDetail: alreadyPersonDetail } = storeToRefs(personConfig)
@@ -13,118 +16,115 @@ const { getAlreadyPersonList: alreadyPersonList, getAlreadyPersonDetail: already
// alreadyPersonList
// )
// const deleteAll = () => {
// personConfig.deleteAllPerson()
// }
const isDetail = ref(false)
const handleMoveNotPerson = (row: IPersonConfig) => {
function handleMoveNotPerson(row: IPersonConfig) {
personConfig.moveAlreadyToNot(row)
}
const tableColumnsList = [
{
label: '编号',
label: i18n.global.t('data.number'),
props: 'uid',
sort: true
sort: true,
},
{
label: '姓名',
label: i18n.global.t('data.number'),
props: 'name',
},
{
label: '部门',
label: i18n.global.t('data.department'),
props: 'department',
},
{
label: '身份',
label: i18n.global.t('data.identity'),
props: 'identity',
},
{
label: '奖品',
label: i18n.global.t('data.prizeName'),
props: 'prizeName',
sort: true
sort: true,
},
{
label: '操作',
label: i18n.global.t('data.operation'),
actions: [
{
label: '移入未中奖名单',
label: i18n.global.t('data.removePerson'),
type: 'btn-info',
onClick: (row: IPersonConfig) => {
handleMoveNotPerson(row)
}
},
]
},
],
},
]
const tableColumnsDetail = [
{
label: '编号',
label: i18n.global.t('data.number'),
props: 'uid',
sort: true
sort: true,
},
{
label: '姓名',
label: i18n.global.t('data.number'),
props: 'name',
},
{
label: '部门',
label: i18n.global.t('data.department'),
props: 'department',
},
{
label: '身份',
label: i18n.global.t('data.identity'),
props: 'identity',
},
{
label: '奖品',
label: i18n.global.t('data.prizeName'),
props: 'prizeName',
sort: true
sort: true,
},
{
label: '中奖时间',
label: i18n.global.t('data.prizeTime'),
props: 'prizeTime',
},
{
label: '操作',
label: i18n.global.t('data.operation'),
actions: [
{
label: '移入未中奖名单',
label: i18n.global.t('data.removePerson'),
type: 'btn-info',
onClick: (row: IPersonConfig) => {
handleMoveNotPerson(row)
}
},
},
]
],
},
]
</script>
<template>
<div class="overflow-y-auto">
<h2>已中奖人员管理</h2>
<h2>{{ t('viewTitle.winnerManagement') }}</h2>
<div class="flex items-center justify-start gap-10">
<!-- <button class="btn btn-error btn-sm" @click="deleteAll">全部删除</button> -->
<div>
<span>中奖人数</span>
<span>{{ t('table.luckyPeopleNumber') }}</span>
<span>{{ alreadyPersonList.length }}</span>
</div>
<div class="flex flex-col">
<div class="form-control">
<label class="cursor-pointer label">
<span class="label-text">详细信息:</span>
<input type="checkbox" class="border-solid toggle toggle-primary border-1" v-model="isDetail" />
<span class="label-text">{{ t('table.detail') }}:</span>
<input v-model="isDetail" type="checkbox" class="border-solid toggle toggle-primary border-1">
</label>
</div>
</div>
</div>
<DaiysuiTable v-if="!isDetail" :tableColumns="tableColumnsList" :data="alreadyPersonList"></DaiysuiTable>
<DaiysuiTable v-if="!isDetail" :table-columns="tableColumnsList" :data="alreadyPersonList" />
<DaiysuiTable v-if="isDetail" :tableColumns="tableColumnsDetail" :data="alreadyPersonDetail"></DaiysuiTable>
<DaiysuiTable v-if="isDetail" :table-columns="tableColumnsDetail" :data="alreadyPersonDetail" />
</div>
</template>

View File

@@ -1,11 +1,9 @@
<script setup lang='ts'>
</script>
<template>
<router-view></router-view>
<router-view />
</template>
<style lang='scss' scoped>

View File

@@ -1,13 +1,16 @@
<script setup lang='ts'>
import { ref, onMounted, watch } from 'vue'
import useStore from '@/store'
import { IPrizeConfig } from '@/types/storeType'
import { storeToRefs } from 'pinia'
import localforage from 'localforage'
import type { IPrizeConfig } from '@/types/storeType'
import EditSeparateDialog from '@/components/NumberSeparate/EditSeparateDialog.vue'
import i18n from '@/locales/i18n'
import useStore from '@/store'
import localforage from 'localforage'
import { storeToRefs } from 'pinia'
import { onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const imageDbStore = localforage.createInstance({
name: 'imgStore'
name: 'imgStore',
})
const prizeConfig = useStore().prizeConfig
const globalConfig = useStore().globalConfig
@@ -19,10 +22,10 @@ const imgList = ref<any[]>([])
const selectedPrize = ref<IPrizeConfig | null>()
const addPrize = () => {
function addPrize() {
const defaultPrizeCOnfig: IPrizeConfig = {
id: new Date().getTime().toString(),
name: '奖项',
name: i18n.global.t('data.prizeName'),
sort: 0,
isAll: false,
count: 1,
@@ -30,11 +33,11 @@ const addPrize = () => {
picture: {
id: '',
name: '',
url: ''
url: '',
},
separateCount: {
enable: false,
countList: []
countList: [],
},
desc: '',
isUsed: false,
@@ -44,7 +47,7 @@ const addPrize = () => {
prizeConfig.addPrizeConfig(defaultPrizeCOnfig)
}
const selectPrize = (item: IPrizeConfig) => {
function selectPrize(item: IPrizeConfig) {
selectedPrize.value = item
selectedPrize.value.isUsedCount = 0
selectedPrize.value.isUsed = false
@@ -59,12 +62,12 @@ const selectPrize = (item: IPrizeConfig) => {
id: '0',
count: item.count,
isUsedCount: 0,
}
]
},
],
}
}
const changePrizeStatus = (item: IPrizeConfig) => {
function changePrizeStatus(item: IPrizeConfig) {
// if (item.isUsed == true) {
// item.isUsedCount = 0;
// if (item.separateCount && item.separateCount.countList.length) {
@@ -81,58 +84,59 @@ const changePrizeStatus = (item: IPrizeConfig) => {
// })
// }
// }
item.isUsed?item.isUsedCount=0:item.isUsedCount=item.count;
item.isUsed ? item.isUsedCount = 0 : item.isUsedCount = item.count
item.separateCount.countList = []
item.isUsed = !item.isUsed
}
const changePrizePerson = (item: IPrizeConfig) => {
let indexPrize = -1;
function changePrizePerson(item: IPrizeConfig) {
let indexPrize = -1
for (let i = 0; i < prizeList.value.length; i++) {
if (prizeList.value[i].id == item.id) {
indexPrize = i;
break;
if (prizeList.value[i].id === item.id) {
indexPrize = i
break
}
}
if (indexPrize > -1) {
prizeList.value[indexPrize].separateCount.countList = []
prizeList.value[indexPrize].isUsed?prizeList.value[indexPrize].isUsedCount=prizeList.value[indexPrize].count:prizeList.value[indexPrize].isUsedCount=0
prizeList.value[indexPrize].isUsed ? prizeList.value[indexPrize].isUsedCount = prizeList.value[indexPrize].count : prizeList.value[indexPrize].isUsedCount = 0
}
}
const submitData = (value: any) => {
selectedPrize.value!.separateCount.countList = value;
function submitData(value: any) {
selectedPrize.value!.separateCount.countList = value
selectedPrize.value = null
}
const resetDefault = () => {
function resetDefault() {
prizeConfig.resetDefault()
}
const getImageDbStore = async () => {
async function getImageDbStore() {
const keys = await imageDbStore.keys()
if (keys.length > 0) {
imageDbStore.iterate((value, key) => {
imgList.value.push({
key,
value
value,
})
})
}
}
const sort = (item: IPrizeConfig, isUp: number) => {
function sort(item: IPrizeConfig, isUp: number) {
const itemIndex = prizeList.value.indexOf(item)
if (isUp == 1) {
if (isUp === 1) {
prizeList.value.splice(itemIndex, 1)
prizeList.value.splice(itemIndex - 1, 0, item)
} else {
}
else {
prizeList.value.splice(itemIndex, 1)
prizeList.value.splice(itemIndex + 1, 0, item)
}
}
const delItem = (item: IPrizeConfig) => {
function delItem(item: IPrizeConfig) {
prizeConfig.deletePrizeConfig(item.id)
}
const delAll = async () => {
async function delAll() {
await prizeConfig.deleteAllPrizeConfig()
}
onMounted(() => {
@@ -141,120 +145,142 @@ onMounted(() => {
watch(() => prizeList.value, (val: IPrizeConfig[]) => {
prizeConfig.setPrizeConfig(val)
}, { deep: true })
</script>
<template>
<div>
<h2>奖项配置</h2>
<h2>{{ t('viewTitle.prizeManagement') }}</h2>
<div class="flex w-full gap-3">
<button class="btn btn-info btn-sm" @click="addPrize">添加</button>
<button class="btn btn-info btn-sm" @click="resetDefault">默认列表</button>
<button class="btn btn-error btn-sm" @click="delAll">全部删除</button>
<button class="btn btn-info btn-sm" @click="addPrize">
{{ t('button.add') }}
</button>
<button class="btn btn-info btn-sm" @click="resetDefault">
{{ t('button.resetDefault') }}
</button>
<button class="btn btn-error btn-sm" @click="delAll">
{{ t('button.allDelete') }}
</button>
</div>
<div role="alert" class="w-full my-4 alert alert-info">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-6 h-6 stroke-current shrink-0">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
<path
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>进行操作可能会重置数据请谨慎操作</span>
<span>{{ t('dialog.tipResetPrize') }}</span>
</div>
<ul class="p-0 m-0">
<li v-for="item in prizeList" :key="item.id" class="flex gap-10"
:class="currentPrize.id == item.id ? 'border-1 border-dotted rounded-xl' : null">
<li
v-for="item in prizeList" :key="item.id" class="flex gap-10"
:class="currentPrize.id === item.id ? 'border-1 border-dotted rounded-xl' : null"
>
<label class="max-w-xs mb-10 form-control">
<!-- 向上向下 -->
<div class="flex flex-col items-center gap-2 pt-5">
<svg-icon class="cursor-pointer hover:text-blue-400"
:class="prizeList.indexOf(item) == 0 ? 'opacity-0 cursor-default' : ''" name="up"
@click="sort(item, 1)"></svg-icon>
<svg-icon class="cursor-pointer hover:text-blue-400" name="down" @click="sort(item, 0)"
:class="prizeList.indexOf(item) == prizeList.length - 1 ? 'opacity-0 cursor-default' : ''"></svg-icon>
<svg-icon
class="cursor-pointer hover:text-blue-400"
:class="prizeList.indexOf(item) === 0 ? 'opacity-0 cursor-default' : ''" name="up"
@click="sort(item, 1)"
/>
<svg-icon
class="cursor-pointer hover:text-blue-400" name="down" :class="prizeList.indexOf(item) === prizeList.length - 1 ? 'opacity-0 cursor-default' : ''"
@click="sort(item, 0)"
/>
</div>
</label>
<label class="w-1/2 max-w-xs mb-10 form-control">
<div class="label">
<span class="label-text">名称</span>
<span class="label-text">{{ t('table.prizeName') }}</span>
</div>
<input type="text" v-model="item.name" placeholder="名称"
class="w-full max-w-xs input-sm input input-bordered" />
<input
v-model="item.name" type="text" :placeholder="t('placeHolder.name')"
class="w-full max-w-xs input-sm input input-bordered"
>
</label>
<label class="w-1/2 max-w-xs mb-10 form-control">
<div class="label">
<span class="label-text">全员参加</span>
<span class="label-text">{{ t('table.fullParticipation') }}</span>
</div>
<input type="checkbox" :checked="item.isAll" @change="item.isAll = !item.isAll"
class="mt-2 border-solid checkbox checkbox-secondary border-1" />
<input
type="checkbox" :checked="item.isAll" class="mt-2 border-solid checkbox checkbox-secondary border-1"
@change="item.isAll = !item.isAll"
>
</label>
<label class="w-1/2 max-w-xs mb-10 form-control">
<div class="label">
<span class="label-text">抽奖人数</span>
<span class="label-text">{{ t('table.numberParticipants') }}</span>
</div>
<input type="number" v-model="item.count" placeholder="获奖人数" @change="changePrizePerson(item)"
class="w-full max-w-xs p-0 m-0 input-sm input input-bordered" />
<div class="tooltip tooltip-bottom" :data-tip="'已抽取:' + item.isUsedCount + '/' + item.count">
<progress class="w-full progress" :value="item.isUsedCount" :max="item.count"></progress>
<input
v-model="item.count" type="number" :placeholder="t('placeHolder.winnerCount')" class="w-full max-w-xs p-0 m-0 input-sm input input-bordered"
@change="changePrizePerson(item)"
>
<div class="tooltip tooltip-bottom" :data-tip="`${t('table.isDone') + item.isUsedCount}/${item.count}`">
<progress class="w-full progress" :value="item.isUsedCount" :max="item.count" />
</div>
</label>
<!-- <label class="w-1/2 max-w-xs mb-10 form-control">
<div class="label">
<span class="label-text">已获奖人数</span>
</div>
<input disabled type="number" v-model="item.isUsedCount" placeholder="获奖人数"
class="w-full max-w-xs input-sm input input-bordered" />
</label> -->
<label class="w-1/2 max-w-xs mb-10 form-control">
<div class="label">
<span class="label-text">已抽取</span>
<span class="label-text">{{ t('table.isDone') }}</span>
</div>
<input type="checkbox" :checked="item.isUsed" @change="changePrizeStatus(item)"
class="mt-2 border-solid checkbox checkbox-secondary border-1" />
<input
type="checkbox" :checked="item.isUsed" class="mt-2 border-solid checkbox checkbox-secondary border-1"
@change="changePrizeStatus(item)"
>
</label>
<label class="w-full max-w-xs mb-10 form-control">
<div class="label">
<span class="label-text">图片</span>
<span class="label-text">{{ t('table.image') }}</span>
</div>
<select class="w-full max-w-xs select select-warning select-sm" v-model="item.picture">
<select v-model="item.picture" class="w-full max-w-xs select select-warning select-sm">
<option v-if="item.picture.id" :value="{ id: '', name: '', url: '' }"></option>
<option disabled selected>选择一张图片</option>
<option disabled selected>{{ t('table.selectPicture') }}</option>
<option v-for="picItem in localImageList" :key="picItem.id" :value="picItem">{{ picItem.name }}
</option>
</select>
</label>
<label class="w-full max-w-xs mb-10 form-control" v-if="item.separateCount">
<label v-if="item.separateCount" class="w-full max-w-xs mb-10 form-control">
<div class="label">
<span class="label-text">单次抽取个数</span>
<span class="label-text">{{ t('table.onceNumber') }}</span>
</div>
<div class="flex justify-start w-full h-full" @click="selectPrize(item)">
<ul class="flex flex-wrap w-full h-full gap-1 p-0 pt-1 m-0 cursor-pointer"
v-if="item.separateCount.countList.length">
<li class="relative flex items-center justify-center w-8 h-8 bg-slate-600/60 separated"
v-for="se in item.separateCount.countList" :key="se.id">
<div class="flex items-center justify-center w-full h-full tooltip"
:data-tip="'已抽取:' + se.isUsedCount + '/' + se.count">
<div class="absolute left-0 z-50 h-full bg-blue-300/80"
:style="`width:${se.isUsedCount * 100 / se.count}%`"></div>
<ul
v-if="item.separateCount.countList.length"
class="flex flex-wrap w-full h-full gap-1 p-0 pt-1 m-0 cursor-pointer"
>
<li
v-for="se in item.separateCount.countList"
:key="se.id" class="relative flex items-center justify-center w-8 h-8 bg-slate-600/60 separated"
>
<div
class="flex items-center justify-center w-full h-full tooltip"
:data-tip="`${t('tooltip.doneCount') + se.isUsedCount}/${se.count}`"
>
<div
class="absolute left-0 z-50 h-full bg-blue-300/80"
:style="`width:${se.isUsedCount * 100 / se.count}%`"
/>
<span>{{ se.count }}</span>
</div>
</li>
</ul>
<button v-else class="btn btn-secondary btn-xs">设置</button>
<button v-else class="btn btn-secondary btn-xs">{{ t('button.setting') }}</button>
</div>
</label>
<label class="w-full max-w-xs mb-10 form-control">
<div class="label">
<span class="label-text">操作</span>
<span class="label-text">{{ t('table.operation') }}</span>
</div>
<div class="flex gap-2">
<button class="btn btn-error btn-sm" @click="delItem(item)">删除</button>
<button class="btn btn-error btn-sm" @click="delItem(item)">{{ t('button.delete') }}</button>
</div>
</label>
</li>
</ul>
<EditSeparateDialog :totalNumber="selectedPrize?.count" :separated-number="selectedPrize?.separateCount.countList"
@submitData="submitData" />
<EditSeparateDialog
:total-number="selectedPrize?.count" :separated-number="selectedPrize?.separateCount.countList"
@submit-data="submitData"
/>
</div>
</template>

View File

@@ -1,12 +1,14 @@
<script setup lang='ts'>
import {ref,onMounted} from 'vue'
import i18n from '@/locales/i18n'
import markdownit from 'markdown-it'
import { onMounted, ref } from 'vue'
const md = markdownit()
const readmeHtml=ref('')
const readMd=()=>{
fetch('/log-lottery/readme.md')
.then(res=>res.text())
.then(res=>{
const readmeHtml = ref('')
function readMd() {
fetch(`/log-lottery/${i18n.global.t('data.readmeName')}`)
.then(res => res.text())
.then((res) => {
readmeHtml.value = md.render(res)
})
}
@@ -17,9 +19,9 @@ onMounted(() => {
</script>
<template>
<div class="w-3/4 mb-10 ml-3">
<div class="markdown-body" v-dompurify-html="readmeHtml"></div>
</div>
<div class="w-3/4 mb-10 ml-3">
<div v-dompurify-html="readmeHtml" class="markdown-body" />
</div>
</template>
<style scoped>

View File

@@ -1,88 +1,94 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { configRoutes } from '../../router';
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute, useRouter } from 'vue-router'
import { configRoutes } from '../../router'
const router = useRouter();
const route = useRoute();
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const menuList = ref<any[]>(configRoutes.children)
const cleanMenuList = (menu: any) => {
const newList = menu;
function cleanMenuList(menu: any) {
const newList = menu
for (let i = 0; i < newList.length; i++) {
if (newList[i].children) {
cleanMenuList(newList[i].children);
cleanMenuList(newList[i].children)
}
if (!newList[i].meta) {
newList.splice(i, 1);
i--;
newList.splice(i, 1)
i--
}
}
return newList;
return newList
}
menuList.value = cleanMenuList(menuList.value);
menuList.value = cleanMenuList(menuList.value)
const skip = (path: string) => {
router.push(path);
function skip(path: string) {
router.push(path)
}
</script>
<template>
<div class="flex min-h-[calc(100%-280px)]">
<ul class="w-56 m-0 mr-3 menu bg-base-200 pt-14">
<ul class="w-56 m-0 mr-3 min-w-56 menu bg-base-200 pt-14">
<li v-for="item in menuList" :key="item.name">
<details open v-if="item.children">
<details v-if="item.children" open>
<summary>{{ item.meta.title }}</summary>
<ul>
<li v-for="subItem in item.children" :key="subItem.name">
<details open v-if="subItem.children">
<details v-if="subItem.children" open>
<summary>{{ subItem.meta!.title }}</summary>
<ul>
<li v-for="subSubItem in subItem.children" :key="subSubItem.name">
<a @click="skip(subItem.path)"
:style="subSubItem.name == route.name ? 'background-color:rgba(12,12,12,0.2)' : ''">{{
<a
:style="subSubItem.name === route.name ? 'background-color:rgba(12,12,12,0.2)' : ''"
@click="skip(subItem.path)"
>{{
subSubItem.meta!.title }}</a>
</li>
</ul>
</details>
<a v-else @click="skip(subItem.path)"
:style="subItem.name == route.name ? 'background-color:rgba(12,12,12,0.2)' : ''">{{
<a
v-else :style="subItem.name === route.name ? 'background-color:rgba(12,12,12,0.2)' : ''"
@click="skip(subItem.path)"
>{{
subItem.meta!.title }}</a>
</li>
</ul>
</details>
<a v-else @click="skip(item.path)"
:style="item.name == route.name ? 'background-color:rgba(12,12,12,0.2)' : ''">{{ item.meta!.title }}</a>
<a
v-else :style="item.name === route.name ? 'background-color:rgba(12,12,12,0.2)' : ''"
@click="skip(item.path)"
>{{ item.meta!.title }}</a>
</li>
</ul>
<router-view class="mt-5"></router-view>
<router-view class="flex-1 mt-5" />
</div>
<footer class="p-10 rounded footer footer-center bg-base-200 text-base-content">
<nav class="grid grid-flow-col gap-4">
<a class="cursor-pointer link link-hover text-inherit" target="_blank" href="https://1kw20.fun">行有不得反求诸己</a>
<a class="cursor-pointer link link-hover text-inherit" target="_blank" href="https://1kw20.fun">{{ t('footer.self-reflection') }}</a>
</nav>
<nav>
<a class="cursor-pointer link link-hover text-inherit" target="_blank" href="https://1kw20.fun">破山中贼易破心中贼难</a>
<a class="cursor-pointer link link-hover text-inherit" target="_blank" href="https://1kw20.fun">{{ t('footer.thiefEasy') }}</a>
</nav>
<nav>
<div class="grid grid-flow-col gap-4">
<a href="https://github.com/LOG1997/log-lottery" target="_blank" class="cursor-pointer text-inherit">
<svg-icon name="github"></svg-icon>
<svg-icon name="github" />
</a>
<a href="https://twitter.com/TaborSwift" target="_blank" class="cursor-pointer "><svg-icon name="twitter"></svg-icon></a>
<a href="https://twitter.com/TaborSwift" target="_blank" class="cursor-pointer "><svg-icon name="twitter" /></a>
<a href="https://www.instagram.com/log.z1997/" target="_blank" class="cursor-pointer ">
<svg-icon name="instagram"></svg-icon>
<svg-icon name="instagram" />
</a>
</div>
</nav>
<aside>
<p class="p-0 m-0">蜀ICP备2021028666号</p>
<p class="p-0 m-0">
蜀ICP备2021028666号
</p>
<p>Copyright © 2024 - All right reserved by Log1997</p>
</aside>
</footer>

View File

@@ -2,9 +2,11 @@
</script>
<template>
<div>
<button class="btn btn-error">打印</button>
</div>
<div>
<button class="btn btn-error">
打印
</button>
</div>
</template>
<style lang='scss' scoped>

View File

@@ -1,14 +1,18 @@
<script setup lang='ts'>
import { ref, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import useStore from '@/store'
import ImageSync from '@/components/ImageSync/index.vue'
import type { IPrizeConfig } from '../../types/storeType'
import defaultPrizeImage from '@/assets/images/龙.png'
import { IPrizeConfig } from '../../types/storeType';
import ImageSync from '@/components/ImageSync/index.vue'
import EditSeparateDialog from '@/components/NumberSeparate/EditSeparateDialog.vue'
import i18n from '@/locales/i18n'
import useStore from '@/store'
import { storeToRefs } from 'pinia'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const prizeConfig = useStore().prizeConfig
const globalConfig = useStore().globalConfig
const system = useStore().system
@@ -21,8 +25,8 @@ const prizeListContainerRef = ref()
const temporaryPrizeRef = ref()
const selectedPrize = ref<IPrizeConfig | null>()
// 获取prizeListRef高度
const getPrizeListHeight = () => {
let height = 200;
function getPrizeListHeight() {
let height = 200
if (prizeListRef.value) {
height = (prizeListRef.value as HTMLElement).offsetHeight
}
@@ -31,25 +35,25 @@ const getPrizeListHeight = () => {
}
const prizeShow = ref(structuredClone(isShowPrizeList.value))
const addTemporaryPrize = () => {
function addTemporaryPrize() {
temporaryPrizeRef.value.showModal()
}
const deleteTemporaryPrize = () => {
function deleteTemporaryPrize() {
temporaryPrize.value.isShow = false
prizeConfig.setTemporaryPrize(temporaryPrize.value)
}
const submitTemporaryPrize = () => {
function submitTemporaryPrize() {
if (!temporaryPrize.value.name || !temporaryPrize.value.count) {
alert('请填写完整信息')
// eslint-disable-next-line no-alert
alert(i18n.global.t('error.completeInformation'))
return
}
temporaryPrize.value.isShow = true
temporaryPrize.value.id=new Date().getTime().toString()
temporaryPrize.value.id = new Date().getTime().toString()
prizeConfig.setCurrentPrize(temporaryPrize.value)
}
const selectPrize = (item: IPrizeConfig) => {
function selectPrize(item: IPrizeConfig) {
selectedPrize.value = item
selectedPrize.value.isUsedCount = 0
selectedPrize.value.isUsed = false
@@ -64,28 +68,28 @@ const selectPrize = (item: IPrizeConfig) => {
id: '0',
count: item.count,
isUsedCount: 0,
}
]
},
],
}
}
const submitData = (value: any) => {
selectedPrize.value!.separateCount.countList = value;
function submitData(value: any) {
selectedPrize.value!.separateCount.countList = value
selectedPrize.value = null
}
const changePersonCount=()=>{
temporaryPrize.value.separateCount.countList=[]
function changePersonCount() {
temporaryPrize.value.separateCount.countList = []
}
const setCurrentPrize=()=>{
for(let i=0;i<localPrizeList.value.length;i++){
if(localPrizeList.value[i].isUsedCount<localPrizeList.value[i].count){
function setCurrentPrize() {
for (let i = 0; i < localPrizeList.value.length; i++) {
if (localPrizeList.value[i].isUsedCount < localPrizeList.value[i].count) {
prizeConfig.setCurrentPrize(localPrizeList.value[i])
return
return
}
}
}
}
onMounted(() => {
prizeListContainerRef.value.style.height = getPrizeListHeight() + 'px'
prizeListContainerRef.value.style.height = `${getPrizeListHeight()}px`
setCurrentPrize()
})
</script>
@@ -94,66 +98,84 @@ onMounted(() => {
<div class="flex items-center">
<dialog id="my_modal_1" ref="temporaryPrizeRef" class="border-none modal">
<div class="modal-box">
<h3 class="text-lg font-bold">增加临时抽奖</h3>
<h3 class="text-lg font-bold">
{{ t('dialog.titleTemporary') }}
</h3>
<div class="flex flex-col gap-3">
<label class="flex w-full max-w-xs">
<div class="label">
<span class="label-text">名称:</span>
<span class="label-text">{{ t('table.name') }}:</span>
</div>
<input type="text" v-model="temporaryPrize.name" placeholder="名称"
class="max-w-xs input-sm input input-bordered" />
<input
v-model="temporaryPrize.name" type="text" :placeholder="t('placeHolder.name')"
class="max-w-xs input-sm input input-bordered"
>
</label>
<label class="flex w-full max-w-xs">
<div class="label">
<span class="label-text">是否全员参加</span>
<span class="label-text">{{ t('table.fullParticipation') }}</span>
</div>
<input type="checkbox" :checked="temporaryPrize.isAll"
<input
type="checkbox" :checked="temporaryPrize.isAll"
class="mt-2 border-solid checkbox checkbox-secondary border-1"
@change="temporaryPrize.isAll = !temporaryPrize.isAll"
class="mt-2 border-solid checkbox checkbox-secondary border-1" />
>
</label>
<label class="flex w-full max-w-xs">
<div class="label">
<span class="label-text">获奖人数</span>
<span class="label-text">{{ t('table.setLuckyNumber') }}</span>
</div>
<input type="number" v-model="temporaryPrize.count" @change="changePersonCount" placeholder="获奖人数"
class="max-w-xs input-sm input input-bordered" />
<input
v-model="temporaryPrize.count" type="number" :placeholder="t('placeHolder.winnerCount')" class="max-w-xs input-sm input input-bordered"
@change="changePersonCount"
>
</label>
<label class="flex w-full max-w-xs">
<div class="label">
<span class="label-text">已获奖人数</span>
<span class="label-text">{{ t('table.luckyPeopleNumber') }}</span>
</div>
<input disabled type="number" v-model="temporaryPrize.isUsedCount" placeholder="获奖人数"
class="max-w-xs input-sm input input-bordered" />
<input
v-model="temporaryPrize.isUsedCount" disabled type="number" :placeholder="t('placeHolder.winnerCount')"
class="max-w-xs input-sm input input-bordered"
>
</label>
<label class="flex w-full max-w-xs" v-if="temporaryPrize.separateCount">
<label v-if="temporaryPrize.separateCount" class="flex w-full max-w-xs">
<div class="label">
<span class="label-text">单次抽取个数</span>
<span class="label-text">{{ t('table.onceNumber') }}</span>
</div>
<div class="flex justify-start h-full" @click="selectPrize(temporaryPrize)">
<ul class="flex flex-wrap w-full h-full gap-1 p-0 pt-1 m-0 cursor-pointer"
v-if="temporaryPrize.separateCount.countList.length">
<li class="relative flex items-center justify-center w-8 h-8 bg-slate-600/60 separated"
v-for="se in temporaryPrize.separateCount.countList" :key="se.id">
<div class="flex items-center justify-center w-full h-full tooltip"
:data-tip="'已抽取:' + se.isUsedCount + '/' + se.count">
<div class="absolute left-0 z-50 h-full bg-blue-300/80"
:style="`width:${se.isUsedCount * 100 / se.count}%`"></div>
<ul
v-if="temporaryPrize.separateCount.countList.length"
class="flex flex-wrap w-full h-full gap-1 p-0 pt-1 m-0 cursor-pointer"
>
<li
v-for="se in temporaryPrize.separateCount.countList"
:key="se.id" class="relative flex items-center justify-center w-8 h-8 bg-slate-600/60 separated"
>
<div
class="flex items-center justify-center w-full h-full tooltip"
:data-tip="`${t('tooltip.doneCount') + se.isUsedCount}/${se.count}`"
>
<div
class="absolute left-0 z-50 h-full bg-blue-300/80"
:style="`width:${se.isUsedCount * 100 / se.count}%`"
/>
<span>{{ se.count }}</span>
</div>
</li>
</ul>
<button v-else class="btn btn-secondary btn-xs">设置</button>
<button v-else class="btn btn-secondary btn-xs">{{ t('button.setting') }}</button>
</div>
</label>
<label class="flex w-full max-w-xs">
<div class="label">
<span class="label-text">图片</span>
<span class="label-text">{{ t('table.image') }}</span>
</div>
<select class="flex-1 w-12 select select-warning select-sm" v-model="temporaryPrize.picture">
<select v-model="temporaryPrize.picture" class="flex-1 w-12 select select-warning select-sm">
<option v-if="temporaryPrize.picture.id" :value="{ id: '', name: '', url: '' }">
</option>
<option disabled selected>选择一张图片</option>
<option class="w-auto" v-for="picItem in localImageList" :key="picItem.id" :value="picItem">{{
<option disabled selected>{{ t('table.selectPicture') }}</option>
<option v-for="picItem in localImageList" :key="picItem.id" class="w-auto" :value="picItem">{{
picItem.name }}
</option>
</select>
@@ -161,89 +183,120 @@ onMounted(() => {
</div>
<div class="modal-action">
<form method="dialog" class="flex gap-3">
<button class="btn btn-sm" @click="submitTemporaryPrize">确定</button>
<button class="btn btn-sm">取消</button>
<button class="btn btn-sm" @click="submitTemporaryPrize">
{{ t('button.confirm') }}
</button>
<button class="btn btn-sm">
{{ t('button.cancel') }}
</button>
</form>
</div>
</div>
</dialog>
<EditSeparateDialog :totalNumber="selectedPrize?.count" :separated-number="selectedPrize?.separateCount.countList"
@submitData="submitData" />
<EditSeparateDialog
:total-number="selectedPrize?.count" :separated-number="selectedPrize?.separateCount.countList"
@submit-data="submitData"
/>
<div ref="prizeListContainerRef">
<div class="h-20 w-72" :class="temporaryPrize.isShow ? 'current-prize' : ''" v-if="temporaryPrize.isShow">
<div v-if="temporaryPrize.isShow" class="h-20 w-72" :class="temporaryPrize.isShow ? 'current-prize' : ''">
<div class="relative flex flex-row items-center justify-between w-full h-full shadow-xl card bg-base-100">
<div v-if="temporaryPrize.isUsed"
class="absolute z-50 w-full h-full bg-gray-800/70 item-mask rounded-xl"></div>
<div
v-if="temporaryPrize.isUsed"
class="absolute z-50 w-full h-full bg-gray-800/70 item-mask rounded-xl"
/>
<figure class="w-10 h-10 rounded-xl">
<ImageSync v-if="temporaryPrize.picture.url" :imgItem="temporaryPrize.picture"></ImageSync>
<img v-else :src="defaultPrizeImage" alt="Prize" class="object-cover h-full rounded-xl" />
<ImageSync v-if="temporaryPrize.picture.url" :img-item="temporaryPrize.picture" />
<img v-else :src="defaultPrizeImage" alt="Prize" class="object-cover h-full rounded-xl">
</figure>
<div class="items-center p-0 text-center card-body">
<div class="tooltip tooltip-left" :data-tip="temporaryPrize.name">
<h2 class="p-0 m-0 overflow-hidden w-28 card-title whitespace-nowrap text-ellipsis">{{
temporaryPrize.name }}</h2>
<h2 class="p-0 m-0 overflow-hidden w-28 card-title whitespace-nowrap text-ellipsis">
{{
temporaryPrize.name }}
</h2>
</div>
<p class="absolute z-40 p-0 m-0 text-gray-300/80 mt-9">{{ temporaryPrize.isUsedCount }}/{{
temporaryPrize.count }}</p>
<progress class="w-3/4 h-6 progress progress-primary" :value="temporaryPrize.isUsedCount"
:max="temporaryPrize.count"></progress>
<p class="absolute z-40 p-0 m-0 text-gray-300/80 mt-9">
{{ temporaryPrize.isUsedCount }}/{{
temporaryPrize.count }}
</p>
<progress
class="w-3/4 h-6 progress progress-primary" :value="temporaryPrize.isUsedCount"
:max="temporaryPrize.count"
/>
<!-- <p class="p-0 m-0">{{ item.isUsedCount }}/{{ item.count }}</p> -->
</div>
<div class="flex flex-col gap-1 mr-2">
<div class="tooltip tooltip-left" data-tip="编辑">
<div class="tooltip tooltip-left" :data-tip="t('tooltip.edit')">
<div class="cursor-pointer hover:text-blue-400" @click="addTemporaryPrize">
<svg-icon name="edit"></svg-icon>
<svg-icon name="edit" />
</div>
</div>
<div class="tooltip tooltip-left" data-tip="删除">
<div class="tooltip tooltip-left" :data-tip="t('tooltip.delete')">
<div class="cursor-pointer hover:text-blue-400" @click="deleteTemporaryPrize">
<svg-icon name="delete"></svg-icon>
<svg-icon name="delete" />
</div>
</div>
</div>
</div>
</div>
<transition name="prize-list" :appear="true">
<div v-if="prizeShow && !isMobile && !temporaryPrize.isShow" class="flex items-center">
<ul class="flex flex-col gap-1 p-2 rounded-xl bg-slate-500/50" ref="prizeListRef">
<li v-for="item in localPrizeList" :key="item.id"
:class="currentPrize.id == item.id ? 'current-prize' : ''">
<div class="relative flex flex-row items-center justify-between w-64 h-20 shadow-xl card bg-base-100"
v-if="item.isShow">
<div v-if="item.isUsed"
class="absolute z-50 w-full h-full bg-gray-800/70 item-mask rounded-xl"></div>
<ul ref="prizeListRef" class="flex flex-col gap-1 p-2 rounded-xl bg-slate-500/50">
<li
v-for="item in localPrizeList" :key="item.id"
:class="currentPrize.id === item.id ? 'current-prize' : ''"
>
<div
v-if="item.isShow"
class="relative flex flex-row items-center justify-between w-64 h-20 shadow-xl card bg-base-100"
>
<div
v-if="item.isUsed"
class="absolute z-50 w-full h-full bg-gray-800/70 item-mask rounded-xl"
/>
<figure class="w-10 h-10 rounded-xl">
<ImageSync v-if="item.picture.url" :imgItem="item.picture"></ImageSync>
<img v-else :src="defaultPrizeImage" alt="Prize"
class="object-cover h-full rounded-xl" />
<ImageSync v-if="item.picture.url" :img-item="item.picture" />
<img
v-else :src="defaultPrizeImage" alt="Prize"
class="object-cover h-full rounded-xl"
>
</figure>
<div class="items-center p-0 text-center card-body">
<div class="tooltip tooltip-left" :data-tip="item.name">
<h2
class="w-24 p-0 m-0 overflow-hidden text-center card-title whitespace-nowrap text-ellipsis">
{{ item.name }}</h2>
class="w-24 p-0 m-0 overflow-hidden text-center card-title whitespace-nowrap text-ellipsis"
>
{{ item.name }}
</h2>
</div>
<p class="absolute z-40 p-0 m-0 text-gray-300/80 mt-9">{{ item.isUsedCount }}/{{
item.count }}</p>
<progress class="w-3/4 h-6 progress progress-primary" :value="item.isUsedCount"
:max="item.count"></progress>
<p class="absolute z-40 p-0 m-0 text-gray-300/80 mt-9">
{{ item.isUsedCount }}/{{
item.count }}
</p>
<progress
class="w-3/4 h-6 progress progress-primary" :value="item.isUsedCount"
:max="item.count"
/>
<!-- <p class="p-0 m-0">{{ item.isUsedCount }}/{{ item.count }}</p> -->
</div>
</div>
</li>
</ul>
<div class="flex flex-col gap-3">
<div class="tooltip tooltip-right" data-tip="奖项列表">
<div class="flex items-center w-6 h-8 rounded-r-lg cursor-pointer prize-option bg-slate-500/50"
@click="prizeShow = !prizeShow">
<svg-icon name="arrow_left" class="w-full h-full"></svg-icon>
<div class="tooltip tooltip-right" :data-tip="t('tooltip.prizeList')">
<div
class="flex items-center w-6 h-8 rounded-r-lg cursor-pointer prize-option bg-slate-500/50"
@click="prizeShow = !prizeShow"
>
<svg-icon name="arrow_left" class="w-full h-full" />
</div>
</div>
<div class="tooltip tooltip-right" data-tip="添加抽奖">
<div class="flex items-center w-6 h-8 rounded-r-lg cursor-pointer prize-option bg-slate-500/50"
@click="addTemporaryPrize">
<svg-icon name="add" class="w-full h-full"></svg-icon>
<div class="tooltip tooltip-right" :data-tip="t('tooltip.addActivity')">
<div
class="flex items-center w-6 h-8 rounded-r-lg cursor-pointer prize-option bg-slate-500/50"
@click="addTemporaryPrize"
>
<svg-icon name="add" class="w-full h-full" />
</div>
</div>
</div>
@@ -252,10 +305,12 @@ onMounted(() => {
</div>
<transition name="prize-operate" :appear="true">
<div class="tooltip tooltip-right" data-tip="奖项列表" v-show="!prizeShow">
<div class="flex items-center w-6 h-8 rounded-r-lg cursor-pointer prize-option bg-slate-500/50"
@click="prizeShow = !prizeShow">
<svg-icon name="arrow_right" class="w-full h-full"></svg-icon>
<div v-show="!prizeShow" class="tooltip tooltip-right" :data-tip="t('tooltip.prizeList')">
<div
class="flex items-center w-6 h-8 rounded-r-lg cursor-pointer prize-option bg-slate-500/50"
@click="prizeShow = !prizeShow"
>
<svg-icon name="arrow_right" class="w-full h-full" />
</div>
</div>
</transition>
@@ -311,7 +366,6 @@ onMounted(() => {
translate: 0% 0%;
}
.current-prize::after {
content: "";
position: absolute;
@@ -401,4 +455,5 @@ onMounted(() => {
100% {
opacity: 1;
}
}</style>
}
</style>

View File

@@ -1,56 +1,39 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, } from 'vue'
import PrizeList from './PrizeList.vue'
import { useElementStyle, useElementPosition } from '@/hooks/useElement'
import type { IPersonConfig } from '@/types/storeType'
import StarsBackground from '@/components/StarsBackground/index.vue'
import confetti from 'canvas-confetti'
import { useElementPosition, useElementStyle } from '@/hooks/useElement'
import i18n from '@/locales/i18n'
import useStore from '@/store'
import { filterData, selectCard } from '@/utils'
import { rgba } from '@/utils/color'
import { IPersonConfig } from '@/types/storeType'
// import * as THREE from 'three'
import { Scene, PerspectiveCamera, Object3D, Vector3 } from 'three'
// import {
// CSS3DRenderer, CSS3DObject
// } from 'three/examples/jsm/renderers/CSS3DRenderer.js';
import { CSS3DRenderer, CSS3DObject } from 'three-css3d'
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js';
// import TrackballControls from 'three-trackballcontrols';
// import TWEEN from 'three/examples/jsm/libs/tween.module.js';
import * as TWEEN from '@tweenjs/tween.js'
import useStore from '@/store'
import confetti from 'canvas-confetti'
import { storeToRefs } from 'pinia'
import { Object3D, PerspectiveCamera, Scene, Vector3 } from 'three'
import { CSS3DObject, CSS3DRenderer } from 'three-css3d'
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'
import { onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toast-notification';
import 'vue-toast-notification/dist/theme-sugar.css';
import { useToast } from 'vue-toast-notification'
import PrizeList from './PrizeList.vue'
import 'vue-toast-notification/dist/theme-sugar.css'
const toast = useToast();
const { t } = useI18n()
const toast = useToast()
const router = useRouter()
const personConfig = useStore().personConfig
const globalConfig = useStore().globalConfig
const prizeConfig = useStore().prizeConfig
const { getAllPersonList: allPersonList,
getNotPersonList: notPersonList,
getNotThisPrizePersonList: notThisPrizePersonList
const { getAllPersonList: allPersonList, getNotPersonList: notPersonList, getNotThisPrizePersonList: notThisPrizePersonList,
} = storeToRefs(personConfig)
const { getCurrentPrize: currentPrize } = storeToRefs(prizeConfig)
const { getTopTitle: topTitle,
getCardColor: cardColor,
getPatterColor: patternColor,
getPatternList: patternList,
getTextColor: textColor,
getLuckyColor: luckyColor,
getCardSize: cardSize,
getTextSize: textSize,
getRowCount: rowCount,
getBackground: homeBackground,
} = storeToRefs(globalConfig)
const { getTopTitle: topTitle, getCardColor: cardColor, getPatterColor: patternColor, getPatternList: patternList, getTextColor: textColor, getLuckyColor: luckyColor, getCardSize: cardSize, getTextSize: textSize, getRowCount: rowCount, getBackground: homeBackground } = storeToRefs(globalConfig)
const tableData = ref<any[]>([])
// const tableData = ref<any[]>(JSON.parse(JSON.stringify(alreadyPersonList.value)).concat(JSON.parse(JSON.stringify(notPersonList.value))))
const currentStatus = ref(0) // 0为初始状态 1为抽奖准备状态2为抽奖中状态3为抽奖结束状态
const ballRotationY = ref(0)
const containerRef = ref<HTMLElement>()
// const LuckyViewRef= ref()
const canOperate = ref(true)
const cameraZ = ref(3000)
@@ -59,188 +42,189 @@ const camera = ref()
const renderer = ref()
const controls = ref()
const objects = ref<any[]>([])
const targets = {
grid: <any[]>[],
helix: <any[]>[],
table: <any[]>[],
sphere: <any[]>[]
};
interface TargetType {
grid: any[]
helix: any[]
table: any[]
sphere: any[]
}
const targets: TargetType = {
grid: [],
helix: [],
table: [],
sphere: [],
}
const luckyTargets = ref<any[]>([])
const luckyCardList = ref<number[]>([])
let luckyCount = ref(10)
const luckyCount = ref(10)
const personPool = ref<IPersonConfig[]>([])
const intervalTimer = ref<any>(null)
// const currentPrizeValue = ref(JSON.parse(JSON.stringify(currentPrize.value)))
// 填充数据,填满七行
function initTableData() {
if (allPersonList.value.length <= 0) {
return
}
const totalCount = rowCount.value * 7
const orginPersonData = JSON.parse(JSON.stringify(allPersonList.value))
const orginPersonLength = orginPersonData.length
if (orginPersonLength < totalCount) {
const repeatCount = Math.ceil(totalCount / orginPersonLength)
const originPersonData = JSON.parse(JSON.stringify(allPersonList.value))
const originPersonLength = originPersonData.length
if (originPersonLength < totalCount) {
const repeatCount = Math.ceil(totalCount / originPersonLength)
// 复制数据
for (let i = 0; i < repeatCount; i++) {
tableData.value = tableData.value.concat(JSON.parse(JSON.stringify(orginPersonData)))
tableData.value = tableData.value.concat(JSON.parse(JSON.stringify(originPersonData)))
}
}
else {
tableData.value = orginPersonData.slice(0, totalCount)
tableData.value = originPersonData.slice(0, totalCount)
}
tableData.value = filterData(tableData.value.slice(0, totalCount), rowCount.value)
}
const init = () => {
const felidView = 40;
const width = window.innerWidth;
const height = window.innerHeight;
const aspect = width / height;
const nearPlane = 1;
const farPlane = 10000;
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);
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.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);
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);
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';
let element = document.createElement('div')
element.className = 'element-card'
const number = document.createElement('div');
number.className = 'card-id';
number.textContent = tableData.value[i].uid;
element.appendChild(number);
const number = document.createElement('div')
number.className = 'card-id'
number.textContent = tableData.value[i].uid
element.appendChild(number)
const symbol = document.createElement('div');
symbol.className = 'card-name';
symbol.textContent = tableData.value[i].name;
element.appendChild(symbol);
const symbol = document.createElement('div')
symbol.className = 'card-name'
symbol.textContent = tableData.value[i].name
element.appendChild(symbol)
const detail = document.createElement('div');
detail.className = 'card-detail';
detail.innerHTML = `${tableData.value[i].department}<br/>${tableData.value[i].identity}`;
element.appendChild(detail);
const detail = document.createElement('div')
detail.className = 'card-detail'
detail.innerHTML = `${tableData.value[i].department}<br/>${tableData.value[i].identity}`
element.appendChild(detail)
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);
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);
objects.value.push(object)
}
createTableVertices();
createSphereVertices();
createHelixVertices();
createTableVertices()
createSphereVertices()
createHelixVertices()
function createTableVertices() {
const tableLen = tableData.value.length;
const tableLen = tableData.value.length
for (let i = 0; i < tableLen; i++) {
const object = new Object3D();
const object = new Object3D()
object.position.x = tableData.value[i].x * (cardSize.value.width + 40) - rowCount.value * 90;
object.position.y = -tableData.value[i].y * (cardSize.value.height + 20) + 1000;
object.position.z = 0;
object.position.x = tableData.value[i].x * (cardSize.value.width + 40) - rowCount.value * 90
object.position.y = -tableData.value[i].y * (cardSize.value.height + 20) + 1000
object.position.z = 0
targets.table.push(object);
targets.table.push(object)
}
}
function createSphereVertices() {
let i = 0;
const objLength = objects.value.length;
const vector = new Vector3();
let i = 0
const objLength = objects.value.length
const vector = new Vector3()
for (; i < objLength; ++i) {
let phi = Math.acos(-1 + (2 * i) / objLength);
let theta = Math.sqrt(objLength * Math.PI) * phi;
const object = new Object3D();
const phi = Math.acos(-1 + (2 * i) / objLength)
const theta = Math.sqrt(objLength * 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);
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);
targets.sphere.push(object);
vector.copy(object.position).multiplyScalar(2)
object.lookAt(vector)
targets.sphere.push(object)
}
}
function createHelixVertices() {
let i = 0;
const vector = new Vector3();
const objLength = objects.value.length;
let i = 0
const vector = new Vector3()
const objLength = objects.value.length
for (; i < objLength; ++i) {
let phi = i * 0.213 + Math.PI;
const phi = i * 0.213 + Math.PI
const object = new Object3D();
const object = new Object3D()
object.position.x = 800 * Math.sin(phi);
object.position.y = -(i * 8) + 450;
object.position.z = 800 * Math.cos(phi + Math.PI);
object.position.x = 800 * Math.sin(phi)
object.position.y = -(i * 8) + 450
object.position.z = 800 * Math.cos(phi + Math.PI)
object.scale.set(1.1, 1.1, 1.1);
object.scale.set(1.1, 1.1, 1.1)
vector.x = object.position.x * 2;
vector.y = object.position.y;
vector.z = object.position.z * 2;
vector.x = object.position.x * 2
vector.y = object.position.y
vector.z = object.position.z * 2
object.lookAt(vector);
object.lookAt(vector)
targets.helix.push(object);
targets.helix.push(object)
}
}
window.addEventListener('resize', onWindowResize, false);
window.addEventListener('resize', onWindowResize, false)
transform(targets.table, 1000)
render();
render()
}
const transform = (targets: any[], duration: number) => {
TWEEN.removeAll();
function transform(targets: any[], duration: number) {
TWEEN.removeAll()
if (intervalTimer.value) {
clearInterval(intervalTimer.value);
clearInterval(intervalTimer.value)
intervalTimer.value = null
randomBallData('sphere')
}
return new Promise((resolve) => {
const objLength = objects.value.length;
const objLength = objects.value.length
for (let i = 0; i < objLength; ++i) {
let object = objects.value[i];
let target = targets[i];
const object = objects.value[i]
const target = targets[i]
new TWEEN.Tween(object.position)
.to({ x: target.position.x, y: target.position.y, z: target.position.z },
Math.random() * duration + duration)
.to({ x: target.position.x, y: target.position.y, z: target.position.z }, Math.random() * duration + duration)
.easing(TWEEN.Easing.Exponential.InOut)
.start();
.start()
new TWEEN.Tween(object.rotation)
.to({ x: target.rotation.x, y: target.rotation.y, z: target.rotation.z }, Math.random() * duration + duration)
@@ -253,11 +237,11 @@ const transform = (targets: any[], duration: number) => {
useElementStyle(item.element, {} as any, i, patternList.value, patternColor.value, cardColor.value, cardSize.value, textSize.value, 'sphere')
})
}
luckyTargets.value = [];
luckyCardList.value = [];
luckyTargets.value = []
luckyCardList.value = []
canOperate.value = true
});
})
}
// 这个补间用来在位置与旋转补间同步执行通过onUpdate在每次更新数据后渲染scene和camera
@@ -268,36 +252,36 @@ const transform = (targets: any[], duration: number) => {
.onComplete(() => {
canOperate.value = true
resolve('')
});
})
})
}
function onWindowResize() {
camera.value.aspect = window.innerWidth / window.innerHeight
camera.value.updateProjectionMatrix();
camera.value.updateProjectionMatrix()
renderer.value.setSize(window.innerWidth, window.innerHeight);
render();
renderer.value.setSize(window.innerWidth, window.innerHeight)
render()
}
/**
* [animation update all tween && controls]
*/
* [animation update all tween && controls]
*/
function animation() {
TWEEN.update();
controls.value.update();
TWEEN.update()
controls.value.update()
// 设置自动旋转
// 设置相机位置
requestAnimationFrame(animation);
requestAnimationFrame(animation)
}
// // 旋转的动画
function rollBall(rotateY: number, duration: number) {
TWEEN.removeAll();
TWEEN.removeAll()
return new Promise((resolve) => {
scene.value.rotation.y = 0;
scene.value.rotation.y = 0
ballRotationY.value = Math.PI * rotateY * 1000
const rotateObj = new TWEEN.Tween(scene.value.rotation);
const rotateObj = new TWEEN.Tween(scene.value.rotation)
rotateObj
.to(
{
@@ -305,9 +289,9 @@ function rollBall(rotateY: number, duration: number) {
x: 0,
y: ballRotationY.value,
// z: Math.PI * rotateZ * 1000
z: 0
z: 0,
},
duration * 1000
duration * 1000,
)
.onUpdate(render)
.start()
@@ -326,9 +310,9 @@ function resetCamera() {
{
x: 0,
y: 0,
z: 3000
z: 3000,
},
1000
1000,
)
.onUpdate(render)
.start()
@@ -338,9 +322,9 @@ function resetCamera() {
{
x: 0,
y: 0,
z: 0
z: 0,
},
1000
1000,
)
.onUpdate(render)
.start()
@@ -359,9 +343,9 @@ function resetCamera() {
}
function render() {
renderer.value.render(scene.value, camera.value);
renderer.value.render(scene.value, camera.value)
}
const enterLottery = async () => {
async function enterLottery() {
if (!canOperate.value) {
return
}
@@ -381,17 +365,17 @@ const enterLottery = async () => {
rollBall(0.1, 2000)
}
// 开始抽奖
const startLottery = () => {
function startLottery() {
if (!canOperate.value) {
return
}
// 验证是否已抽完全部奖项
if (currentPrize.value.isUsed || !currentPrize.value) {
toast.open({
message: '抽奖抽完了',
message: i18n.global.t('error.personIsAllDone'),
type: 'warning',
position: 'top-right',
duration: 10000
duration: 10000,
})
return
@@ -400,13 +384,13 @@ const startLottery = () => {
// 验证抽奖人数是否还够
if (personPool.value.length < currentPrize.value.count - currentPrize.value.isUsedCount) {
toast.open({
message: '抽奖人数不够',
message: i18n.global.t('error.personNotEnough'),
type: 'warning',
position: 'top-right',
duration: 10000
duration: 10000,
})
return;
return
}
luckyCount.value = 10
// 自定义抽奖个数
@@ -417,11 +401,11 @@ const startLottery = () => {
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;
break
}
}
}
leftover < luckyCount.value ? luckyCount.value = leftover : luckyCount
luckyCount.value = leftover < luckyCount.value ? leftover : luckyCount.value
for (let i = 0; i < luckyCount.value; i++) {
if (personPool.value.length > 0) {
const randomIndex = Math.round(Math.random() * (personPool.value.length - 1))
@@ -429,17 +413,19 @@ const startLottery = () => {
personPool.value.splice(randomIndex, 1)
}
}
toast.open({
message: `现在抽取${currentPrize.value.name} ${leftover}`,
// message: `现在抽取${currentPrize.value.name} ${leftover}人`,
message: i18n.global.t('error.startDraw', { count: currentPrize.value.name, leftover }),
type: 'default',
position: 'top-right',
duration: 8000
duration: 8000,
})
currentStatus.value = 2
rollBall(10, 3000)
}
const stopLottery = async () => {
async function stopLottery() {
if (!canOperate.value) {
return
}
@@ -450,16 +436,16 @@ const stopLottery = async () => {
const windowSize = { width: window.innerWidth, height: window.innerHeight }
luckyTargets.value.forEach((person: IPersonConfig, index: number) => {
let cardIndex = selectCard(luckyCardList.value, tableData.value.length, person.id)
const cardIndex = selectCard(luckyCardList.value, tableData.value.length, person.id)
luckyCardList.value.push(cardIndex)
const totalLuckyCount=luckyTargets.value.length
let item = objects.value[cardIndex]
const { xTable, yTable } = useElementPosition(item, rowCount.value,totalLuckyCount, { width: cardSize.value.width * 2, height: cardSize.value.height * 2 }, windowSize, index)
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
z: 1000,
}, 1200)
.easing(TWEEN.Easing.Exponential.InOut)
.onStart(() => {
@@ -474,7 +460,7 @@ const stopLottery = async () => {
.to({
x: 0,
y: 0,
z: 0
z: 0,
}, 900)
.easing(TWEEN.Easing.Exponential.InOut)
.start()
@@ -485,7 +471,7 @@ const stopLottery = async () => {
})
}
// 继续
const continueLottery = async () => {
async function continueLottery() {
if (!canOperate.value) {
return
}
@@ -495,7 +481,7 @@ const continueLottery = async () => {
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;
break
}
}
}
@@ -509,13 +495,13 @@ const continueLottery = async () => {
prizeConfig.updatePrizeConfig(currentPrize.value)
await enterLottery()
}
const quitLottery = () => {
function quitLottery() {
enterLottery()
currentStatus.value = 0
}
// 庆祝动画
const confettiFire = () => {
const duration = 3 * 1000;
function confettiFire() {
const duration = 3 * 1000
const end = Date.now() + duration;
(function frame() {
// launch a few confetti from the left edge
@@ -523,60 +509,60 @@ const confettiFire = () => {
particleCount: 2,
angle: 60,
spread: 55,
origin: { x: 0 }
});
origin: { x: 0 },
})
// and launch a few from the right edge
confetti({
particleCount: 2,
angle: 120,
spread: 55,
origin: { x: 1 }
});
origin: { x: 1 },
})
// keep going until we are out of time
if (Date.now() < end) {
requestAnimationFrame(frame);
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
});
scalar: 0.8,
})
centerFire(0.1, {
spread: 120,
startVelocity: 25,
decay: 0.92,
scalar: 1.2
});
scalar: 1.2,
})
centerFire(0.1, {
spread: 120,
startVelocity: 45,
});
})
}
const centerFire = (particleRatio: number, opts: any) => {
function centerFire(particleRatio: number, opts: any) {
const count = 200
confetti({
origin: { y: 0.7 },
...opts,
particleCount: Math.floor(count * particleRatio)
});
particleCount: Math.floor(count * particleRatio),
})
}
const setDefaultPersonList = () => {
function setDefaultPersonList() {
personConfig.setDefaultPersonList()
// 刷新页面
window.location.reload()
}
// 随机替换数据
const randomBallData = (mod: 'default' | 'lucky' | 'sphere' = 'default') => {
function randomBallData(mod: 'default' | 'lucky' | 'sphere' = 'default') {
// 两秒执行一次
intervalTimer.value = setInterval(() => {
// 产生随机数数组
@@ -589,14 +575,14 @@ const randomBallData = (mod: 'default' | 'lucky' | 'sphere' = 'default') => {
}
for (let i = 0; i < cardRandomIndexArr.length; i++) {
if (!objects.value[cardRandomIndexArr[i]]) {
continue;
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)
}
}, 200)
}
// 监听键盘
const listenKeyboard = () => {
function listenKeyboard() {
window.addEventListener('keydown', (e: any) => {
if ((e.keyCode !== 32 || e.keyCode !== 27) && !canOperate.value) {
return
@@ -610,112 +596,118 @@ const listenKeyboard = () => {
switch (currentStatus.value) {
case 0:
enterLottery()
break;
break
case 1:
startLottery()
break;
break
case 2:
stopLottery()
break;
break
case 3:
continueLottery()
break;
break
default:
break;
break
}
})
}
onMounted(() => {
initTableData();
init();
animation();
initTableData()
init()
animation()
containerRef.value!.style.color = `${textColor}`
randomBallData()
listenKeyboard()
});
})
onUnmounted(() => {
clearInterval(intervalTimer.value)
intervalTimer.value = null
window.removeEventListener('keydown', listenKeyboard)
})
// watch(() => currentPrize.value.isUsed, (val) => {
// if (val) {
// currentPrize.value = JSON.parse(JSON.stringify(currentPrize.value))
// }
// })
</script>
<template>
<div class="absolute z-10 flex flex-col items-center justify-center -translate-x-1/2 left-1/2">
<h2 class="pt-12 m-0 mb-12 font-mono tracking-wide text-center leading-12 header-title"
:style="{ fontSize: textSize * 1.5 + 'px', color: textColor }">{{ topTitle }}</h2>
<h2
class="pt-12 m-0 mb-12 font-mono tracking-wide text-center leading-12 header-title"
:style="{ fontSize: `${textSize * 1.5}px`, color: textColor }"
>
{{ topTitle }}
</h2>
<div class="flex gap-3">
<button v-if="tableData.length <= 0" class="cursor-pointer btn btn-outline btn-secondary btn-lg"
@click="router.push('config')">暂无人员信息前往导入</button>
<button v-if="tableData.length <= 0" class="cursor-pointer btn btn-outline btn-secondary btn-lg"
@click="setDefaultPersonList">使用默认数据</button>
<button
v-if="tableData.length <= 0" class="cursor-pointer btn btn-outline btn-secondary btn-lg"
@click="router.push('config')"
>
{{ t('button.noInfoAndImport') }}
</button>
<button
v-if="tableData.length <= 0" class="cursor-pointer btn btn-outline btn-secondary btn-lg"
@click="setDefaultPersonList"
>
{{ t('button.useDefault') }}
</button>
</div>
</div>
<div id="container" ref="containerRef" class="3dContainer">
<!-- 选中菜单结构 start-->
<!-- 选中菜单结构 start -->
<div id="menu">
<button class="btn-end " @click="enterLottery"
v-if="currentStatus == 0 && tableData.length > 0">进入抽奖</button>
<button v-if="currentStatus === 0 && tableData.length > 0" class="btn-end " @click="enterLottery">
{{ t('button.enterLottery') }}
</button>
<div class="start" v-if="currentStatus == 1">
<button class="btn-start" @click="startLottery"><strong>开始</strong>
<div v-if="currentStatus === 1" class="start">
<button class="btn-start" @click="startLottery">
<strong>{{ t('button.start') }}</strong>
<div id="container-stars">
<div id="stars"></div>
<div id="stars" />
</div>
<div id="glow">
<div class="circle"></div>
<div class="circle"></div>
<div class="circle" />
<div class="circle" />
</div>
</button>
</div>
<button class="btn-end btn glass btn-lg" @click="stopLottery" v-if="currentStatus == 2">抽取幸运儿</button>
<button v-if="currentStatus === 2" class="btn-end btn glass btn-lg" @click="stopLottery">
{{ t('button.selectLucky') }}
</button>
<div v-if="currentStatus == 3" class="flex justify-center gap-6 enStop">
<div v-if="currentStatus === 3" class="flex justify-center gap-6 enStop">
<div class="start">
<button class="btn-start" @click="continueLottery"><strong>继续</strong>
<button class="btn-start" @click="continueLottery">
<strong>{{ t('button.continue') }}</strong>
<div id="container-stars">
<div id="stars"></div>
<div id="stars" />
</div>
<div id="glow">
<div class="circle"></div>
<div class="circle"></div>
<div class="circle" />
<div class="circle" />
</div>
</button>
</div>
<div class="start">
<button class="btn-cancel" @click="quitLottery"><strong>取消</strong>
<button class="btn-cancel" @click="quitLottery">
<strong>{{ t('button.cancel') }}</strong>
<div id="container-stars">
<div id="stars"></div>
<div id="stars" />
</div>
<div id="glow">
<div class="circle"></div>
<div class="circle"></div>
<div class="circle" />
<div class="circle" />
</div>
</button>
</div>
</div>
<!-- <button id="table" @click="transform(targets.table, 2000)">TABLE</button> -->
<!-- <button id="helix" @click="transform(targets.helix, 2000)">HELIX</button> -->
</div>
<!-- end -->
</div>
<StarsBackground :home-background="homeBackground"></StarsBackground>
<!-- <LuckyView :luckyPersonList="luckyTargets" ref="LuckyViewRef"></LuckyView> -->
<!-- <PlayMusic class="absolute right-0 bottom-1/2"></PlayMusic> -->
<PrizeList class="absolute left-0 top-32"></PrizeList>
<StarsBackground :home-background="homeBackground" />
<PrizeList class="absolute left-0 top-32" />
</template>
<style scoped lang="scss">

7
src/vite-env.d.ts vendored
View File

@@ -1,9 +1,10 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, any>
export default component
}
declare module 'sparticles'

View File

@@ -19,6 +19,6 @@
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"include": ["src/**/*.ts","src/**/*.d.ts","src/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -1,29 +1,36 @@
/// <reference types="vitest" />
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import Icons from 'unplugin-icons/vite';
import IconsResolver from 'unplugin-icons/resolver';
import { visualizer } from 'rollup-plugin-visualizer';
import viteCompression from 'vite-plugin-compression';
import legacy from '@vitejs/plugin-legacy';
// import vueDevTools from 'vite-plugin-vue-devtools'
import { createRequire } from 'node:module'
import path from 'node:path'
import legacy from '@vitejs/plugin-legacy'
import vue from '@vitejs/plugin-vue'
import { visualizer } from 'rollup-plugin-visualizer'
import AutoImport from 'unplugin-auto-import/vite'
import IconsResolver from 'unplugin-icons/resolver'
import Icons from 'unplugin-icons/vite'
import Components from 'unplugin-vue-components/vite'
import { defineConfig, loadEnv } from 'vite'
import viteCompression from 'vite-plugin-compression'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vitejs.dev/config/
const require = createRequire(import.meta.url)
const process = require('node:process')
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, __dirname);
const chunkName = mode == 'prebuild' ? '[name]' : 'chunk';
const env = loadEnv(mode, __dirname)
const chunkName = mode === 'prebuild' ? '[name]' : 'chunk'
return {
base:mode == 'file'?'./':'/log-lottery/',
base: mode === 'file' ? './' : '/log-lottery/',
plugins: [
vue(),
mode == 'file'?legacy({
additionalLegacyPolyfills: ['regenerator-runtime/runtime']
}):null,
mode === 'file'
? legacy({
additionalLegacyPolyfills: ['regenerator-runtime/runtime'],
})
: null,
// vueDevTools(),
viteCompression({
verbose: true,
@@ -33,11 +40,11 @@ export default defineConfig(({ mode }) => {
ext: '.gz',
}),
visualizer({
emitFile: true, //是否被触摸
filename: 'test.html', //生成分析网页文件名
open: true, //在默认用户代理中打开生成的文件
gzipSize: true, //从源代码中收集 gzip 大小并将其显示在图表中
brotliSize: true, //从源代码中收集 brotli 大小并将其显示在图表中
emitFile: true, // 是否被触摸
filename: 'test.html', // 生成分析网页文件名
open: true, // 在默认用户代理中打开生成的文件
gzipSize: true, // 从源代码中收集 gzip 大小并将其显示在图表中
brotliSize: true, // 从源代码中收集 brotli 大小并将其显示在图表中
}),
createSvgIconsPlugin({
@@ -90,7 +97,7 @@ export default defineConfig(({ mode }) => {
// 是否跨域
changeOrigin: true,
// 路径重写
rewrite: (path) => path.replace(/^\/api/, ''),
rewrite: path => path.replace(/^\/api/, ''),
},
},
},
@@ -103,7 +110,7 @@ export default defineConfig(({ mode }) => {
minify: 'terser',
terserOptions: {
compress: {
//生产环境时移除console
// 生产环境时移除console
drop_console: true,
drop_debugger: true,
},
@@ -123,7 +130,7 @@ export default defineConfig(({ mode }) => {
.toString()
.split('node_modules/')[1]
.split('/')[0]
.toString();
.toString()
}
},
},
@@ -139,5 +146,5 @@ export default defineConfig(({ mode }) => {
web: [/\.[jt]sx$/],
},
},
};
});
}
})