feat(主题): 添加暗黑模式支持并改进WebView兼容性
实现暗黑模式功能,包括: 1. 在Tailwind配置中添加darkMode选项 2. 为所有主要组件添加暗黑样式 3. 创建WebView兼容性工具处理旧版本兼容问题 4. 在设置页面添加暗黑模式切换开关 5. 更新多语言文件支持暗黑模式相关文本
This commit is contained in:
+1
-1
@@ -1,2 +1,2 @@
|
||||
node_modules
|
||||
node_modules
|
||||
dist
|
||||
|
||||
Vendored
-58
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-1
@@ -1 +0,0 @@
|
||||
import{W as w}from"./index-B7lrTY2m.js";class y extends w{async getId(){return{identifier:this.getUid()}}async getInfo(){if(typeof navigator>"u"||!navigator.userAgent)throw this.unavailable("Device API not available in this browser");const e=navigator.userAgent,i=this.parseUa(e);return{model:i.model,platform:"web",operatingSystem:i.operatingSystem,osVersion:i.osVersion,manufacturer:navigator.vendor,isVirtual:!1,webViewVersion:i.browserVersion}}async getBatteryInfo(){if(typeof navigator>"u"||!navigator.getBattery)throw this.unavailable("Device API not available in this browser");let e={};try{e=await navigator.getBattery()}catch{}return{batteryLevel:e.level,isCharging:e.charging}}async getLanguageCode(){return{value:navigator.language.split("-")[0].toLowerCase()}}async getLanguageTag(){return{value:navigator.language}}parseUa(e){const i={},r=e.indexOf("(")+1;let a=e.indexOf(") AppleWebKit");e.indexOf(") Gecko")!==-1&&(a=e.indexOf(") Gecko"));const s=e.substring(r,a);if(e.indexOf("Android")!==-1){const t=s.replace("; wv","").split("; ").pop();t&&(i.model=t.split(" Build")[0]),i.osVersion=s.split("; ")[1]}else if(i.model=s.split("; ")[0],typeof navigator<"u"&&navigator.oscpu)i.osVersion=navigator.oscpu;else if(e.indexOf("Windows")!==-1)i.osVersion=s;else{const t=s.split("; ").pop();if(t){const n=t.replace(" like Mac OS X","").split(" ");i.osVersion=n[n.length-1].replace(/_/g,".")}}/android/i.test(e)?i.operatingSystem="android":/iPad|iPhone|iPod/.test(e)&&!window.MSStream?i.operatingSystem="ios":/Win/.test(e)?i.operatingSystem="windows":/Mac/i.test(e)?i.operatingSystem="mac":i.operatingSystem="unknown";const l=!!window.ApplePaySession,x=!!window.chrome,p=/Firefox/.test(e),d=/Edg/.test(e),g=/FxiOS/.test(e),c=/CriOS/.test(e),f=/EdgiOS/.test(e);if(l||x&&!d||g||c||f){let t;g?t="FxiOS":c?t="CriOS":f?t="EdgiOS":l?t="Version":t="Chrome";const n=e.split(" ");for(const o of n)if(o.includes(t)){const v=o.split("/")[1];i.browserVersion=v}}else if(p||d){const o=e.split("").reverse().join("").split("/")[0].split("").reverse().join("");i.browserVersion=o}return i}getUid(){if(typeof window<"u"&&window.localStorage){let e=window.localStorage.getItem("_capuid");return e||(e=this.uuid4(),window.localStorage.setItem("_capuid",e),e)}return this.uuid4()}uuid4(){return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){const i=Math.random()*16|0;return(e==="x"?i:i&3|8).toString(16)})}}export{y as DeviceWeb};
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
import{W as p}from"./index-B7lrTY2m.js";class f extends p{constructor(){super(...arguments),this.group="CapacitorStorage"}async configure({group:e}){typeof e=="string"&&(this.group=e)}async get(e){return{value:this.impl.getItem(this.applyPrefix(e.key))}}async set(e){this.impl.setItem(this.applyPrefix(e.key),e.value)}async remove(e){this.impl.removeItem(this.applyPrefix(e.key))}async keys(){return{keys:this.rawKeys().map(t=>t.substring(this.prefix.length))}}async clear(){for(const e of this.rawKeys())this.impl.removeItem(e)}async migrate(){var e;const t=[],s=[],n="_cap_",o=Object.keys(this.impl).filter(i=>i.indexOf(n)===0);for(const i of o){const r=i.substring(n.length),a=(e=this.impl.getItem(i))!==null&&e!==void 0?e:"",{value:l}=await this.get({key:r});typeof l=="string"?s.push(r):(await this.set({key:r,value:a}),t.push(r))}return{migrated:t,existing:s}}async removeOld(){const e="_cap_",t=Object.keys(this.impl).filter(s=>s.indexOf(e)===0);for(const s of t)this.impl.removeItem(s)}get impl(){return window.localStorage}get prefix(){return this.group==="NativeStorage"?"":`${this.group}.`}rawKeys(){return Object.keys(this.impl).filter(e=>e.indexOf(this.prefix)===0)}applyPrefix(e){return this.prefix+e}}export{f as PreferencesWeb};
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
import{W as i}from"./index-B7lrTY2m.js";function o(){const t=window.navigator.connection||window.navigator.mozConnection||window.navigator.webkitConnection;let n="unknown";const e=t?t.type||t.effectiveType:null;if(e&&typeof e=="string")switch(e){case"bluetooth":case"cellular":n="cellular";break;case"none":n="none";break;case"ethernet":case"wifi":case"wimax":n="wifi";break;case"other":case"unknown":n="unknown";break;case"slow-2g":case"2g":case"3g":n="cellular";break;case"4g":n="wifi";break}return n}class s extends i{constructor(){super(),this.handleOnline=()=>{const e={connected:!0,connectionType:o()};this.notifyListeners("networkStatusChange",e)},this.handleOffline=()=>{const n={connected:!1,connectionType:"none"};this.notifyListeners("networkStatusChange",n)},typeof window<"u"&&(window.addEventListener("online",this.handleOnline),window.addEventListener("offline",this.handleOffline))}async getStatus(){if(!window.navigator)throw this.unavailable("Browser does not support the Network Information API");const n=window.navigator.onLine,e=o();return{connected:n,connectionType:n?e:"none"}}}const r=new s;export{r as Network,s as NetworkWeb};
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
import{W as s}from"./index-B7lrTY2m.js";class c extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i.name=="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{c as LocalNotificationsWeb};
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
import{W as n}from"./index-B7lrTY2m.js";class r extends n{async enable(e){}async disable(e){}}export{r as SafeAreaWeb};
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
import{W as t}from"./index-B7lrTY2m.js";class s extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}}export{s as AppWeb};
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
import{W as o}from"./index-B7lrTY2m.js";class a extends o{async getCurrentPosition(e){return new Promise((t,n)=>{navigator.geolocation.getCurrentPosition(i=>{t(i)},i=>{n(i)},Object.assign({enableHighAccuracy:!1,timeout:1e4,maximumAge:0},e))})}async watchPosition(e,t){return`${navigator.geolocation.watchPosition(i=>{t(i)},i=>{t(null,i)},Object.assign({enableHighAccuracy:!1,timeout:1e4,maximumAge:0,minimumUpdateInterval:5e3},e))}`}async clearWatch(e){navigator.geolocation.clearWatch(parseInt(e.id,10))}async checkPermissions(){if(typeof navigator>"u"||!navigator.permissions)throw this.unavailable("Permissions API not available in this browser");const e=await navigator.permissions.query({name:"geolocation"});return{location:e.state,coarseLocation:e.state}}async requestPermissions(){throw this.unimplemented("Not implemented on web.")}}const c=new a;export{c as Geolocation,a as GeolocationWeb};
|
||||
Vendored
+2
-2
@@ -8,8 +8,8 @@
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title>Vite App</title>
|
||||
<script type="module" crossorigin src="/assets/index-B7lrTY2m.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DnygOJsq.css">
|
||||
<script type="module" crossorigin src="/assets/index-DHS6QlZu.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-PWd6qU--.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
+26
-1
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-100 text-gray-900">
|
||||
<div class="min-h-screen bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 transition-colors duration-300">
|
||||
<!-- App Blocker Overlay -->
|
||||
<div v-if="isBlocked" class="blocker-overlay">
|
||||
<div class="blocker-content">
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
<!-- Bottom Navigation (only show for worker routes) -->
|
||||
<BottomNavigation v-if="showBottomNav && !isBlocked" />
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -28,6 +30,7 @@ import { SafeArea } from '@capacitor-community/safe-area'
|
||||
import { nativeServicesManager } from '@/services/nativeServicesManager.js'
|
||||
import { authService } from '@/services/authService.js'
|
||||
import BottomNavigation from '@/components/BottomNavigation.vue'
|
||||
import { initWebViewCompatibility, applyThemeWithCompat, getSystemDarkModePreference } from '@/utils/webviewCompat'
|
||||
|
||||
const { locale } = useI18n()
|
||||
|
||||
@@ -37,7 +40,26 @@ const route = useRoute()
|
||||
const isLoggedIn = ref(false)
|
||||
const isBlocked = ref(false)
|
||||
const blockMessage = ref('')
|
||||
// Theme initialization with WebView compatibility
|
||||
const updateTheme = (isDark) => {
|
||||
applyThemeWithCompat(isDark)
|
||||
}
|
||||
|
||||
const initializeTheme = () => {
|
||||
// Initialize WebView compatibility first
|
||||
initWebViewCompatibility()
|
||||
|
||||
// Check for saved theme preference or default to light mode
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
let isDark = false
|
||||
if (savedTheme) {
|
||||
isDark = savedTheme === 'dark'
|
||||
} else {
|
||||
// Check system preference with WebView compatibility
|
||||
isDark = getSystemDarkModePreference()
|
||||
}
|
||||
updateTheme(isDark)
|
||||
}
|
||||
|
||||
// Show bottom navigation only for worker routes
|
||||
const showBottomNav = computed(() => {
|
||||
@@ -112,6 +134,9 @@ watch(
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
// Initialize theme
|
||||
initializeTheme()
|
||||
|
||||
// Add app blocked event listener
|
||||
window.addEventListener('app-blocked', handleAppBlocked)
|
||||
window.addEventListener('user-forced-clock-out', handleForcedClockOut)
|
||||
|
||||
@@ -106,4 +106,201 @@
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Smooth transitions for theme changes */
|
||||
* {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Dark mode scrollbar styling */
|
||||
html.dark ::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
html.dark ::-webkit-scrollbar-track {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
html.dark ::-webkit-scrollbar-thumb {
|
||||
background: #6b7280;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
html.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
}
|
||||
|
||||
/* WebView Compatibility Styles */
|
||||
@layer utilities {
|
||||
/* Force specific styles for older WebView versions */
|
||||
.webview-compat {
|
||||
/* Background colors */
|
||||
--bg-color: #ffffff;
|
||||
--text-color: #000000;
|
||||
--card-bg: #ffffff;
|
||||
--gray-bg: #f3f4f6;
|
||||
--text-gray: #6b7280;
|
||||
--border-color: #e5e7eb;
|
||||
--nav-bg: #ffffff;
|
||||
--nav-border: #e5e7eb;
|
||||
}
|
||||
|
||||
.webview-compat.dark {
|
||||
--bg-color: #1a1a1a;
|
||||
--text-color: #ffffff;
|
||||
--card-bg: #2d2d2d;
|
||||
--gray-bg: #1f2937;
|
||||
--text-gray: #d1d5db;
|
||||
--border-color: #404040;
|
||||
--nav-bg: #1f2937;
|
||||
--nav-border: #374151;
|
||||
}
|
||||
|
||||
/* Apply fallback styles for older WebView */
|
||||
.webview-compat .bg-white {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.webview-compat.dark .bg-white {
|
||||
background-color: #2d2d2d !important;
|
||||
}
|
||||
|
||||
.webview-compat .bg-gray-100 {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.webview-compat.dark .bg-gray-100 {
|
||||
background-color: #1a1a1a !important;
|
||||
}
|
||||
|
||||
.webview-compat .bg-gray-800 {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
.webview-compat.dark .bg-gray-800 {
|
||||
background-color: #2d2d2d !important;
|
||||
}
|
||||
|
||||
/* Navigation bar specific styles for WebView compatibility */
|
||||
.webview-compat .bottom-nav-content {
|
||||
background-color: #ffffff !important;
|
||||
border-top: 1px solid #e5e7eb !important;
|
||||
}
|
||||
|
||||
.webview-compat.dark .bottom-nav-content {
|
||||
background-color: #1f2937 !important;
|
||||
border-top: 1px solid #374151 !important;
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
/* Text colors */
|
||||
.webview-compat .text-gray-600 {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.webview-compat.dark .text-gray-600 {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
.webview-compat .text-gray-800 {
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.webview-compat.dark .text-gray-800 {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
/* Worker dashboard specific styles */
|
||||
.webview-compat .text-gray-900 {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.webview-compat.dark .text-gray-900 {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
.webview-compat .text-gray-400 {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.webview-compat.dark .text-gray-400 {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
/* Navigation bar text styles */
|
||||
.webview-compat .bottom-nav-content .text-gray-600 {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.webview-compat.dark .bottom-nav-content .text-gray-600 {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
.webview-compat .bottom-nav-content .text-blue-600 {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.webview-compat.dark .bottom-nav-content .text-blue-600 {
|
||||
color: #60a5fa !important;
|
||||
}
|
||||
|
||||
.webview-compat .bottom-nav-content span {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.webview-compat.dark .bottom-nav-content span {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
/* Border colors
|
||||
.webview-compat .border-gray-200 {
|
||||
border-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.webview-compat.dark .border-gray-200 {
|
||||
border-color: #404040 !important;
|
||||
}
|
||||
|
||||
/* Ensure transitions work in older WebView */
|
||||
.webview-compat * {
|
||||
-webkit-transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* CSS Variables with fallbacks for older WebView */
|
||||
:root {
|
||||
--bg-color: #ffffff;
|
||||
--text-color: #000000;
|
||||
--card-bg: #ffffff;
|
||||
--border-color: #e5e7eb;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
--bg-color: #1a1a1a;
|
||||
--text-color: #ffffff;
|
||||
--card-bg: #2d2d2d;
|
||||
--border-color: #404040;
|
||||
}
|
||||
|
||||
/* Apply CSS variables with fallbacks */
|
||||
.theme-bg {
|
||||
background-color: #ffffff; /* fallback */
|
||||
background-color: var(--bg-color, #ffffff);
|
||||
}
|
||||
|
||||
.theme-text {
|
||||
color: #000000; /* fallback */
|
||||
color: var(--text-color, #000000);
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
background-color: #ffffff; /* fallback */
|
||||
background-color: var(--card-bg, #ffffff);
|
||||
}
|
||||
|
||||
.theme-border {
|
||||
border-color: #e5e7eb; /* fallback */
|
||||
border-color: var(--border-color, #e5e7eb);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<router-link
|
||||
to="/worker/dashboard"
|
||||
class="flex-1 flex flex-col items-center py-3 px-2 text-center transition-colors duration-200"
|
||||
:class="isClockInActive ? 'text-blue-600 bg-blue-50' : 'text-gray-600 hover:text-blue-600 hover:bg-gray-50'"
|
||||
:class="isClockInActive ? 'text-blue-600' : 'text-gray-600 hover:text-blue-600 hover:bg-gray-50 dark:hover:bg-gray-700'"
|
||||
>
|
||||
<component :is="isClockInActive ? ClockIconSolid : ClockIconOutline" class="w-7 h-7 mb-1" />
|
||||
<span class="text-xs font-medium">{{ $t('clockIn') }}</span>
|
||||
@@ -15,7 +15,7 @@
|
||||
<router-link
|
||||
to="/worker/settings"
|
||||
class="flex-1 flex flex-col items-center py-3 px-2 text-center transition-colors duration-200"
|
||||
:class="isSettingsActive ? 'text-blue-600 bg-blue-50' : 'text-gray-600 hover:text-blue-600 hover:bg-gray-50'"
|
||||
:class="isSettingsActive ? 'text-blue-600 bg-blue-50 dark:bg-blue-900/50' : 'text-gray-600 hover:text-blue-600 hover:bg-gray-50 dark:hover:bg-gray-700'"
|
||||
>
|
||||
<component :is="isSettingsActive ? SettingsIconSolid : SettingsIconOutline" class="w-7 h-7 mb-1" />
|
||||
<span class="text-xs font-medium">{{ $t('setting') }}</span>
|
||||
@@ -73,6 +73,13 @@ const isSettingsActive = computed(() =>
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Dark mode styles */
|
||||
html.dark .bottom-nav-content {
|
||||
background-color: #1f2937;
|
||||
border-top: 1px solid #374151;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Prevent any scroll-related movement */
|
||||
.bottom-nav-container::before {
|
||||
content: '';
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"password": "পাসওয়ার্ড",
|
||||
"loggingIn": "লগ ইন করা হচ্ছে...",
|
||||
"language": "ভাষা",
|
||||
"darkMode": "ডার্ক মোড",
|
||||
"toggleDarkMode": "হালকা এবং অন্ধকার থিমের মধ্যে পরিবর্তন করুন",
|
||||
"failedConnection": "সার্ভারের সাথে সংযোগ করতে পারেনি।",
|
||||
"invalidToken": "সার্ভার থেকে অবৈধ টোকেন পাওয়া গেছে।",
|
||||
"invalidCredentials": "ভুল ইউজারনেম বা পাসওয়ার্ড।",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"password": "Password",
|
||||
"loggingIn": "Logging in...",
|
||||
"language": "Language",
|
||||
"darkMode": "Dark Mode",
|
||||
"toggleDarkMode": "Switch between light and dark themes",
|
||||
"failedConnection": "Failed to connect to the server.",
|
||||
"invalidToken": "Invalid token received from server.",
|
||||
"invalidCredentials": "Invalid username or password.",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"password": "Kata Laluan",
|
||||
"loggingIn": "Sedang log masuk...",
|
||||
"language": "Bahasa",
|
||||
"darkMode": "Mod Gelap",
|
||||
"toggleDarkMode": "Tukar antara tema terang dan gelap",
|
||||
"failedConnection": "Gagal untuk berhubung dengan pelayan.",
|
||||
"invalidCredentials": "Nama pengguna atau kata laluan tidak sah.",
|
||||
"invalidToken": "Token tidak sah diterima dari pelayan.",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"password": "လျှို့ဝှက်နံပါတ်",
|
||||
"loggingIn": "ဝင်ရောက်နေသည်...",
|
||||
"language": "ဘာသာစကား",
|
||||
"darkMode": "မှောင်မိုက်မုဒ်",
|
||||
"toggleDarkMode": "အလင်းနှင့် မှောင်မိုက်အပြင်အဆင်များကြား ပြောင်းလဲရန်",
|
||||
"failedConnection": "ဆာဗာနှင့် ချိတ်ဆက်၍မရပါ။",
|
||||
"invalidToken": "ဆာဗာမှ မမှန်ကန်သော တိုကင်ရရှိခဲ့သည်။",
|
||||
"invalidCredentials": "အသုံးပြုသူအမည် သို့မဟုတ် လျှို့ဝှက်နံပါတ် မမှန်ကန်ပါ။",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"password": "पासवर्ड",
|
||||
"loggingIn": "लगइन गर्दै...",
|
||||
"language": "भाषा",
|
||||
"darkMode": "डार्क मोड",
|
||||
"toggleDarkMode": "उज्यालो र अँध्यारो थिमहरू बीच स्विच गर्नुहोस्",
|
||||
"failedConnection": "सर्भरसँग जडान गर्न सकिएन।",
|
||||
"invalidToken": "सर्भरबाट अमान्य टोकन प्राप्त भयो।",
|
||||
"invalidCredentials": "गलत प्रयोगकर्ता नाम वा पासवर्ड।",
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"password": "கடவுச்சொல்",
|
||||
"loggingIn": "உள்நுழைகிறது...",
|
||||
"language": "மொழி",
|
||||
"darkMode": "இருண்ட பயன்முறை",
|
||||
"toggleDarkMode": "வெளிச்சம் மற்றும் இருண்ட தீம்களுக்கு இடையில் மாற்றவும்",
|
||||
"failedConnection": "சர்வருடன் இணைக்க முடியவில்லை.",
|
||||
"invalidToken": "சர்வரிலிருந்து தவறான டோக்கன் பெறப்பட்டது.",
|
||||
"invalidCredentials": "தவறான பயனர் பெயர் அல்லது கடவுச்சொல்.",
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* WebView Compatibility Utilities
|
||||
* Provides compatibility handling for older WebView versions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Detect WebView version and capabilities
|
||||
*/
|
||||
export const detectWebViewCapabilities = () => {
|
||||
const capabilities = {
|
||||
cssVariables: true,
|
||||
matchMedia: true,
|
||||
modernCSS: true,
|
||||
version: null
|
||||
}
|
||||
|
||||
try {
|
||||
// Check for CSS Variables support
|
||||
if (!CSS || !CSS.supports || !CSS.supports('color', 'var(--test)')) {
|
||||
capabilities.cssVariables = false
|
||||
}
|
||||
} catch (error) {
|
||||
capabilities.cssVariables = false
|
||||
}
|
||||
|
||||
try {
|
||||
// Check for matchMedia support
|
||||
if (!window.matchMedia || typeof window.matchMedia !== 'function') {
|
||||
capabilities.matchMedia = false
|
||||
}
|
||||
} catch (error) {
|
||||
capabilities.matchMedia = false
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to detect WebView version from user agent
|
||||
const userAgent = navigator.userAgent
|
||||
const chromeMatch = userAgent.match(/Chrome\/(\d+\.\d+\.\d+\.\d+)/)
|
||||
if (chromeMatch) {
|
||||
capabilities.version = chromeMatch[1]
|
||||
const majorVersion = parseInt(chromeMatch[1].split('.')[0])
|
||||
|
||||
// Chrome 88+ has better CSS support
|
||||
if (majorVersion < 88) {
|
||||
capabilities.modernCSS = false
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not detect WebView version:', error)
|
||||
}
|
||||
|
||||
return capabilities
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply WebView compatibility class to document
|
||||
*/
|
||||
export const applyWebViewCompatibility = () => {
|
||||
const capabilities = detectWebViewCapabilities()
|
||||
|
||||
// Add webview-compat class if needed
|
||||
if (!capabilities.cssVariables || !capabilities.modernCSS) {
|
||||
document.documentElement.classList.add('webview-compat')
|
||||
console.info('Applied WebView compatibility mode')
|
||||
}
|
||||
|
||||
return capabilities
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced theme application with WebView compatibility
|
||||
*/
|
||||
export const applyThemeWithCompat = (isDark) => {
|
||||
// --- FINAL FIX: Delay execution to prevent a race condition with Vue's rendering cycle. ---
|
||||
setTimeout(() => {
|
||||
const capabilities = detectWebViewCapabilities()
|
||||
|
||||
try {
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add('dark')
|
||||
|
||||
if (!capabilities.cssVariables || !capabilities.modernCSS) {
|
||||
document.documentElement.style.backgroundColor = '#1a1a1a';
|
||||
document.documentElement.style.color = '#ffffff';
|
||||
document.body.style.backgroundColor = '#1a1a1a';
|
||||
document.body.style.color = '#ffffff';
|
||||
|
||||
const navElements = document.querySelectorAll('.bottom-nav-content');
|
||||
navElements.forEach(el => {
|
||||
el.style.setProperty('background-color', '#1f2937', 'important');
|
||||
el.style.setProperty('border-top', '1px solid #374151', 'important');
|
||||
el.style.setProperty('color', '#f9fafb', 'important');
|
||||
});
|
||||
|
||||
const navInactiveElements = document.querySelectorAll('.bottom-nav-content .text-gray-600 span, .bottom-nav-content .text-gray-600 svg');
|
||||
navInactiveElements.forEach(el => {
|
||||
el.style.setProperty('color', '#d1d5db', 'important');
|
||||
});
|
||||
|
||||
const navActiveElements = document.querySelectorAll('.bottom-nav-content .text-blue-600');
|
||||
navActiveElements.forEach(el => {
|
||||
el.style.setProperty('color', '#60a5fa', 'important');
|
||||
el.style.setProperty('background-color', 'rgba(96, 165, 250, 0.15)', 'important');
|
||||
});
|
||||
}
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
|
||||
if (!capabilities.cssVariables || !capabilities.modernCSS) {
|
||||
document.documentElement.style.backgroundColor = '#ffffff';
|
||||
document.documentElement.style.color = '#000000';
|
||||
document.body.style.backgroundColor = '#ffffff';
|
||||
document.body.style.color = '#000000';
|
||||
|
||||
const navElements = document.querySelectorAll('.bottom-nav-content');
|
||||
navElements.forEach(el => {
|
||||
el.style.setProperty('background-color', '#ffffff', 'important');
|
||||
el.style.setProperty('border-top', '1px solid #e5e7eb', 'important');
|
||||
el.style.removeProperty('color');
|
||||
});
|
||||
|
||||
const navInactiveElements = document.querySelectorAll('.bottom-nav-content .text-gray-600 span, .bottom-nav-content .text-gray-600 svg');
|
||||
navInactiveElements.forEach(el => {
|
||||
el.style.setProperty('color', '#4b5563', 'important');
|
||||
});
|
||||
|
||||
const navActiveElements = document.querySelectorAll('.bottom-nav-content .text-blue-600');
|
||||
navActiveElements.forEach(el => {
|
||||
el.style.setProperty('color', '#2563eb', 'important');
|
||||
el.style.setProperty('background-color', '#eff6ff', 'important');
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Error applying theme with compatibility:', error);
|
||||
if (isDark) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
}
|
||||
}, 100); // A 100ms delay to ensure Vue has rendered.
|
||||
}
|
||||
|
||||
/**
|
||||
* Check system dark mode preference with WebView compatibility
|
||||
*/
|
||||
export const getSystemDarkModePreference = () => {
|
||||
try {
|
||||
if (window.matchMedia && typeof window.matchMedia === 'function') {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('matchMedia not supported, defaulting to light mode')
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize WebView compatibility on app startup
|
||||
*/
|
||||
export const initWebViewCompatibility = () => {
|
||||
const capabilities = applyWebViewCompatibility()
|
||||
|
||||
console.info('WebView Capabilities:', capabilities)
|
||||
|
||||
if (!capabilities.cssVariables) {
|
||||
console.warn('CSS Variables not supported, using fallback styles')
|
||||
}
|
||||
|
||||
if (!capabilities.matchMedia) {
|
||||
console.warn('matchMedia not supported, system theme detection disabled')
|
||||
}
|
||||
|
||||
if (!capabilities.modernCSS) {
|
||||
console.warn('Modern CSS features limited, using compatibility mode')
|
||||
}
|
||||
|
||||
return capabilities
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="flex justify-center items-center mobile-viewport bg-gray-100 safe-top safe-bottom">
|
||||
<div class="w-full max-w-sm p-8 space-y-3 bg-white rounded-2xl shadow-lg">
|
||||
<div class="flex justify-center items-center mobile-viewport bg-gray-100 dark:bg-gray-900 safe-top safe-bottom transition-colors duration-300">
|
||||
<div class="w-full max-w-sm p-8 space-y-3 bg-white dark:bg-gray-800 rounded-2xl shadow-lg transition-colors duration-300">
|
||||
<!-- App Logo -->
|
||||
<div class="flex justify-center">
|
||||
<ArrowRightOnRectangleIcon class="w-16 h-16 text-blue-600" />
|
||||
<ArrowRightOnRectangleIcon class="w-16 h-16 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="text-3xl font-extrabold text-center text-gray-900">
|
||||
<h2 class="text-3xl font-extrabold text-center text-gray-900 dark:text-gray-100">
|
||||
{{ t('login') }}
|
||||
</h2>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div>
|
||||
<label for="username" class="sr-only">{{ t('username') }}</label>
|
||||
<input type="text" id="username" v-model="username" autocomplete="username"
|
||||
class="w-full px-4 py-3 text-gray-700 bg-gray-50 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
class="w-full px-4 py-3 text-gray-700 dark:text-gray-200 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent transition-colors duration-300"
|
||||
:placeholder="t('username')"
|
||||
required />
|
||||
</div>
|
||||
@@ -25,7 +25,7 @@
|
||||
<div>
|
||||
<label for="password" class="sr-only">{{ t('password') }}</label>
|
||||
<input type="password" id="password" v-model="password" autocomplete="current-password"
|
||||
class="w-full px-4 py-3 text-gray-700 bg-gray-50 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
class="w-full px-4 py-3 text-gray-700 dark:text-gray-200 bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent transition-colors duration-300"
|
||||
:placeholder="t('password')"
|
||||
required />
|
||||
</div>
|
||||
@@ -34,8 +34,8 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="rememberMe" v-model="rememberMe"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500" />
|
||||
<label for="rememberMe" class="ml-2 block text-sm text-gray-900">
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 dark:focus:ring-blue-400" />
|
||||
<label for="rememberMe" class="ml-2 block text-sm text-gray-900 dark:text-gray-200">
|
||||
{{ t('rememberMe') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
+69
-23
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="mobile-viewport bg-gray-100 min-h-screen">
|
||||
<div class="mobile-viewport bg-gray-100 dark:bg-gray-900 min-h-screen transition-colors duration-300">
|
||||
<!-- Fixed Header -->
|
||||
<header class="fixed-header-safe bg-blue-600 text-white shadow-lg">
|
||||
<header class="fixed-header-safe bg-blue-600 dark:bg-gray-800 text-white shadow-lg transition-colors duration-300">
|
||||
<div class="px-4 py-6">
|
||||
<h1 class="text-3xl font-bold text-center">{{ $t('setting') }}</h1>
|
||||
</div>
|
||||
@@ -10,42 +10,60 @@
|
||||
<!-- Scrollable Main Content -->
|
||||
<main class="main-with-fixed-header-and-nav px-4 py-8 space-y-4">
|
||||
<!-- Menu Items -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden mt-8">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden mt-8 transition-colors duration-300">
|
||||
<!-- Clock History -->
|
||||
<router-link to="/worker/history"
|
||||
class="flex items-center p-5 border-b border-gray-200 hover:bg-gray-50 transition-colors">
|
||||
class="flex items-center p-5 border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center mr-5">
|
||||
<ChartBarIcon class="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h3 class="font-semibold text-lg text-gray-900">{{ $t('clockHistory') }}</h3>
|
||||
<p class="text-sm text-gray-500">{{ $t('viewMyClockHistory') }}</p>
|
||||
<h3 class="font-semibold text-lg text-gray-900 dark:text-gray-100">{{ $t('clockHistory') }}</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ $t('viewMyClockHistory') }}</p>
|
||||
</div>
|
||||
<ChevronRightIcon class="w-6 h-6 text-gray-400" />
|
||||
<ChevronRightIcon class="w-6 h-6 text-gray-400 dark:text-gray-500" />
|
||||
</router-link>
|
||||
|
||||
<!-- Change Password -->
|
||||
<router-link to="/worker/change-password"
|
||||
class="flex items-center p-5 border-b border-gray-200 hover:bg-gray-50 transition-colors">
|
||||
class="flex items-center p-5 border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<div class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center mr-5">
|
||||
<LockClosedIcon class="w-8 h-8 text-orange-600" />
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h3 class="font-semibold text-lg text-gray-900">{{ $t('changePassword') }}</h3>
|
||||
<p class="text-sm text-gray-500">{{ $t('updateYourPassword') }}</p>
|
||||
<h3 class="font-semibold text-lg text-gray-900 dark:text-gray-100">{{ $t('changePassword') }}</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ $t('updateYourPassword') }}</p>
|
||||
</div>
|
||||
<ChevronRightIcon class="w-6 h-6 text-gray-400" />
|
||||
<ChevronRightIcon class="w-6 h-6 text-gray-400 dark:text-gray-500" />
|
||||
</router-link>
|
||||
|
||||
<!-- Dark Mode Toggle -->
|
||||
<div class="flex items-center p-5 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="w-12 h-12 bg-indigo-100 rounded-xl flex items-center justify-center mr-5">
|
||||
<MoonIcon v-if="!isDarkMode" class="w-8 h-8 text-indigo-600" />
|
||||
<SunIcon v-else class="w-8 h-8 text-indigo-600" />
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h3 class="font-semibold text-lg text-gray-900 dark:text-gray-100">{{ $t('darkMode') }}</h3>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ $t('toggleDarkMode') }}</p>
|
||||
</div>
|
||||
<button @click="toggleDarkMode"
|
||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
:class="isDarkMode ? 'bg-indigo-600' : 'bg-gray-200'">
|
||||
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
||||
:class="isDarkMode ? 'translate-x-6' : 'translate-x-1'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Language Selection -->
|
||||
<div class="flex items-center p-5">
|
||||
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center mr-5">
|
||||
<LanguageIcon class="w-8 h-8 text-purple-600" />
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h3 class="font-semibold text-lg text-gray-900 mb-2">{{ $t('language') }}</h3>
|
||||
<h3 class="font-semibold text-lg text-gray-900 dark:text-gray-100 mb-2">{{ $t('language') }}</h3>
|
||||
<select v-model="currentLang" @change="changeLang"
|
||||
class="w-full px-4 py-3 border border-gray-300 bg-white text-gray-900 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent">
|
||||
class="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent transition-colors duration-300">
|
||||
<option value="en">{{ $t('english') }}</option>
|
||||
<option value="ms">{{ $t('malay') }}</option>
|
||||
<option value="tm">{{ $t('tamil') }}</option>
|
||||
@@ -58,29 +76,29 @@
|
||||
</div>
|
||||
|
||||
<!-- App Information -->
|
||||
<div class="bg-white rounded-2xl shadow-lg p-6">
|
||||
<h3 class="font-semibold text-lg text-gray-900 mb-4">{{ $t('appInformation') }}</h3>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 transition-colors duration-300">
|
||||
<h3 class="font-semibold text-lg text-gray-900 dark:text-gray-100 mb-4">{{ $t('appInformation') }}</h3>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">{{ $t('version') }}</span>
|
||||
<span class="font-medium text-gray-900">1.0.0</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ $t('version') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">1.0.0</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-600">{{ $t('platform') }}</span>
|
||||
<span class="font-medium text-gray-900">{{ $t('android') }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-400">{{ $t('platform') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-gray-100">{{ $t('android') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logout Button -->
|
||||
<button @click="logout"
|
||||
class="w-full flex items-center justify-center p-5 bg-white rounded-2xl shadow-lg hover:bg-red-50 transition-colors">
|
||||
class="w-full flex items-center justify-center p-5 bg-white dark:bg-gray-800 rounded-2xl shadow-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors duration-300">
|
||||
<div class="w-12 h-12 bg-red-100 rounded-xl flex items-center justify-center mr-5">
|
||||
<ArrowRightOnRectangleIcon class="w-8 h-8 text-red-600" />
|
||||
</div>
|
||||
<div class="flex-grow text-left">
|
||||
<h3 class="font-semibold text-lg text-red-600">{{ $t('logout') }}</h3>
|
||||
<p class="text-sm text-red-500">{{ $t('signOutOfAccount') }}</p>
|
||||
<h3 class="font-semibold text-lg text-red-600 dark:text-red-400">{{ $t('logout') }}</h3>
|
||||
<p class="text-sm text-red-500 dark:text-red-400">{{ $t('signOutOfAccount') }}</p>
|
||||
</div>
|
||||
</button>
|
||||
</main>
|
||||
@@ -91,20 +109,34 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ChartBarIcon, LockClosedIcon, LanguageIcon, ArrowRightOnRectangleIcon, ChevronRightIcon } from '@heroicons/vue/24/outline'
|
||||
import { ChartBarIcon, LockClosedIcon, LanguageIcon, ArrowRightOnRectangleIcon, ChevronRightIcon, MoonIcon, SunIcon } from '@heroicons/vue/24/outline'
|
||||
import { authService } from '@/services/authService.js'
|
||||
import { nativeServicesManager } from '@/services/nativeServicesManager.js'
|
||||
import { applyThemeWithCompat, getSystemDarkModePreference } from '@/utils/webviewCompat.js'
|
||||
|
||||
const { locale } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
const currentLang = ref(locale.value)
|
||||
const isDarkMode = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
const savedLang = localStorage.getItem('lang')
|
||||
if (savedLang) {
|
||||
currentLang.value = savedLang
|
||||
}
|
||||
|
||||
// Initialize dark mode with WebView compatibility
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
const prefersDark = getSystemDarkModePreference()
|
||||
|
||||
if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
|
||||
isDarkMode.value = true
|
||||
applyThemeWithCompat(true)
|
||||
} else {
|
||||
isDarkMode.value = false
|
||||
applyThemeWithCompat(false)
|
||||
}
|
||||
})
|
||||
|
||||
const changeLang = () => {
|
||||
@@ -113,6 +145,20 @@ const currentLang = ref(locale.value)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
isDarkMode.value = !isDarkMode.value
|
||||
|
||||
applyThemeWithCompat(isDarkMode.value)
|
||||
|
||||
if (isDarkMode.value) {
|
||||
localStorage.setItem('theme', 'dark')
|
||||
} else {
|
||||
localStorage.setItem('theme', 'light')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await authService.logout()
|
||||
|
||||
@@ -4,6 +4,7 @@ export default {
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user