Feature action (#150)

* ci: 👷 整合github action配置文件

* docs: 📝 贡献文档修改

* style: 💄 更新版本

* style: 💄 cargo.lock版本更新

* feat(husky): 增强Git标签版本校验脚本

添加了对Git标签指向提交与release分支一致性的校验功能。
脚本现在会检查tag指向的提交是否与当前或任何release分支的最新提交一致,
确保发布流程的准确性。如果当前在release分支上,直接比较分支HEAD与tag指向的提交;
如果不在release分支上,则遍历所有release分支查找匹配的提交。

* feat:  国际化

* fix: 🐛 修复国际化问题;修复字体大小未生效问题

* fix: 🐛 修复部分问题

* docs: 📝 update readme
This commit is contained in:
LOG1997
2025-12-30 17:30:46 +08:00
committed by GitHub
parent 80aacffe07
commit ed98307da4
10 changed files with 251 additions and 224 deletions

View File

@@ -41,7 +41,6 @@ jobs:
- name: Prepare directories - name: Prepare directories
run: | run: |
mv dist dist-prod mv dist dist-prod
mv dist-prod/index.html dist-prod/404.html
- name: Create production build archive - name: Create production build archive
run: | run: |
@@ -63,6 +62,16 @@ jobs:
name: github-pages name: github-pages
path: dist-gh-pages path: dist-gh-pages
- name: Prepare GitHub Pages artifact
run: |
cp -r dist-prod dist-gh-pages
- name: Upload GitHub Pages artifact
uses: actions/upload-artifact@v4
with:
name: github-pages
path: dist-gh-pages
publish-tauri: publish-tauri:
permissions: permissions:
contents: write contents: write

View File

@@ -16,7 +16,6 @@
log-lottery是一个可配置可定制化的抽奖应用炫酷3D球体可用于年会抽奖等活动支持奖品、人员、界面、图片音乐配置。 log-lottery是一个可配置可定制化的抽奖应用炫酷3D球体可用于年会抽奖等活动支持奖品、人员、界面、图片音乐配置。
> 如果进入网站遇到图片无法显示或有报错的情况,请先到【全局配置】-【界面配置】菜单中点击【重置所有数据】按钮清除数据后进行更新。 > 如果进入网站遇到图片无法显示或有报错的情况,请先到【全局配置】-【界面配置】菜单中点击【重置所有数据】按钮清除数据后进行更新。
> 该项目将在近期进行**内部代码重构**及**开发新功能**,预计元旦节前三天上线新版本。
## 要求 ## 要求
@@ -24,12 +23,15 @@ log-lottery是一个可配置可定制化的抽奖应用炫酷3D球体
访问地址: 访问地址:
<https://to2026.xyz/log-lottery> <https://lottery.to2026.xyz/log-lottery>
or or
<https://log1997.github.io/log-lottery/> <https://log1997.github.io/log-lottery/>
开发仓促若以上网站内容存在bug还请宽容。
如果想要访问2025年12月31日前的版本请前往<https://lottery.to2026.xyz/log-lottery>
## TODO ## TODO
- [x] 🕍 炫酷3D球体年会抽奖必备开箱即用 - [x] 🕍 炫酷3D球体年会抽奖必备开箱即用

View File

@@ -47,6 +47,9 @@ export const tableEn = {
timedStop: 'Timed Stop', timedStop: 'Timed Stop',
playWinMusic: 'Play Win Music', playWinMusic: 'Play Win Music',
resetAllData: 'Reset All Data', resetAllData: 'Reset All Data',
globalFont: 'Global Font',
titleFont: 'Title Font',
syncGlobalFont: 'Sync Global Font',
} }
export const tableZhCn = { export const tableZhCn = {
@@ -95,7 +98,9 @@ export const tableZhCn = {
timedStop: '定时停止', timedStop: '定时停止',
playWinMusic: '播放中奖音乐', playWinMusic: '播放中奖音乐',
resetAllData: '重置数据', resetAllData: '重置数据',
globalFont: '全局字体',
titleFont: '标题字体',
syncGlobalFont: '同步全局字体',
} }
export const table = { export const table = {

View File

@@ -19,7 +19,7 @@ const uploadVisible = ref(false)
<template> <template>
<fieldset class="p-4 border text-setting fieldset bg-base-200 border-base-300 rounded-box w-xs pb-10"> <fieldset class="p-4 border text-setting fieldset bg-base-200 border-base-300 rounded-box w-xs pb-10">
<legend class="fieldset-legend"> <legend class="fieldset-legend">
{{ t('table.DataSetting') }} {{ t('table.dataSetting') }}
</legend> </legend>
<dialog id="my_modal_1" ref="resetDataDialogRef" class="border-none modal"> <dialog id="my_modal_1" ref="resetDataDialogRef" class="border-none modal">
<div class="modal-box"> <div class="modal-box">

View File

@@ -46,20 +46,20 @@ const titleFontSyncGlobalValue = defineModel<boolean>('titleFontSyncGlobalValue'
</label> </label>
<label class="w-full max-w-xs form-control mt-3"> <label class="w-full max-w-xs form-control mt-3">
<div class="label"> <div class="label">
<span class="label-text">全局字体</span> <span class="label-text">{{ t('table.globalFont') }}</span>
</div> </div>
<SelectFont v-model:selected-font="currentFontValue" /> <SelectFont v-model:selected-font="currentFontValue" />
</label> </label>
<label class="flex flex-row w-full max-w-xs mt-5 gap-10 form-control"> <label class="flex flex-row w-full max-w-xs mt-5 gap-10 form-control">
<div class="w-3/4"> <div class="w-3/4">
<div class="label"> <div class="label">
<span class="label-text">标题字体</span> <span class="label-text">{{ t('table.titleFont') }}</span>
</div> </div>
<SelectFont v-model:selected-font="currentTitleFontValue" :disabled="titleFontSyncGlobalValue" /> <SelectFont v-model:selected-font="currentTitleFontValue" :disabled="titleFontSyncGlobalValue" />
</div> </div>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="label"> <div class="label">
<span class="label-text">同全局</span> <span class="label-text">{{ t('table.syncGlobalFont') }}</span>
</div> </div>
<input type="checkbox" :checked="titleFontSyncGlobalValue" class="border-solid checkbox checkbox-secondary border" @change="titleFontSyncGlobalValue = !titleFontSyncGlobalValue"> <input type="checkbox" :checked="titleFontSyncGlobalValue" class="border-solid checkbox checkbox-secondary border" @change="titleFontSyncGlobalValue = !titleFontSyncGlobalValue">
</div> </div>

View File

@@ -7,232 +7,235 @@ import { themeChange } from '@/utils'
import { clearAllDbStore } from '@/utils/localforage' import { clearAllDbStore } from '@/utils/localforage'
export function useViewModel() { export function useViewModel() {
type ValidatePayload = zod.infer<typeof schema> type ValidatePayload = zod.infer<typeof schema>
const globalConfig = useStore().globalConfig const globalConfig = useStore().globalConfig
const personConfig = useStore().personConfig const personConfig = useStore().personConfig
const prizeConfig = useStore().prizeConfig const prizeConfig = useStore().prizeConfig
const { const {
getGlobalConfig: globalConfigData, getGlobalConfig: globalConfigData,
getTopTitle: topTitle, getTopTitle: topTitle,
getTheme: localTheme, getTheme: localTheme,
getPatterColor: patternColor, getPatterColor: patternColor,
getPatternList: patternList, getPatternList: patternList,
getCardColor: cardColor, getCardColor: cardColor,
getLuckyColor: luckyCardColor, getLuckyColor: luckyCardColor,
getTextColor: textColor, getTextColor: textColor,
getCardSize: cardSize, getCardSize: cardSize,
getTextSize: textSize, getTextSize: textSize,
getRowCount: rowCount, getRowCount: rowCount,
getIsShowPrizeList: isShowPrizeList, getIsShowPrizeList: isShowPrizeList,
getLanguage: userLanguage, getLanguage: userLanguage,
getBackground: backgroundImage, getBackground: backgroundImage,
getFont: currentFont, getFont: currentFont,
getTitleFont: currentTitleFont, getTitleFont: currentTitleFont,
getTitleFontSyncGlobal: titleFontSyncGlobal, getTitleFontSyncGlobal: titleFontSyncGlobal,
getImageList: imageList, getImageList: imageList,
getIsShowAvatar: isShowAvatar, getIsShowAvatar: isShowAvatar,
getDefiniteTime: definiteTime, getDefiniteTime: definiteTime,
getWinMusic: isWinMusic, getWinMusic: isWinMusic,
} = storeToRefs(globalConfig) } = storeToRefs(globalConfig)
const { getAlreadyPersonList: alreadyPersonList, getNotPersonList: notPersonList } = storeToRefs(personConfig) const { getAlreadyPersonList: alreadyPersonList, getNotPersonList: notPersonList } = storeToRefs(personConfig)
const isRowCountChange = ref(0) // 0未改变1改变,2加载中 const isRowCountChange = ref(0) // 0未改变1改变,2加载中
const themeValue = ref(localTheme.value.name) const themeValue = ref(localTheme.value.name)
const topTitleValue = ref(structuredClone(topTitle.value)) const topTitleValue = ref(structuredClone(topTitle.value))
const cardColorValue = ref(structuredClone(cardColor.value)) const cardColorValue = ref(structuredClone(cardColor.value))
const luckyCardColorValue = ref(structuredClone(luckyCardColor.value)) const luckyCardColorValue = ref(structuredClone(luckyCardColor.value))
const textColorValue = ref(structuredClone(textColor.value)) const textColorValue = ref(structuredClone(textColor.value))
const cardSizeValue = ref(structuredClone(cardSize.value)) const cardSizeValue = ref(structuredClone(cardSize.value))
const textSizeValue = ref(structuredClone(textSize.value)) const textSizeValue = ref(structuredClone(textSize.value))
const rowCountValue = ref(structuredClone(rowCount.value)) const rowCountValue = ref(structuredClone(rowCount.value))
const languageValue = ref(structuredClone(userLanguage.value)) const languageValue = ref(structuredClone(userLanguage.value))
const isShowPrizeListValue = ref(structuredClone(isShowPrizeList.value)) const isShowPrizeListValue = ref(structuredClone(isShowPrizeList.value))
const isShowAvatarValue = ref(structuredClone(isShowAvatar.value)) const isShowAvatarValue = ref(structuredClone(isShowAvatar.value))
const patternColorValue = ref(structuredClone(patternColor.value)) const patternColorValue = ref(structuredClone(patternColor.value))
const backgroundImageValue = ref(backgroundImage.value) const backgroundImageValue = ref(backgroundImage.value)
const currentFontValue = ref(structuredClone(currentFont.value)) const currentFontValue = ref(structuredClone(currentFont.value))
const currentTitleFontValue = ref(structuredClone(currentTitleFont.value)) const currentTitleFontValue = ref(structuredClone(currentTitleFont.value))
const titleFontSyncGlobalValue = ref(structuredClone(titleFontSyncGlobal.value)) const titleFontSyncGlobalValue = ref(structuredClone(titleFontSyncGlobal.value))
const definiteTimeValue = ref(structuredClone(definiteTime.value)) const definiteTimeValue = ref(structuredClone(definiteTime.value))
const isWinMusicValue = ref(structuredClone(isWinMusic.value)) const isWinMusicValue = ref(structuredClone(isWinMusic.value))
const formData = ref({ const formData = ref({
rowCount: rowCountValue, rowCount: rowCountValue,
})
const formErr = ref({
rowCount: '',
})
const schema = zod.object({
rowCount: zod.number({
error: i18n.global.t('error.require'),
// required_error: i18n.global.t('error.require'),
// invalid_type_error: i18n.global.t('error.requireNumber'),
}) })
const formErr = ref({ .min(1, i18n.global.t('error.minNumber1'))
rowCount: '', .max(100, i18n.global.t('error.maxNumber100')),
}) // 格式化
const schema = zod.object({
rowCount: zod.number({
error: i18n.global.t('error.require'),
// required_error: i18n.global.t('error.require'),
// invalid_type_error: i18n.global.t('error.requireNumber'),
})
.min(1, i18n.global.t('error.minNumber1'))
.max(100, i18n.global.t('error.maxNumber100')),
// 格式化
}) })
const payload: ValidatePayload = { const payload: ValidatePayload = {
rowCount: formData.value.rowCount, rowCount: formData.value.rowCount,
} }
function parseSchema(props: ValidatePayload) { function parseSchema(props: ValidatePayload) {
return schema.parseAsync(props) return schema.parseAsync(props)
} }
function resetPersonLayout() { function resetPersonLayout() {
isRowCountChange.value = 2 isRowCountChange.value = 2
setTimeout(() => { setTimeout(() => {
const alreadyLen = alreadyPersonList.value.length const alreadyLen = alreadyPersonList.value.length
const notLen = notPersonList.value.length const notLen = notPersonList.value.length
if (alreadyLen <= 0 && notLen <= 0) { if (alreadyLen <= 0 && notLen <= 0) {
return return
} }
const allPersonList = alreadyPersonList.value.concat(notPersonList.value) const allPersonList = alreadyPersonList.value.concat(notPersonList.value)
const newAlreadyPersonList = allPersonList.slice(0, alreadyLen) const newAlreadyPersonList = allPersonList.slice(0, alreadyLen)
const newNotPersonList = allPersonList.slice(alreadyLen, notLen + alreadyLen) const newNotPersonList = allPersonList.slice(alreadyLen, notLen + alreadyLen)
personConfig.deleteAllPerson() personConfig.deleteAllPerson()
personConfig.addNotPersonList(newNotPersonList) personConfig.addNotPersonList(newNotPersonList)
personConfig.addAlreadyPersonList(newAlreadyPersonList, null) personConfig.addAlreadyPersonList(newAlreadyPersonList, null)
isRowCountChange.value = 0 isRowCountChange.value = 0
}, 1000) }, 1000)
} }
function clearPattern() { function clearPattern() {
globalConfig.setPatternList([] as number[]) globalConfig.setPatternList([] as number[])
} }
function resetPattern() { function resetPattern() {
globalConfig.resetPatternList() globalConfig.resetPatternList()
} }
function resetData() { function resetData() {
globalConfig.reset() globalConfig.reset()
personConfig.reset() personConfig.reset()
prizeConfig.resetDefault() prizeConfig.resetDefault()
// 删除所有indexDb // 删除所有indexDb
clearAllDbStore() clearAllDbStore()
// 刷新页面 // 刷新页面
window.location.reload() window.location.reload()
} }
function exportAllConfigData() { function exportAllConfigData() {
// const globalConfigData = globalConfig.getGlobalConfig() // const globalConfigData = globalConfig.getGlobalConfig()
// console.log(globalConfigData.value) // console.log(globalConfigData.value)
// const globalConfigData = globalConfig.getGlobalConfig() // const globalConfigData = globalConfig.getGlobalConfig()
const dataStr = JSON.stringify(globalConfigData.value, null, 2) const dataStr = JSON.stringify(globalConfigData.value, null, 2)
const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}` const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`
const exportFileDefaultName = 'global-config.json' const exportFileDefaultName = 'global-config.json'
const linkElement = document.createElement('a') const linkElement = document.createElement('a')
linkElement.setAttribute('href', dataUri) linkElement.setAttribute('href', dataUri)
linkElement.setAttribute('download', exportFileDefaultName) linkElement.setAttribute('download', exportFileDefaultName)
linkElement.click() linkElement.click()
} }
function importAllConfigData(data: any) { function importAllConfigData(data: any) {
globalConfig.setGlobalConfig(data) globalConfig.setGlobalConfig(data)
window.location.reload() window.location.reload()
} }
watch(() => formData.value.rowCount, () => { watch(() => formData.value.rowCount, () => {
payload.rowCount = formData.value.rowCount payload.rowCount = formData.value.rowCount
parseSchema(payload).then((res) => { parseSchema(payload).then((res) => {
if (res.rowCount) { if (res.rowCount) {
isRowCountChange.value = 1 isRowCountChange.value = 1
globalConfig.setRowCount(res.rowCount) globalConfig.setRowCount(res.rowCount)
} }
}).catch((err) => { }).catch((err) => {
formErr.value.rowCount = err.issues[0].message formErr.value.rowCount = err.issues[0].message
})
}) })
})
watch(topTitleValue, (val) => { watch(topTitleValue, (val) => {
globalConfig.setTopTitle(val) globalConfig.setTopTitle(val)
}) })
watch(themeValue, (val: any) => { watch(themeValue, (val: any) => {
globalConfig.setTheme({ name: val }) globalConfig.setTheme({ name: val })
themeChange(val) themeChange(val)
}, { deep: true }) }, { deep: true })
watch(cardColorValue, (val: string) => { watch(cardColorValue, (val: string) => {
globalConfig.setCardColor(val) globalConfig.setCardColor(val)
}, { deep: true }) }, { deep: true })
watch(luckyCardColorValue, (val: string) => { watch(luckyCardColorValue, (val: string) => {
globalConfig.setLuckyCardColor(val) globalConfig.setLuckyCardColor(val)
}, { deep: true }) }, { deep: true })
watch(patternColorValue, (val: string) => { watch(patternColorValue, (val: string) => {
globalConfig.setPatterColor(val) globalConfig.setPatterColor(val)
}) })
watch(textColorValue, (val: string) => { watch(textColorValue, (val: string) => {
globalConfig.setTextColor(val) globalConfig.setTextColor(val)
}, { deep: true }) }, { deep: true })
watch(cardSizeValue, (val: { width: number, height: number }) => { watch(cardSizeValue, (val: { width: number, height: number }) => {
globalConfig.setCardSize(val) globalConfig.setCardSize(val)
}, { deep: true }) }, { deep: true })
watch(isShowPrizeListValue, () => { watch(isShowPrizeListValue, () => {
globalConfig.setIsShowPrizeList(isShowPrizeListValue.value) globalConfig.setIsShowPrizeList(isShowPrizeListValue.value)
}) })
watch(backgroundImageValue, (val) => { watch(backgroundImageValue, (val) => {
globalConfig.setBackground(val) globalConfig.setBackground(val)
}) })
watch(currentFontValue, (val) => { watch(currentFontValue, (val) => {
globalConfig.setFont(val) globalConfig.setFont(val)
document.documentElement.style.setProperty('--app-font-family', `"${val}", -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`) document.documentElement.style.setProperty('--app-font-family', `"${val}", -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif`)
}) })
watch(currentTitleFontValue, (val) => { watch(currentTitleFontValue, (val) => {
globalConfig.setTitleFont(val) globalConfig.setTitleFont(val)
}) })
watch(titleFontSyncGlobalValue, (val) => { watch(titleFontSyncGlobalValue, (val) => {
globalConfig.setTitleFontSyncGlobal(val) globalConfig.setTitleFontSyncGlobal(val)
}) })
watch(languageValue, (val: string) => { watch(languageValue, (val: string) => {
globalConfig.setLanguage(val) globalConfig.setLanguage(val)
}) })
watch(isShowAvatarValue, () => { watch(isShowAvatarValue, () => {
globalConfig.setIsShowAvatar(isShowAvatarValue.value) globalConfig.setIsShowAvatar(isShowAvatarValue.value)
}) })
watch(definiteTimeValue, () => { watch(definiteTimeValue, () => {
globalConfig.setDefiniteTime(definiteTimeValue.value) globalConfig.setDefiniteTime(definiteTimeValue.value)
}) })
watch(isWinMusicValue, () => { watch(isWinMusicValue, () => {
globalConfig.setIsPlayWinMusic(isWinMusicValue.value) globalConfig.setIsPlayWinMusic(isWinMusicValue.value)
}) })
onMounted(() => { watch(textSizeValue, (val: number) => {
}) globalConfig.setTextSize(val)
return { })
resetData, onMounted(() => {
topTitleValue, })
languageValue, return {
textSizeValue, resetData,
currentFontValue, topTitleValue,
currentTitleFontValue, languageValue,
titleFontSyncGlobalValue, textSizeValue,
languageList, currentFontValue,
formErr, currentTitleFontValue,
formData, titleFontSyncGlobalValue,
cardSizeValue, languageList,
isShowPrizeListValue, formErr,
isShowAvatarValue, formData,
resetPersonLayout, cardSizeValue,
isRowCountChange, isShowPrizeListValue,
themeValue, isShowAvatarValue,
backgroundImageValue, resetPersonLayout,
cardColorValue, isRowCountChange,
luckyCardColorValue, themeValue,
textColorValue, backgroundImageValue,
patternColorValue, cardColorValue,
imageList, luckyCardColorValue,
rowCount, textColorValue,
cardColor, patternColorValue,
patternColor, imageList,
patternList, rowCount,
clearPattern, cardColor,
resetPattern, patternColor,
exportAllConfigData, patternList,
importAllConfigData, clearPattern,
definiteTimeValue, resetPattern,
isWinMusicValue, exportAllConfigData,
} importAllConfigData,
definiteTimeValue,
isWinMusicValue,
}
} }

View File

@@ -40,7 +40,7 @@ watch(() => imgUploadToast.value, (val) => {
<UploadDialog v-model:visible="uploadVisible" /> <UploadDialog v-model:visible="uploadVisible" />
<div> <div>
<PageHeader title="图片管理"> <PageHeader :title="t('sidebar.imagesManagement')">
<template #buttons> <template #buttons>
<div class=""> <div class="">
<label for="explore"> <label for="explore">

View File

@@ -37,7 +37,7 @@ globalThis.onmessage = async (e: MessageEvent<WorkerMessage>) => {
globalThis.postMessage({ globalThis.postMessage({
type: 'error', type: 'error',
data: null, data: null,
message: i18n.global.t('error.excelFileError'), message: 'not right template',
}) })
return return
} }

View File

@@ -75,6 +75,14 @@ export function useViewModel({ exportInputFileRef }: { exportInputFileRef: Ref<H
clearFileInput() clearFileInput()
} }
if (e.data.type === 'error') { if (e.data.type === 'error') {
if (e.data.message === 'not right template') {
toast.open({
message: t('error.excelFileError'),
type: 'error',
position: 'top-right',
})
return
}
toast.open({ toast.open({
message: e.data.message || t('error.importFail'), message: e.data.message || t('error.importFail'),
type: 'error', type: 'error',

View File

@@ -40,13 +40,13 @@ export default defineConfig(({ mode }) => {
algorithm: 'gzip', algorithm: 'gzip',
ext: '.gz', ext: '.gz',
}), }),
visualizer({ mode === 'prebuild' ? visualizer({
emitFile: true, // 是否被触摸 emitFile: true, // 是否被触摸
filename: 'test.html', // 生成分析网页文件名 filename: 'test.html', // 生成分析网页文件名
open: true, // 在默认用户代理中打开生成的文件 open: true, // 在默认用户代理中打开生成的文件
gzipSize: true, // 从源代码中收集 gzip 大小并将其显示在图表中 gzipSize: true, // 从源代码中收集 gzip 大小并将其显示在图表中
brotliSize: true, // 从源代码中收集 brotli 大小并将其显示在图表中 brotliSize: true, // 从源代码中收集 brotli 大小并将其显示在图表中
}), }) : null,
createSvgIconsPlugin({ createSvgIconsPlugin({
// 指定需要缓存的图标文件夹 // 指定需要缓存的图标文件夹