Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions server/.env
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
XIAOJU_SURVEY_MONGO_URL=
XIAOJU_SURVEY_MONGO_DB_NAME=xiaoju
XIAOJU_SURVEY_MONGO_URL=mongodb+srv://yahyatruth:ZcYYjxgtfkg5fyNg@cluster0.corddh7.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=

XIAOJU_SURVEY_REDIS_HOST=
Expand Down
5 changes: 2 additions & 3 deletions server/.env.development
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
XIAOJU_SURVEY_MONGO_URL=mongodb://127.0.0.1:27017

XIAOJU_SURVEY_MONGO_DB_NAME=xiaoju
XIAOJU_SURVEY_MONGO_URL=mongodb+srv://yahyatruth:ZcYYjxgtfkg5fyNg@cluster0.corddh7.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=

XIAOJU_SURVEY_RESPONSE_AES_ENCRYPT_SECRET_KEY=dataAesEncryptSecretKey
Expand Down
4 changes: 2 additions & 2 deletions server/.env.production
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
XIAOJU_SURVEY_MONGO_DB_NAME=xiaojuSurvey
XIAOJU_SURVEY_MONGO_URL=
XIAOJU_SURVEY_MONGO_DB_NAME=xiaoju
XIAOJU_SURVEY_MONGO_URL=mongodb+srv://yahyatruth:ZcYYjxgtfkg5fyNg@cluster0.corddh7.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
XIAOJU_SURVEY_MONGO_AUTH_SOURCE=

XIAOJU_SURVEY_REDIS_HOST=
Expand Down
3 changes: 3 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"qrcode": "^1.5.3",
"uuid": "^10.0.0",
"vue": "^3.4.15",
"vue-i18n": "^9.14.5",
"vue-router": "^4.2.5",
"vuedraggable": "^4.1.0",
"xss": "^1.0.14",
Expand All @@ -47,6 +48,7 @@
"@types/node": "^20.11.19",
"@types/qrcode": "^1.5.5",
"@types/uuid": "^10.0.0",
"@types/vue": "^2.0.0",
"@vitejs/plugin-vue": "^5.0.3",
"@vitejs/plugin-vue-jsx": "^3.1.0",
"@vue/eslint-config-prettier": "^8.0.0",
Expand All @@ -64,6 +66,7 @@
"unplugin-icons": "^0.18.5",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.1.4",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-virtual-mpa": "^1.11.0",
"vue-tsc": "^1.8.27"
},
Expand Down
39 changes: 39 additions & 0 deletions web/public/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Service Worker for Performance Optimization
const CACHE_NAME = 'xiaoju-survey-v1'
const urlsToCache = ['/', '/imgs/Logo.webp', '/imgs/avatar.webp', '/imgs/favicon.ico']

// Install event - cache resources
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(urlsToCache)
})
)
self.skipWaiting()
})

// Fetch event - serve from cache, fallback to network
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
// Return cached version or fetch from network
return response || fetch(event.request)
})
)
})

// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName)
}
})
)
})
)
self.clients.claim()
})
127 changes: 127 additions & 0 deletions web/src/common/components/LanguageSelector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<template>
<el-dropdown trigger="click" @command="handleCommand" class="language-selector" aria-label="Language selector"
role="listbox">
<span class="el-dropdown-link selector-btn" tabindex="0" aria-haspopup="listbox" aria-expanded="false">
<el-icon class="globe-icon" aria-hidden="true">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="9" stroke="currentColor" stroke-width="2" />
<path
d="M10 1C13.866 1 17 4.13401 17 8C17 11.866 13.866 15 10 15C6.13401 15 3 11.866 3 8C3 4.13401 6.13401 1 10 1Z"
stroke="currentColor" stroke-width="1.5" />
</svg>
</el-icon>
<span class="flag" aria-hidden="true">{{ currentLanguage.flag }}</span>
<span class="name">{{ currentLanguage.name }}</span>
<el-icon class="el-icon--right" aria-hidden="true">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="lang in supportedLocales" :key="lang.code" :command="lang.code"
:class="{ 'is-active': currentLocale === lang.code }" role="option"
:aria-selected="currentLocale === lang.code">
<span class="flag" aria-hidden="true">{{ lang.flag }}</span>
<span class="name">{{ lang.name }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>

<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { ArrowDown } from '@element-plus/icons-vue'
import { supportedLocales, switchLanguage } from '@/common/locales'

const { locale } = useI18n()

const currentLocale = computed(() => locale.value)

const currentLanguage = computed(() => {
return supportedLocales.find(lang => lang.code === currentLocale.value) || supportedLocales[0]
})

const handleCommand = (command: string) => {
if (command !== currentLocale.value) {
switchLanguage(command)
// 重新加载页面以确保所有组件都更新
// window.location.reload()
}
}
</script>


<style scoped lang="scss">
.language-selector {
cursor: pointer;

.selector-btn {
display: flex;
align-items: center;
gap: 8px;
background: var(--el-bg-color);
border-radius: 24px;
padding: 6px 16px;
color: var(--el-text-color-regular);
font-size: 15px;
font-weight: 500;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
transition: box-shadow 0.2s, background 0.2s, color 0.2s;
outline: none;
border: 2px solid transparent;

&:hover,
&:focus {
color: var(--el-color-primary);
background: var(--el-color-info-light-9);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
border-color: var(--el-color-primary);
}

&:focus {
outline: 2px solid var(--el-color-primary);
}

.globe-icon {
margin-right: 4px;
color: var(--el-color-primary);
vertical-align: middle;
}

.flag {
font-size: 18px;
margin-right: 4px;
}

.name {
margin-right: 2px;
}
}
}

:deep(.el-dropdown-menu__item) {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
padding: 8px 18px;

.flag {
font-size: 18px;
margin-right: 6px;
}

&.is-active {
color: var(--el-color-primary);
font-weight: 600;
background: var(--el-color-info-light-8);
border-radius: 8px;
}

&:focus {
outline: 2px solid var(--el-color-primary);
}
}
</style>
12 changes: 12 additions & 0 deletions web/src/common/components/LocaleProvider.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<el-config-provider :locale="elementLocale">
<slot />
</el-config-provider>
</template>

<script setup lang="ts">
import { ElConfigProvider } from 'element-plus'
import { useElementPlusLocale } from '@/common/composables/useElementLocale'

const { elementLocale } = useElementPlusLocale()
</script>
80 changes: 80 additions & 0 deletions web/src/common/composables/useAppI18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useI18n } from 'vue-i18n'
import { computed } from 'vue'

/**
* Custom composable to use i18n with better typing and easier access
*/
export function useAppI18n() {
const { t, locale, availableLocales } = useI18n()

// Current locale info
const currentLocale = computed(() => locale.value)

// Translation function with better error handling
const translate = (key: string, values?: Record<string, any>, fallback?: string) => {
try {
const translated = values ? t(key, values) : t(key)
return translated !== key ? translated : fallback || key
} catch (error) {
console.warn(`Translation key "${key}" not found`)
return fallback || key
}
}

// Common translations shortcuts
const common = {
save: () => translate('common.save'),
cancel: () => translate('common.cancel'),
confirm: () => translate('common.confirm'),
delete: () => translate('common.delete'),
edit: () => translate('common.edit'),
add: () => translate('common.add'),
create: () => translate('common.create'),
submit: () => translate('common.submit'),
loading: () => translate('common.loading'),
success: () => translate('common.success'),
error: () => translate('common.error'),
preview: () => translate('common.preview'),
publish: () => translate('common.publish'),
back: () => translate('common.back'),
next: () => translate('common.next'),
close: () => translate('common.close')
}

// Survey specific translations
const survey = {
title: () => translate('editor.surveyTitle'),
description: () => translate('editor.surveyDesc'),
addQuestion: () => translate('editor.addQuestion'),
deleteQuestion: () => translate('editor.deleteQuestion'),
settings: () => translate('editor.settings')
}

// Error messages
const error = {
networkError: () => translate('error.networkError'),
serverError: () => translate('error.serverError'),
permissionDenied: () => translate('error.permissionDenied'),
unknown: () => translate('error.unknown'),
saveFailed: () => translate('error.saveFailed')
}

// Success messages
const success = {
saved: () => translate('success.saved'),
deleted: () => translate('success.deleted'),
created: () => translate('success.created'),
updated: () => translate('success.updated'),
published: () => translate('success.published')
}

return {
t: translate,
locale: currentLocale,
availableLocales,
common,
survey,
error,
success
}
}
24 changes: 24 additions & 0 deletions web/src/common/composables/useElementLocale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'

// Element Plus locale imports
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en'

const elementLocales = {
'zh-CN': zhCn,
'en-US': en,
'id-ID': en // Use English as fallback for Indonesian
}

export function useElementPlusLocale() {
const { locale } = useI18n()

const elementLocale = computed(() => {
return elementLocales[locale.value as keyof typeof elementLocales] || en
})

return {
elementLocale
}
}
Loading