完善游客模式登录 并且新增设备管理

This commit is contained in:
Rust 2025-10-17 21:11:40 +08:00
parent 2333ad52a9
commit de9cf751f3
46 changed files with 4590 additions and 543 deletions

View File

@ -6,6 +6,7 @@
"codeSent": "A 6-digit code has been sent to {account}. Please enter it within 30 minutes.",
"back": "Back",
"enterEmailOrPhone": "Enter Email or Phone Number",
"enterEmail": "Please enter email address",
"enterCode": "Please enter verification code",
"enterPassword": "Please enter password",
"reenterPassword": "Please re-enter password",

View File

@ -6,6 +6,7 @@
"codeSent": "Se ha enviado un código de 6 dígitos a {account}. Por favor, ingrésalo en los próximos 30 minutos.",
"back": "Atrás",
"enterEmailOrPhone": "Ingresa correo o teléfono",
"enterEmail": "Please enter email address",
"enterCode": "Por favor, ingresa el código de verificación",
"enterPassword": "Por favor, ingresa la contraseña",
"reenterPassword": "Por favor, reingresa la contraseña",

View File

@ -0,0 +1,441 @@
{
"login": {
"welcome": "¡Bienvenido a BearVPN!",
"verifyPhone": "Verifica tu número de teléfono",
"verifyEmail": "Verifica tu correo electrónico",
"codeSent": "Se ha enviado un código de 6 dígitos a {account}. Por favor, ingrésalo en los próximos 30 minutos.",
"back": "Atrás",
"enterEmailOrPhone": "Ingresa correo o teléfono",
"enterCode": "Por favor, ingresa el código de verificación",
"enterPassword": "Por favor, ingresa la contraseña",
"reenterPassword": "Por favor, reingresa la contraseña",
"forgotPassword": "Olvidé mi contraseña",
"codeLogin": "Iniciar sesión con código",
"passwordLogin": "Iniciar sesión con contraseña",
"agreeTerms": "Iniciar sesión/Crear cuenta, acepto",
"termsOfService": "Términos de servicio",
"privacyPolicy": "Política de privacidad",
"next": "Siguiente",
"registerNow": "Registrarse ahora",
"setAndLogin": "Configurar e iniciar sesión",
"enterAccount": "Por favor, ingresa la cuenta",
"passwordMismatch": "Las dos contraseñas no coinciden",
"sendCode": "Enviar código",
"codeSentCountdown": "Código enviado {seconds}s",
"and": "y",
"enterInviteCode": "Ingresa código de invitación (opcional)",
"registerSuccess": "Registro exitoso"
},
"failure": {
"unexpected": "Error inesperado",
"clash": {
"unexpected": "Error inesperado",
"core": "Error de Clash ${reason}"
},
"singbox": {
"unexpected": "Error de servicio inesperado",
"serviceNotRunning": "Servicio no en ejecución",
"missingPrivilege": "Privilegios insuficientes",
"missingPrivilegeMsg": "El modo VPN requiere privilegios de administrador. Reinicia la aplicación como administrador o cambia el modo de servicio",
"missingGeoAssets": "Faltan recursos GEO",
"missingGeoAssetsMsg": "Faltan archivos de recursos GEO. Considera cambiar los recursos activos o descarga los recursos seleccionados en configuración.",
"invalidConfigOptions": "Opciones de configuración inválidas",
"invalidConfig": "Configuración inválida",
"create": "Error al crear servicio",
"start": "Error al iniciar servicio"
},
"connectivity": {
"unexpected": "Fallo inesperado",
"missingVpnPermission": "Falta permiso VPN",
"missingNotificationPermission": "Falta permiso de notificaciones",
"core": "Error del núcleo"
},
"profiles": {
"unexpected": "Error inesperado",
"notFound": "Perfil no encontrado",
"invalidConfig": "Configuración inválida",
"invalidUrl": "URL inválida"
},
"connection": {
"unexpected": "Error de conexión inesperado",
"timeout": "Tiempo de conexión agotado",
"badResponse": "Respuesta incorrecta",
"connectionError": "Error de conexión",
"badCertificate": "Certificado inválido"
},
"geoAssets": {
"unexpected": "Error inesperado",
"notUpdate": "No hay actualizaciones disponibles",
"activeNotFound": "No se encontraron recursos GEO activos"
}
},
"userInfo": {
"title": "Mi información",
"bindingTip": "Correo/teléfono no vinculado",
"myAccount": "Mi cuenta",
"balance": "Saldo",
"noValidSubscription": "No tiene una suscripción válida",
"subscribeNow": "Suscribirse ahora",
"shortcuts": "Accesos directos",
"adBlock": "Bloqueo de anuncios",
"dnsUnlock": "Desbloqueo DNS",
"contactUs": "Contáctanos",
"others": "Otros",
"logout": "Cerrar sesión",
"logoutConfirmTitle": "Cerrar sesión",
"logoutConfirmMessage": "¿Estás seguro de que quieres cerrar sesión?",
"logoutCancel": "Cancelar",
"vpnWebsite": "Sitio web VPN",
"telegram": "Telegram",
"mail": "Correo",
"phone": "Teléfono",
"customerService": "Servicio al cliente",
"workOrder": "Enviar ticket",
"pleaseLogin": "Por favor, inicia sesión primero",
"subscriptionValid": "Suscripción válida",
"startTime": "Fecha de inicio:",
"expireTime": "Fecha de vencimiento:",
"loginNow": "Iniciar sesión ahora",
"trialPeriod": "Bienvenido a la prueba Premium",
"remainingTime": "Tiempo restante",
"trialExpired": "Prueba expirada, conexión desconectada",
"subscriptionExpired": "Suscripción expirada, conexión desconectada",
"copySuccess": "Copiado con éxito",
"notAvailable": "No disponible",
"deviceLimit": "Límite de dispositivos: {count}",
"reset": "Restablecer",
"trafficUsage": "Usado: {used} / {total}",
"trafficProgress": {
"title": "Uso de tráfico",
"unlimited": "Tráfico ilimitado",
"limited": "Tráfico usado"
},
"switchSubscription": "Cambiar Suscripción",
"resetTrafficTitle": "Restablecer Tráfico",
"resetTrafficMessage": "Ejemplo de restablecimiento de tráfico del plan mensual: restablecer el tráfico del siguiente ciclo mensualmente, y el período de validez de la suscripción se adelantará de {currentTime} a {newTime}",
"trialStatus": "Estado de prueba",
"trialing": "En período de prueba",
"trialEndMessage": "No podrá continuar usando después de que expire el período de prueba",
"lastDaySubscriptionStatus": "Suscripción por expirar",
"lastDaySubscriptionMessage": "Por expirar",
"subscriptionEndMessage": "No podrá continuar usando después de que expire la suscripción",
"trialTimeWithDays": "{days}d {hours}h {minutes}m {seconds}s",
"trialTimeWithHours": "{hours}h {minutes}m {seconds}s",
"trialTimeWithMinutes": "{minutes}m {seconds}s",
"refreshLatency": "Actualizar latencia",
"testLatency": "Probar latencia",
"testing": "Probando latencia",
"refreshLatencyDesc": "Actualizar latencia de todos los nodos",
"testAllNodesLatency": "Probar la latencia de red de todos los nodos",
"autoSelect": "Selección automática",
"selected": "Seleccionado",
"willBeDeleted": "será eliminado",
"deleteAccountWarning": "La eliminación de la cuenta es permanente. Una vez que se elimine su cuenta, no podrá utilizar ninguna función. ¿Continuar?",
"requestDelete": "Solicitar eliminación"
},
"setting": {
"title": "Configuración",
"vpnConnection": "Conexión VPN",
"general": "General",
"autoConnect": "Conexión automática",
"routeRule": "Reglas de ruta",
"countrySelector": "Seleccionar país",
"appearance": "Apariencia",
"notifications": "Notificaciones",
"helpImprove": "Ayúdanos a mejorar",
"helpImproveSubtitle": "Subtítulo de ayuda para mejorar",
"requestDeleteAccount": "Solicitar eliminación de cuenta",
"goToDelete": "Ir a eliminar",
"rateUs": "Califícanos en App Store",
"iosRating": "Calificación iOS",
"version": "Versión",
"switchLanguage": "Cambiar idioma",
"system": "Sistema",
"light": "Claro",
"dark": "Oscuro",
"vpnModeSmart": "Modo inteligente",
"mode": "Modo de salida",
"connectionTypeGlobal": "Proxy global",
"connectionTypeGlobalRemark": "Cuando está activado, todo el tráfico pasa por el proxy",
"connectionTypeRule": "Proxy inteligente",
"connectionTypeRuleRemark": "Cuando el [Modo de salida] está configurado en [Proxy inteligente], el sistema dividirá automáticamente el tráfico nacional e internacional según el país seleccionado: las IPs/dominios nacionales se conectan directamente, mientras que las solicitudes extranjeras se acceden a través del proxy",
"connectionTypeDirect": "Conexión directa",
"connectionTypeDirectRemark": "Cuando está activado, todo el tráfico evita el proxy",
"smartMode": "Modo inteligente",
"secureMode": "Modo seguro"
},
"statistics": {
"title": "Estadísticas",
"vpnStatus": "Estado VPN",
"ipAddress": "Dirección IP",
"connectionTime": "Tiempo de conexión",
"protocol": "Protocolo",
"weeklyProtectionTime": "Tiempo de protección semanal",
"currentStreak": "Racha actual",
"highestStreak": "Mejor racha",
"longestConnection": "Conexión más larga",
"days": "{days} días",
"daysOfWeek": {
"monday": "Lun",
"tuesday": "Mar",
"wednesday": "Mié",
"thursday": "Jue",
"friday": "Vie",
"saturday": "Sáb",
"sunday": "Dom"
}
},
"message": {
"title": "Notificaciones",
"system": "Mensajes del sistema",
"promotion": "Mensajes promocionales"
},
"home": {
"welcome": "Bienvenido a BearVPN",
"disconnected": "Desconectado",
"connecting": "Conectando",
"connected": "Conectado",
"disconnecting": "Desconectando",
"currentConnectionTitle": "Conexión actual",
"switchNode": "Cambiar nodo",
"timeout": "Tiempo de espera agotado",
"loading": "Cargando...",
"error": "Error de carga",
"checkNetwork": "Verifique su conexión de red e intente nuevamente",
"retry": "Reintentar",
"connectionSectionTitle": "Método de conexión",
"dedicatedServers": "Servidores dedicados",
"countryRegion": "País/Región",
"serverListTitle": "Grupos de servidores dedicados",
"nodeListTitle": "Todos los nodos",
"countryListTitle": "Lista de países/regiones",
"noServers": "No hay servidores disponibles",
"noNodes": "No hay nodos disponibles",
"noRegions": "No hay regiones disponibles",
"subscriptionDescription": "Obtenga acceso premium a la red global de alta velocidad",
"subscribe": "Suscribirse",
"trialPeriod": "Bienvenido a la versión de prueba Premium",
"remainingTime": "Tiempo restante",
"trialExpired": "Período de prueba expirado, conexión terminada",
"subscriptionExpired": "Suscripción expirada, conexión terminada",
"subscriptionUpdated": "Suscripción actualizada",
"subscriptionUpdatedMessage": "Su información de suscripción ha sido actualizada, actualice para ver el último estado",
"trialStatus": "Estado de prueba",
"trialing": "En período de prueba",
"trialEndMessage": "No podrá continuar usando después de que expire el período de prueba",
"lastDaySubscriptionStatus": "Suscripción por expirar",
"lastDaySubscriptionMessage": "Por expirar",
"subscriptionEndMessage": "No podrá continuar usando después de que expire la suscripción",
"trialTimeWithDays": "{days}d {hours}h {minutes}m {seconds}s",
"trialTimeWithHours": "{hours}h {minutes}m {seconds}s",
"trialTimeWithMinutes": "{minutes}m {seconds}s",
"refreshLatency": "Actualizar latencia",
"testLatency": "Probar latencia",
"testing": "Probando latencia",
"refreshLatencyDesc": "Actualizar latencia de todos los nodos",
"testAllNodesLatency": "Probar la latencia de red de todos los nodos",
"autoSelect": "Selección automática",
"selected": "Seleccionado"
},
"invite": {
"title": "Invitar amigos",
"progress": "Progreso de invitación",
"inviteStats": "Estadísticas de invitación",
"registers": "Registrados",
"totalCommission": "Comisión total",
"rewardDetails": "Detalles de recompensa >",
"steps": "Pasos de invitación",
"inviteFriend": "Invitar amigo",
"acceptInvite": "El amigo acepta la invitación\ny se registra",
"getReward": "Obtener recompensa",
"shareLink": "Compartir por enlace",
"shareQR": "Compartir por código QR",
"rules": "Reglas de invitación",
"rule1": "1. Puedes invitar amigos compartiendo tu enlace o código de invitación exclusivo.",
"rule2": "2. Después de que tu amigo complete el registro e inicie sesión, la recompensa por invitación se enviará automáticamente a tu cuenta.",
"pending": "Pendiente de descarga",
"processing": "En proceso",
"success": "Exitoso",
"expired": "Expirado",
"myInviteCode": "Mi código de invitación",
"inviteCodeCopied": "Código de invitación copiado al portapapeles"
},
"purchaseMembership": {
"purchasePackage": "Comprar Paquete",
"noData": "No hay paquetes disponibles",
"myAccount": "Mi Cuenta",
"selectPackage": "Seleccionar Paquete",
"packageDescription": "Descripción del Paquete",
"paymentMethod": "Método de Pago",
"cancelAnytime": "Puedes cancelar en cualquier momento en la APP",
"startSubscription": "Comenzar Suscripción",
"renewNow": "Renovar Ahora",
"month": "{quantity} meses",
"year": "{quantity} años",
"day": "{quantity} días",
"unlimitedTraffic": "Tráfico Ilimitado",
"unlimitedDevices": "Dispositivos Ilimitados",
"devices": "{count} dispositivos",
"features": "Características del Paquete",
"expand": "Expandir",
"collapse": "Colapsar",
"confirmPurchase": "Confirmar Compra",
"confirmPurchaseDesc": "¿Está seguro de que desea comprar este paquete?"
},
"orderStatus": {
"title": "Estado del Pedido",
"pending": {
"title": "Pago Pendiente",
"description": "Por favor complete el pago"
},
"paid": {
"title": "Pago Recibido",
"description": "Procesando su pedido"
},
"success": {
"title": "¡Felicitaciones! Pago Exitoso",
"description": "Su paquete ha sido comprado exitosamente"
},
"closed": {
"title": "Pedido Cerrado",
"description": "Por favor realice un nuevo pedido"
},
"failed": {
"title": "Pago Fallido",
"description": "Por favor intente el pago nuevamente"
},
"unknown": {
"title": "Estado Desconocido",
"description": "Por favor contacte al servicio al cliente"
},
"checkFailed": {
"title": "Verificación Fallida",
"description": "Por favor intente nuevamente más tarde"
},
"initial": {
"title": "Procesando Pago",
"description": "Por favor espere mientras procesamos su pago"
}
},
"dialog": {
"confirm": "Confirmar",
"cancel": "Cancelar",
"ok": "OK",
"iKnow": "Lo entiendo"
},
"splash": {
"appName": "BearVPN",
"slogan": "Red global de alta velocidad",
"initializing": "Inicializando...",
"networkConnectionFailure": "Error de conexión de red, verifique e intente nuevamente",
"retry": "Reintentar",
"networkPermissionFailed": "Error al obtener permiso de red",
"initializationFailed": "Error de inicialización"
},
"network": {
"status": {
"connected": "Conectado",
"disconnected": "Desconectado",
"connecting": "Conectando...",
"disconnecting": "Desconectando...",
"reconnecting": "Reconectando...",
"failed": "Error de conexión"
},
"permission": {
"title": "Permiso de red",
"description": "Se requiere permiso de red para proporcionar el servicio VPN",
"goToSettings": "Ir a configuración",
"cancel": "Cancelar"
}
},
"update": {
"title": "Actualización disponible",
"content": "¿Actualizar ahora?",
"updateNow": "Actualizar ahora",
"updateLater": "Más tarde",
"defaultContent": "1. Optimización del rendimiento de la aplicación\n2. Corrección de problemas conocidos\n3. Mejora de la experiencia del usuario"
},
"kr_invite": {
"close": "Cerrar",
"saveQRCode": "Guardar código QR",
"qrCodeSaved": "Código QR guardado",
"shareLink": "Compartir enlace",
"shareQR": "Compartir código QR",
"myInviteCode": "Mi código de invitación"
},
"country": {
"cn": "China",
"ir": "Irán",
"af": "Afganistán",
"ru": "Rusia",
"id": "Indonesia",
"tr": "Turquía",
"br": "Brasil"
},
"error": {
"200": "Éxito",
"500": "Error interno del servidor",
"10001": "Error de consulta a la base de datos",
"10002": "Error de actualización de la base de datos",
"10003": "Error de inserción en la base de datos",
"10004": "Error de eliminación de la base de datos",
"20001": "El usuario ya existe",
"20002": "El usuario no existe",
"20003": "Contraseña de usuario incorrecta",
"20004": "Usuario deshabilitado",
"20005": "Saldo insuficiente",
"20006": "Registro detenido",
"20007": "Telegram no vinculado",
"20008": "Usuario no ha vinculado OAuth",
"20009": "Código de invitación incorrecto",
"30001": "El nodo ya existe",
"30002": "El nodo no existe",
"30003": "El grupo de nodos ya existe",
"30004": "El grupo de nodos no existe",
"30005": "El grupo de nodos no está vacío",
"400": "Error de parámetros",
"40002": "Token de usuario vacío",
"40003": "Token de usuario inválido",
"40004": "Token de usuario expirado",
"40005": "No ha iniciado sesión",
"401": "Demasiadas solicitudes",
"50001": "El cupón no existe",
"50002": "El cupón ya ha sido usado",
"50003": "El cupón no coincide",
"60001": "Suscripción expirada",
"60002": "Suscripción no disponible",
"60003": "El usuario ya tiene una suscripción",
"60004": "La suscripción ya ha sido usada",
"60005": "Límite de suscripción única excedido",
"60006": "Límite de cuota de suscripción",
"70001": "Código de verificación incorrecto",
"80001": "Error al encolar",
"90001": "Modo de depuración habilitado",
"90002": "Error al enviar SMS",
"90003": "Función SMS no habilitada",
"90004": "Función de correo electrónico no habilitada",
"90005": "Método de inicio de sesión no soportado",
"90006": "El autenticador no soporta este método",
"90007": "Código de país de teléfono vacío",
"90008": "Contraseña vacía",
"90009": "Código de país vacío",
"90010": "Se requiere contraseña o código de verificación",
"90011": "El correo electrónico ya existe",
"90012": "El número de teléfono ya existe",
"90013": "El dispositivo ya existe",
"90014": "Número de teléfono incorrecto",
"90015": "Este cuenta ha alcanzado el límite de envío hoy",
"90017": "El dispositivo no existe",
"90018": "ID de usuario no coincide",
"61001": "El pedido no existe",
"61002": "Método de pago no encontrado",
"61003": "Estado de pedido incorrecto",
"61004": "Período de reinicio insuficiente",
"61005": "Existe tráfico sin usar"
},
"tray": {
"open_dashboard": "Abrir panel",
"copy_to_terminal": "Copiar al terminal",
"exit_app": "Salir de la aplicación"
}
}

View File

@ -6,6 +6,7 @@
"codeSent": "6-kohaline kood on saadetud aadressile {account}. Palun sisesta see 30 minuti jooksul.",
"back": "Tagasi",
"enterEmailOrPhone": "Sisesta e-post või telefoninumber",
"enterEmail": "Please enter email address",
"enterCode": "Palun sisesta kinnituskood",
"enterPassword": "Palun sisesta parool",
"reenterPassword": "Palun sisesta parool uuesti",

View File

@ -0,0 +1,423 @@
{
"login": {
"welcome": "Tere tulemast BearVPN-i!",
"verifyPhone": "Kinnita oma telefoninumber",
"verifyEmail": "Kinnita oma e-post",
"codeSent": "6-kohaline kood on saadetud aadressile {account}. Palun sisesta see 30 minuti jooksul.",
"back": "Tagasi",
"enterEmailOrPhone": "Sisesta e-post või telefoninumber",
"enterCode": "Palun sisesta kinnituskood",
"enterPassword": "Palun sisesta parool",
"reenterPassword": "Palun sisesta parool uuesti",
"forgotPassword": "Unustasid parooli",
"codeLogin": "Koodiga sisselogimine",
"passwordLogin": "Parooliga sisselogimine",
"agreeTerms": "Logi sisse/Loo konto, nõustun",
"termsOfService": "Teenusetingimustega",
"privacyPolicy": "Privaatsuspoliitikaga",
"next": "Edasi",
"registerNow": "Registreeru kohe",
"setAndLogin": "Seadista ja logi sisse",
"enterAccount": "Palun sisesta konto",
"passwordMismatch": "Kaks sisestatud parooli ei ühti",
"sendCode": "Saada kood",
"codeSentCountdown": "Kood saadetud {seconds}s",
"and": "ja",
"enterInviteCode": "Sisesta kutse kood (valikuline)",
"registerSuccess": "Registreerimine õnnestus"
},
"failure": {
"unexpected": "Ootamatu viga",
"clash": {
"unexpected": "Ootamatu viga",
"core": "Clash viga ${reason}"
},
"singbox": {
"unexpected": "Ootamatu teenuse viga",
"serviceNotRunning": "Teenus ei tööta",
"missingPrivilege": "Puuduvad õigused",
"missingPrivilegeMsg": "VPN režiim vajab administraatori õigusi. Taaskäivitage rakendus administraatorina või muutke teenuse režiimi",
"missingGeoAssets": "Puuduvad GEO ressursid",
"missingGeoAssetsMsg": "Puuduvad GEO ressursifailid. Kaaluge aktiivsete ressursside muutmist või laadige valitud ressursid seadetest alla.",
"invalidConfigOptions": "Kehtetud seadistuse valikud",
"invalidConfig": "Kehtetu seadistus",
"create": "Teenuse loomise viga",
"start": "Teenuse käivitamise viga"
},
"connectivity": {
"unexpected": "Ootamatu tõrge",
"missingVpnPermission": "Puudub VPN luba",
"missingNotificationPermission": "Puudub teavituste luba",
"core": "Tuuma viga"
},
"profiles": {
"unexpected": "Ootamatu viga",
"notFound": "Profiili ei leitud",
"invalidConfig": "Kehtetu seadistus",
"invalidUrl": "Kehtetu URL"
},
"connection": {
"unexpected": "Ootamatu ühenduse viga",
"timeout": "Ühenduse ajalõpp",
"badResponse": "Halb vastus",
"connectionError": "Ühenduse viga",
"badCertificate": "Kehtetu sertifikaat"
},
"geoAssets": {
"unexpected": "Ootamatu viga",
"notUpdate": "Uuendusi pole saadaval",
"activeNotFound": "Aktiivseid GEO ressursse ei leitud"
}
},
"userInfo": {
"title": "Minu info",
"bindingTip": "E-post/telefon sidumata",
"myAccount": "Minu konto",
"balance": "Jääk",
"noValidSubscription": "Teil pole kehtivat tellimust",
"subscribeNow": "Telli kohe",
"shortcuts": "Otseteed",
"adBlock": "Reklaami blokeerimine",
"dnsUnlock": "DNS avamine",
"contactUs": "Võta meiega ühendust",
"others": "Muu",
"logout": "Logi välja",
"logoutConfirmTitle": "Logi välja",
"logoutConfirmMessage": "Kas oled kindel, et soovid välja logida?",
"logoutCancel": "Tühista",
"vpnWebsite": "VPN veebileht",
"telegram": "Telegram",
"mail": "E-post",
"phone": "Telefon",
"customerService": "Klienditugi",
"workOrder": "Esita taotlus",
"pleaseLogin": "Palun logi esmalt sisse",
"subscriptionValid": "Tellimus kehtiv",
"startTime": "Algusaeg:",
"expireTime": "Aegumisaeg:",
"loginNow": "Logi kohe sisse",
"trialPeriod": "Tere tulemast Premium prooviperioodi",
"remainingTime": "Järelejäänud aeg",
"trialExpired": "Prooviperiood on lõppenud, ühendus katkestatud",
"subscriptionExpired": "Tellimus on aegunud, ühendus katkestatud",
"switchSubscription": "Vaheta tellimust",
"resetTrafficTitle": "Lähtesta liiklus",
"resetTrafficMessage": "Kuupaketi liikluse lähtestamise näide: lähtesta järgmise tsükli liiklus igakuiselt ja tellimuse kehtivusaeg edasi lükatakse {currentTime} kuni {newTime}",
"reset": "Lähtesta",
"deviceLimit": "Seadmete limiit: {count}",
"trafficUsage": "Kasutatud: {used} / {total}",
"trafficProgress": {
"title": "Liikluse kasutamine",
"unlimited": "Piiramatu liiklus",
"limited": "Kasutatud liiklus"
},
"copySuccess": "Kopeeritud",
"notAvailable": "Pole saadaval",
"willBeDeleted": "kustutatakse",
"deleteAccountWarning": "Konto kustutamine on püsiv. Kui teie konto on kustutatud, ei saa te enam ühtegi funktsiooni kasutada. Jätkata?",
"requestDelete": "Taotle kustutamist"
},
"setting": {
"title": "Seaded",
"vpnConnection": "VPN ühendus",
"general": "Üldine",
"autoConnect": "Automaatne ühendus",
"routeRule": "Marsruudi reeglid",
"countrySelector": "Vali riik",
"appearance": "Välimus",
"notifications": "Teavitused",
"helpImprove": "Aita meil paremaks muutuda",
"helpImproveSubtitle": "Aita meil paremaks muutuda alapealkiri",
"requestDeleteAccount": "Taotle konto kustutamist",
"goToDelete": "Mine kustutama",
"rateUs": "Hinda meid App Store'is",
"iosRating": "iOS hinnang",
"version": "Versioon",
"switchLanguage": "Vaheta keelt",
"system": "Süsteem",
"light": "Hele",
"dark": "Tume",
"vpnModeSmart": "Nutikas režiim",
"mode": "Väljundrežiim",
"connectionTypeGlobal": "Globaalne puhverserver",
"connectionTypeGlobalRemark": "Lubamisel suunatakse kogu liiklus puhverserveri kaudu",
"connectionTypeRule": "Nutikas puhverserver",
"connectionTypeRuleRemark": "Lubamisel, kui väljundrežiim on seatud nutikaks puhverserveriks, valitud riigi põhjal, jaotatakse liiklus automaatselt: kohalikud IP-d/domeenid ühenduvad otse, välismaised päringud suunatakse puhverserverisse",
"connectionTypeDirect": "Otsene ühendus",
"connectionTypeDirectRemark": "Lubamisel suunatakse kogu liiklus otse",
"smartMode": "Nutikas režiim",
"secureMode": "Turvaline režiim"
},
"statistics": {
"title": "Statistika",
"vpnStatus": "VPN olek",
"ipAddress": "IP aadress",
"connectionTime": "Ühenduse aeg",
"protocol": "Protokoll",
"weeklyProtectionTime": "Iganädalane kaitseaeg",
"currentStreak": "Praegune seeria",
"highestStreak": "Parim seeria",
"longestConnection": "Pikim ühendus",
"days": "{days} päeva",
"daysOfWeek": {
"monday": "E",
"tuesday": "T",
"wednesday": "K",
"thursday": "N",
"friday": "R",
"saturday": "L",
"sunday": "P"
}
},
"message": {
"title": "Teavitused",
"system": "Süsteemi sõnumid",
"promotion": "Kampaania sõnumid"
},
"invite": {
"title": "Kutsu sõbrad",
"progress": "Kutse edenemine",
"inviteStats": "Kutse statistika",
"registers": "Registreeritud",
"totalCommission": "Kogu komisjonitasu",
"rewardDetails": "Tasu üksikasjad >",
"steps": "Kutse Sammud",
"inviteFriend": "Kutsu Sõbrad",
"acceptInvite": "Sõbrad aktsepteerivad kutset\nTee tellimus ja registreeru",
"getReward": "Saada Tasu",
"shareLink": "Jaga Linki",
"shareQR": "Jaga QR-koodi",
"rules": "Kutse Reeglid",
"rule1": "1. Saad kutsuda sõpru meiega liituma, jagades oma erilist kutselinki või kutsekoodi.",
"rule2": "2. Pärast seda, kui sõbrad on registreerunud ja sisse loginud, krediteeritakse kutsetasud automaatselt teie kontole.",
"pending": "Ootel",
"processing": "Töötlemisel",
"success": "Õnnestus",
"expired": "Aegunud",
"myInviteCode": "Minu Kutsekood",
"inviteCodeCopied": "Kutsekood kopeeritud lõikelauale",
"close": "Sulge",
"saveQRCode": "Salvesta QR-kood",
"qrCodeSaved": "QR-kood salvestatud",
"copiedToClipboard": "Kopeeritud lõikelauale",
"getInviteCodeFailed": "Kutsekoodi hankimine ebaõnnestus, proovi hiljem uuesti",
"generateQRCodeFailed": "QR-koodi genereerimine ebaõnnestus, proovi hiljem uuesti",
"generateShareLinkFailed": "Jagamislinki genereerimine ebaõnnestus, proovi hiljem uuesti"
},
"purchaseMembership": {
"purchasePackage": "Paketi Ostmine",
"noData": "Saadaval pakette pole",
"myAccount": "Minu Konto",
"selectPackage": "Vali Pakett",
"packageDescription": "Paketi Kirjeldus",
"paymentMethod": "Makseviis",
"cancelAnytime": "Saad igal ajal rakenduses tühistada",
"startSubscription": "Alusta Tellimust",
"renewNow": "Uuenda Kohe",
"month": "{months} kuud",
"year": "{years} aastat",
"day": "{days} päeva",
"unlimitedTraffic": "Piiramatu Liiklus",
"unlimitedDevices": "Piiramatu Seadmete Arv",
"devices": "{count} seadet",
"features": "Paketi Funktsioonid",
"expand": "Laienda",
"collapse": "Sulge",
"confirmPurchase": "Kinnita Ost",
"confirmPurchaseDesc": "Kas olete kindel, et soovite selle paketi osta?"
},
"orderStatus": {
"title": "Tellimuse olek",
"pending": {
"title": "Makse ootel",
"description": "Palun täitke makse"
},
"paid": {
"title": "Makse vastu võetud",
"description": "Tellimust töödeldakse"
},
"success": {
"title": "Palju õnne! Makse õnnestus",
"description": "Teie pakett on edukalt ostetud"
},
"closed": {
"title": "Tellimus suletud",
"description": "Palun esitage uus tellimus"
},
"failed": {
"title": "Makse ebaõnnestus",
"description": "Palun proovige makset uuesti"
},
"unknown": {
"title": "Tundmatu olek",
"description": "Palun võtke ühendust klienditeenindusega"
},
"checkFailed": {
"title": "Kontroll ebaõnnestus",
"description": "Palun proovige hiljem uuesti"
},
"initial": {
"title": "Makset töödeldakse",
"description": "Palun oodake, kui töötleme teie makset"
}
},
"home": {
"welcome": "Tere tulemast BearVPN-i",
"disconnected": "Ühendus katkestatud",
"connecting": "Ühendumine",
"connected": "Ühendatud",
"disconnecting": "Ühenduse katkestamine",
"currentConnectionTitle": "Praegune ühendus",
"switchNode": "Vaheta sõlme",
"timeout": "Aegumine",
"loading": "Laadimine...",
"error": "Laadimise viga",
"checkNetwork": "Kontrollige võrguühendust ja proovige uuesti",
"retry": "Proovi uuesti",
"connectionSectionTitle": "Ühendusviis",
"dedicatedServers": "Pühendatud serverid",
"countryRegion": "Riik/Regioon",
"serverListTitle": "Pühendatud serverite grupid",
"nodeListTitle": "Kõik sõlmed",
"countryListTitle": "Riikide/Regioonide nimekiri",
"noServers": "Saadaval pole ühtegi serverit",
"noNodes": "Saadaval pole ühtegi sõlme",
"noRegions": "Saadaval pole ühtegi regiooni",
"subscriptionDescription": "Hankige premium juurdepääs kiirele globaalsele võrgustikule",
"subscribe": "Telli",
"trialPeriod": "Tere tulemast Premium prooviversiooni",
"remainingTime": "Järelejäänud aeg",
"trialExpired": "Prooviaeg on lõppenud, ühendus katkestatud",
"subscriptionExpired": "Tellimus on aegunud, ühendus katkestatud",
"subscriptionUpdated": "Tellimus värskendatud",
"subscriptionUpdatedMessage": "Teie tellimuse teave on värskendatud, värskendage viimase oleku vaatamiseks",
"trialStatus": "Proovi olek",
"trialing": "Proovimas",
"trialEndMessage": "Prooviaja lõppedes ei saa enam kasutada",
"lastDaySubscriptionStatus": "Tellimus aegub varsti",
"lastDaySubscriptionMessage": "Aegub varsti",
"subscriptionEndMessage": "Tellimuse lõppedes ei saa enam kasutada",
"trialTimeWithDays": "{days}p {hours}t {minutes}m {seconds}s",
"trialTimeWithHours": "{hours}t {minutes}m {seconds}s",
"trialTimeWithMinutes": "{minutes}m {seconds}s",
"refreshLatency": "Värskenda latentsust",
"testLatency": "Testi latentsust",
"testing": "Latentsuse testimine",
"refreshLatencyDesc": "Värskenda kõigi sõlmede latentsust",
"testAllNodesLatency": "Testi kõigi sõlmede võrgu latentsust",
"autoSelect": "Automaatne valik",
"selected": "Valitud"
},
"dialog": {
"confirm": "Kinnita",
"cancel": "Tühista",
"ok": "OK"
},
"splash": {
"appName": "BearVPN",
"slogan": "Kiire globaalne võrgustik",
"initializing": "Initsialiseerimine...",
"networkConnectionFailure": "Võrguühenduse viga, kontrollige ja proovige uuesti",
"retry": "Proovi uuesti",
"networkPermissionFailed": "Võrguõiguse hankimine ebaõnnestus",
"initializationFailed": "Initsialiseerimine ebaõnnestus"
},
"network": {
"status": {
"connected": "Ühendatud",
"disconnected": "Ühendus katkestatud",
"connecting": "Ühendumine...",
"disconnecting": "Ühenduse katkestamine...",
"reconnecting": "Ühenduse taastamine...",
"failed": "Ühenduse viga"
},
"permission": {
"title": "Võrguõigus",
"description": "VPN teenuse pakkumiseks on vaja võrguõigust",
"goToSettings": "Mine seadetesse",
"cancel": "Tühista"
}
},
"update": {
"title": "Uuendus saadaval",
"content": "Uuendada nüüd?",
"updateNow": "Uuenda nüüd",
"updateLater": "Hiljem",
"defaultContent": "1. Rakenduse jõudluse optimeerimine\n2. Teadaolevate probleemide parandamine\n3. Kasutajamugavuse parandamine"
},
"country": {
"cn": "Hiina",
"ir": "Iraan",
"af": "Afganistan",
"ru": "Venemaa",
"id": "Indoneesia",
"tr": "Türgi",
"br": "Brasiilia"
},
"error": {
"200": "Edukas",
"500": "Serveri sisemine viga",
"10001": "Andmebaasi päringu viga",
"10002": "Andmebaasi uuendamise viga",
"10003": "Andmebaasi sisestamise viga",
"10004": "Andmebaasi kustutamise viga",
"20001": "Kasutaja on juba olemas",
"20002": "Kasutajat pole olemas",
"20003": "Vale kasutaja parool",
"20004": "Kasutaja on keelatud",
"20005": "Ebapiisav saldo",
"20006": "Registreerimine peatatud",
"20007": "Telegram pole seotud",
"20008": "Kasutaja pole OAuth-i seostanud",
"20009": "Vale kutsekood",
"30001": "Sõlm on juba olemas",
"30002": "Sõlme pole olemas",
"30003": "Sõlme grupp on juba olemas",
"30004": "Sõlme gruppi pole olemas",
"30005": "Sõlme grupp pole tühi",
"400": "Parameetri viga",
"40002": "Kasutaja token on tühi",
"40003": "Vale kasutaja token",
"40004": "Kasutaja token on aegunud",
"40005": "Te pole sisse logitud",
"401": "Liiga palju päringuid",
"50001": "Kupongi pole olemas",
"50002": "Kupong on juba kasutatud",
"50003": "Kupong ei vasta",
"60001": "Tellimus on aegunud",
"60002": "Tellimus pole saadaval",
"60003": "Kasutajal on juba tellimus",
"60004": "Tellimus on juba kasutatud",
"60005": "Üksiku tellimuse režiimi limiit ületatud",
"60006": "Tellimuse kvoodi limiit",
"70001": "Vale kinnituskood",
"80001": "Järjekorda lisamise viga",
"90001": "Silumisrežiim on lubatud",
"90002": "SMS-i saatmise viga",
"90003": "SMS funktsioon pole lubatud",
"90004": "E-posti funktsioon pole lubatud",
"90005": "Toetamata sisselogimise meetod",
"90006": "Autentifikaator ei toeta seda meetodit",
"90007": "Telefoni riigi kood on tühi",
"90008": "Parool on tühi",
"90009": "Riigi kood on tühi",
"90010": "Parool või kinnituskood on vajalik",
"90011": "E-post on juba olemas",
"90012": "Telefoninumber on juba olemas",
"90013": "Seade on juba olemas",
"90014": "Vale telefoninumber",
"90015": "See konto on täna saavutanud saatmise limiidi",
"90017": "Seadet pole olemas",
"90018": "Kasutaja ID ei vasta",
"61001": "Tellimust pole olemas",
"61002": "Makseviisi ei leitud",
"61003": "Vale tellimuse olek",
"61004": "Ebapiisav lähtestamise periood",
"61005": "Kasutamata liiklus on olemas"
},
"tray": {
"open_dashboard": "Ava armatuurlaud",
"copy_to_terminal": "Kopeeri terminali",
"exit_app": "Välju rakendusest"
}
}

View File

@ -6,6 +6,7 @@
"codeSent": "{account}に6桁のコードを送信しました。30分以内に入力してください。",
"back": "戻る",
"enterEmailOrPhone": "メールアドレスまたは電話番号を入力",
"enterEmail": "Please enter email address",
"enterCode": "認証コードを入力してください",
"enterPassword": "パスワードを入力してください",
"reenterPassword": "パスワードを再入力してください",

View File

@ -0,0 +1,441 @@
{
"login": {
"welcome": "BearVPNへようこそ",
"verifyPhone": "電話番号を認証",
"verifyEmail": "メールアドレスを認証",
"codeSent": "{account}に6桁のコードを送信しました。30分以内に入力してください。",
"back": "戻る",
"enterEmailOrPhone": "メールアドレスまたは電話番号を入力",
"enterCode": "認証コードを入力してください",
"enterPassword": "パスワードを入力してください",
"reenterPassword": "パスワードを再入力してください",
"forgotPassword": "パスワードをお忘れの方",
"codeLogin": "認証コードでログイン",
"passwordLogin": "パスワードでログイン",
"agreeTerms": "ログイン/アカウント作成,に同意します",
"termsOfService": "利用規約",
"privacyPolicy": "プライバシーポリシー",
"next": "次へ",
"registerNow": "今すぐ登録",
"setAndLogin": "設定してログイン",
"enterAccount": "アカウントを入力してください",
"passwordMismatch": "2回のパスワード入力が一致しません",
"sendCode": "認証コードを送信",
"codeSentCountdown": "認証コード送信済み {seconds}秒",
"and": "および",
"enterInviteCode": "招待コードを入力(任意)",
"registerSuccess": "登録成功"
},
"failure": {
"unexpected": "予期せぬエラー",
"clash": {
"unexpected": "予期せぬエラー",
"core": "Clashエラー ${reason}"
},
"singbox": {
"unexpected": "予期せぬサービスエラー",
"serviceNotRunning": "サービスが実行されていません",
"missingPrivilege": "権限がありません",
"missingPrivilegeMsg": "VPNモードには管理者権限が必要です。管理者として再起動するか、サービスモードを変更してください",
"missingGeoAssets": "GEOリソースファイルがありません",
"missingGeoAssetsMsg": "GEOリソースファイルがありません。アクティブなリソースを変更するか、設定で選択したリソースをダウンロードしてください。",
"invalidConfigOptions": "設定オプションが無効です",
"invalidConfig": "無効な設定",
"create": "サービス作成エラー",
"start": "サービス起動エラー"
},
"connectivity": {
"unexpected": "予期せぬ失敗",
"missingVpnPermission": "VPN権限がありません",
"missingNotificationPermission": "通知権限がありません",
"core": "コアエラー"
},
"profiles": {
"unexpected": "予期せぬエラー",
"notFound": "プロファイルが見つかりません",
"invalidConfig": "無効な設定",
"invalidUrl": "無効なURL"
},
"connection": {
"unexpected": "予期せぬ接続エラー",
"timeout": "接続タイムアウト",
"badResponse": "不正なレスポンス",
"connectionError": "接続エラー",
"badCertificate": "無効な証明書"
},
"geoAssets": {
"unexpected": "予期せぬエラー",
"notUpdate": "利用可能な更新はありません",
"activeNotFound": "アクティブなGEOリソースファイルが見つかりません"
}
},
"userInfo": {
"title": "マイ情報",
"bindingTip": "メール/電話番号が未登録です",
"myAccount": "マイアカウント",
"balance": "残高",
"noValidSubscription": "有効なサブスクリプションがありません",
"subscribeNow": "今すぐ購読",
"shortcuts": "ショートカット",
"adBlock": "広告ブロック",
"dnsUnlock": "DNSアンロック",
"contactUs": "お問い合わせ",
"others": "その他",
"logout": "ログアウト",
"logoutConfirmTitle": "ログアウト",
"logoutConfirmMessage": "ログアウトしますか?",
"logoutCancel": "キャンセル",
"vpnWebsite": "VPN公式サイト",
"telegram": "Telegram",
"mail": "メール",
"phone": "電話",
"customerService": "カスタマーサービス",
"workOrder": "チケット送信",
"pleaseLogin": "ログインしてください",
"subscriptionValid": "サブスクリプション有効",
"startTime": "開始時間:",
"expireTime": "有効期限:",
"loginNow": "今すぐログイン",
"trialPeriod": "トライアル期間",
"remainingTime": "残り時間",
"trialExpired": "トライアル期間が終了しました",
"subscriptionExpired": "サブスクリプションが期限切れです",
"copySuccess": "コピーしました",
"notAvailable": "利用不可",
"willBeDeleted": "削除されます",
"deleteAccountWarning": "アカウントの削除は永久的です。アカウントを削除すると、すべての機能が使用できなくなります。続行しますか?",
"requestDelete": "削除をリクエスト",
"deviceLimit": "デバイス制限:{count}",
"reset": "リセット",
"trafficUsage": "トラフィック:{used} / {total}",
"trafficProgress": {
"title": "トラフィック使用状況",
"unlimited": "無制限",
"limited": "使用済み"
},
"switchSubscription": "サブスクリプションの切り替え",
"resetTrafficTitle": "トラフィックリセット",
"resetTrafficMessage": "月間プランのトラフィックリセット例:次のサイクルのトラフィックを月次でリセットし、サブスクリプションの有効期限が{currentTime}から{newTime}に繰り上げられます",
"trialStatus": "トライアル状態",
"trialing": "トライアル中",
"trialEndMessage": "トライアル期間終了後は使用できなくなります",
"lastDaySubscriptionStatus": "サブスクリプション期限切れ間近",
"lastDaySubscriptionMessage": "期限切れ間近",
"subscriptionEndMessage": "サブスクリプション終了後は使用できなくなります",
"trialTimeWithDays": "{days}日{hours}時間{minutes}分{seconds}秒",
"trialTimeWithHours": "{hours}時間{minutes}分{seconds}秒",
"trialTimeWithMinutes": "{minutes}分{seconds}秒",
"refreshLatency": "レイテンシーを更新",
"testLatency": "レイテンシーテスト",
"testing": "レイテンシーテスト中",
"refreshLatencyDesc": "すべてのノードのレイテンシーを更新",
"testAllNodesLatency": "すべてのノードのネットワークレイテンシーをテスト",
"autoSelect": "自動選択",
"selected": "選択済み"
},
"setting": {
"title": "設定",
"vpnConnection": "VPN接続",
"countrySelector": "国を選択",
"general": "一般",
"autoConnect": "自動接続",
"routeRule": "ルーティングルール",
"appearance": "外観",
"notifications": "通知",
"helpImprove": "改善にご協力ください",
"helpImproveSubtitle": "改善にご協力くださいサブタイトル",
"requestDeleteAccount": "アカウント削除をリクエスト",
"goToDelete": "削除へ",
"rateUs": "App Storeで評価する",
"iosRating": "iOS評価",
"version": "バージョン",
"switchLanguage": "言語を切り替え",
"system": "システム",
"light": "ライト",
"dark": "ダーク",
"vpnModeSmart": "スマートモード",
"mode": "アウトバウンドモード",
"connectionTypeGlobal": "グローバルプロキシ",
"connectionTypeGlobalRemark": "有効時、すべてのトラフィックはプロキシ経由でルーティングされます",
"connectionTypeRule": "スマートプロキシ",
"connectionTypeRuleRemark": "[アウトバウンドモード]が[スマートプロキシ]に設定されている場合、システムは選択された国に基づいて国内と海外のトラフィックを自動的に分割します国内IP/ドメインは直接接続、海外リクエストはプロキシ経由でアクセス",
"connectionTypeDirect": "ダイレクト接続",
"connectionTypeDirectRemark": "有効時、すべてのトラフィックはプロキシをバイパスします",
"smartMode": "スマートモード",
"secureMode": "セキュアモード"
},
"statistics": {
"title": "統計",
"vpnStatus": "VPNステータス",
"ipAddress": "IPアドレス",
"connectionTime": "接続時間",
"protocol": "プロトコル",
"weeklyProtectionTime": "週間保護時間",
"currentStreak": "現在の連続記録",
"highestStreak": "最高記録",
"longestConnection": "最長接続時間",
"days": "{days}日",
"daysOfWeek": {
"monday": "月",
"tuesday": "火",
"wednesday": "水",
"thursday": "木",
"friday": "金",
"saturday": "土",
"sunday": "日"
}
},
"message": {
"title": "通知",
"system": "システムメッセージ",
"promotion": "プロモーションメッセージ"
},
"invite": {
"title": "友達を招待",
"progress": "招待の進捗",
"inviteStats": "招待統計",
"registers": "登録済み",
"totalCommission": "総報酬",
"rewardDetails": "報酬の詳細 >",
"steps": "招待の手順",
"inviteFriend": "友達を招待",
"acceptInvite": "友達が招待を受け入れ\n注文して登録",
"getReward": "報酬を獲得",
"shareLink": "リンクを共有",
"shareQR": "QRコードを共有",
"rules": "招待ルール",
"rule1": "1. 専用の招待リンクまたは招待コードを共有して、友達を招待できます。",
"rule2": "2. 友達が登録とログインを完了すると、招待報酬が自動的にアカウントに付与されます。",
"pending": "保留中",
"processing": "処理中",
"success": "成功",
"expired": "期限切れ",
"myInviteCode": "招待コード",
"inviteCodeCopied": "招待コードをクリップボードにコピーしました",
"close": "閉じる",
"saveQRCode": "QRコードを保存",
"qrCodeSaved": "QRコードを保存しました",
"copiedToClipboard": "クリップボードにコピーしました",
"getInviteCodeFailed": "招待コードの取得に失敗しました。後ほど再試行してください",
"generateQRCodeFailed": "QRコードの生成に失敗しました。後ほど再試行してください",
"generateShareLinkFailed": "共有リンクの生成に失敗しました。後ほど再試行してください"
},
"purchaseMembership": {
"purchasePackage": "パッケージ購入",
"noData": "利用可能なパッケージはありません",
"myAccount": "マイアカウント",
"selectPackage": "パッケージを選択",
"packageDescription": "パッケージ説明",
"paymentMethod": "支払い方法",
"cancelAnytime": "アプリでいつでもキャンセル可能",
"startSubscription": "サブスクリプションを開始",
"renewNow": "今すぐ更新",
"month": "{months}ヶ月",
"year": "{years}年",
"day": "{days}日",
"unlimitedTraffic": "無制限トラフィック",
"unlimitedDevices": "無制限デバイス",
"devices": "{count}台",
"features": "パッケージ機能",
"expand": "展開",
"collapse": "折りたたむ",
"confirmPurchase": "購入を確認",
"confirmPurchaseDesc": "このパッケージを購入してもよろしいですか?",
"subscriptionPrivacyInfo": "サブスクリプションとプライバシー情報"
},
"orderStatus": {
"title": "注文状態",
"pending": {
"title": "支払い待ち",
"description": "支払いを完了してください"
},
"paid": {
"title": "支払い完了",
"description": "注文を処理中です"
},
"success": {
"title": "おめでとうございます!支払い成功",
"description": "パッケージの購入が完了しました"
},
"closed": {
"title": "注文キャンセル",
"description": "新規注文をお願いします"
},
"failed": {
"title": "支払い失敗",
"description": "支払いを再試行してください"
},
"unknown": {
"title": "不明な状態",
"description": "カスタマーサービスにお問い合わせください"
},
"checkFailed": {
"title": "確認失敗",
"description": "後でもう一度お試しください"
},
"initial": {
"title": "支払い処理中",
"description": "支払い処理中です。お待ちください"
}
},
"home": {
"welcome": "BearVPNへようこそ",
"disconnected": "未接続",
"connecting": "接続中",
"connected": "接続済み",
"disconnecting": "切断中",
"currentConnectionTitle": "現在の接続",
"switchNode": "ノード切替",
"timeout": "タイムアウト",
"loading": "読み込み中...",
"error": "読み込みエラー",
"checkNetwork": "ネットワーク接続を確認して再試行してください",
"retry": "再試行",
"connectionSectionTitle": "接続方法",
"dedicatedServers": "専用サーバー",
"countryRegion": "国/地域",
"serverListTitle": "専用サーバーグループ",
"nodeListTitle": "全ノード",
"countryListTitle": "国/地域リスト",
"noServers": "利用可能なサーバーがありません",
"noNodes": "利用可能なノードがありません",
"noRegions": "利用可能な地域がありません",
"subscriptionDescription": "プレミアムアクセスで高速グローバルネットワークを利用",
"subscribe": "購読する",
"trialPeriod": "プレミアムトライアルへようこそ",
"remainingTime": "残り時間",
"trialExpired": "トライアル期間が終了し、接続が切断されました",
"subscriptionExpired": "サブスクリプションが期限切れとなり、接続が切断されました",
"subscriptionUpdated": "サブスクリプションが更新されました",
"subscriptionUpdatedMessage": "サブスクリプション情報が更新されました。最新の状態を確認するには更新してください",
"trialStatus": "トライアル状態",
"trialing": "トライアル中",
"trialEndMessage": "トライアル期間終了後は使用できなくなります",
"lastDaySubscriptionStatus": "サブスクリプション期限切れ間近",
"lastDaySubscriptionMessage": "期限切れ間近",
"subscriptionEndMessage": "サブスクリプション終了後は使用できなくなります",
"trialTimeWithDays": "{days}日{hours}時間{minutes}分{seconds}秒",
"trialTimeWithHours": "{hours}時間{minutes}分{seconds}秒",
"trialTimeWithMinutes": "{minutes}分{seconds}秒",
"refreshLatency": "レイテンシー更新",
"testLatency": "レイテンシーテスト",
"testing": "レイテンシーテスト中",
"refreshLatencyDesc": "全ノードのレイテンシーを更新",
"testAllNodesLatency": "全ノードのネットワークレイテンシーをテスト",
"autoSelect": "自動選択",
"selected": "選択済み"
},
"dialog": {
"confirm": "確認",
"cancel": "キャンセル",
"ok": "OK",
"iKnow": "分かりました"
},
"splash": {
"appName": "BearVPN",
"slogan": "高速グローバルネットワーク",
"initializing": "初期化中...",
"networkConnectionFailure": "ネットワーク接続エラー、確認して再試行してください",
"retry": "再試行",
"networkPermissionFailed": "ネットワーク権限の取得に失敗しました",
"initializationFailed": "初期化に失敗しました"
},
"network": {
"status": {
"connected": "接続済み",
"disconnected": "未接続",
"connecting": "接続中...",
"disconnecting": "切断中...",
"reconnecting": "再接続中...",
"failed": "接続エラー"
},
"permission": {
"title": "ネットワーク権限",
"description": "VPNサービスを提供するにはネットワーク権限が必要です",
"goToSettings": "設定へ",
"cancel": "キャンセル"
}
},
"update": {
"title": "アップデートが利用可能",
"content": "今すぐアップデートしますか?",
"updateNow": "今すぐアップデート",
"updateLater": "後で",
"defaultContent": "1. アプリのパフォーマンス最適化\n2. 既知の問題の修正\n3. ユーザー体験の向上"
},
"country": {
"cn": "中国",
"ir": "イラン",
"af": "アフガニスタン",
"ru": "ロシア",
"id": "インドネシア",
"tr": "トルコ",
"br": "ブラジル"
},
"error": {
"200": "成功",
"500": "サーバー内部エラー",
"10001": "データベースクエリエラー",
"10002": "データベース更新エラー",
"10003": "データベース挿入エラー",
"10004": "データベース削除エラー",
"20001": "ユーザーは既に存在します",
"20002": "ユーザーが存在しません",
"20003": "ユーザーパスワードが間違っています",
"20004": "ユーザーは無効化されています",
"20005": "残高不足",
"20006": "登録は停止されています",
"20007": "Telegramが未連携です",
"20008": "ユーザーがOAuthを連携していません",
"20009": "招待コードが間違っています",
"30001": "ノードは既に存在します",
"30002": "ノードが存在しません",
"30003": "ノードグループは既に存在します",
"30004": "ノードグループが存在しません",
"30005": "ノードグループは空ではありません",
"400": "パラメータエラー",
"40002": "ユーザートークンが空です",
"40003": "ユーザートークンが無効です",
"40004": "ユーザートークンの有効期限が切れています",
"40005": "ログインしていません",
"401": "リクエストが多すぎます",
"50001": "クーポンが存在しません",
"50002": "クーポンは既に使用されています",
"50003": "クーポンが一致しません",
"60001": "サブスクリプションの有効期限が切れています",
"60002": "サブスクリプションは利用できません",
"60003": "ユーザーは既にサブスクリプションを持っています",
"60004": "サブスクリプションは既に使用されています",
"60005": "単一サブスクリプションモードの制限を超えています",
"60006": "サブスクリプションクォータ制限",
"70001": "認証コードが間違っています",
"80001": "キューイングエラー",
"90001": "デバッグモードが有効です",
"90002": "SMS送信エラー",
"90003": "SMS機能が有効になっていません",
"90004": "メール機能が有効になっていません",
"90005": "サポートされていないログイン方法",
"90006": "認証器がこの方法をサポートしていません",
"90007": "電話国番号が空です",
"90008": "パスワードが空です",
"90009": "国番号が空です",
"90010": "パスワードまたは認証コードが必要です",
"90011": "メールアドレスは既に存在します",
"90012": "電話番号は既に存在します",
"90013": "デバイスは既に存在します",
"90014": "電話番号が間違っています",
"90015": "このアカウントは本日の送信制限に達しました",
"90017": "デバイスが存在しません",
"90018": "ユーザーIDが一致しません",
"61001": "注文が存在しません",
"61002": "支払い方法が見つかりません",
"61003": "注文状態が間違っています",
"61004": "リセット期間が不足しています",
"61005": "未使用のトラフィックが存在します"
},
"tray": {
"open_dashboard": "ダッシュボードを開く",
"copy_to_terminal": "ターミナルにコピー",
"exit_app": "アプリを終了"
}
}

View File

@ -6,6 +6,7 @@
"codeSent": "6-значный код отправлен на {account}. Введите его в течение 30 минут.",
"back": "Назад",
"enterEmailOrPhone": "Введите email или номер телефона",
"enterEmail": "Please enter email address",
"enterCode": "Введите код подтверждения",
"enterPassword": "Введите пароль",
"reenterPassword": "Повторите пароль",

View File

@ -0,0 +1,442 @@
{
"login": {
"welcome": "Добро пожаловать в BearVPN!",
"verifyPhone": "Подтвердите номер телефона",
"verifyEmail": "Подтвердите email",
"codeSent": "6-значный код отправлен на {account}. Введите его в течение 30 минут.",
"back": "Назад",
"enterEmailOrPhone": "Введите email или номер телефона",
"enterCode": "Введите код подтверждения",
"enterPassword": "Введите пароль",
"reenterPassword": "Повторите пароль",
"forgotPassword": "Забыли пароль",
"codeLogin": "Вход по коду",
"passwordLogin": "Вход по паролю",
"agreeTerms": "Вход/Создать аккаунт, я соглашаюсь с",
"termsOfService": "Условиями использования",
"privacyPolicy": "Политикой конфиденциальности",
"next": "Далее",
"registerNow": "Зарегистрироваться",
"setAndLogin": "Установить и войти",
"enterAccount": "Введите аккаунт",
"passwordMismatch": "Два введенных пароля не совпадают",
"sendCode": "Отправить код",
"codeSentCountdown": "Код отправлен {seconds}с",
"and": "и",
"enterInviteCode": "Введите код приглашения (необязательно)",
"registerSuccess": "Регистрация успешна"
},
"failure": {
"unexpected": "Непредвиденная ошибка",
"clash": {
"unexpected": "Непредвиденная ошибка",
"core": "Ошибка Clash ${reason}"
},
"singbox": {
"unexpected": "Непредвиденная ошибка сервиса",
"serviceNotRunning": "Сервис не запущен",
"missingPrivilege": "Отсутствуют привилегии",
"missingPrivilegeMsg": "Режим VPN требует прав администратора. Перезапустите приложение с правами администратора или измените режим сервиса",
"missingGeoAssets": "Отсутствуют GEO-ресурсы",
"missingGeoAssetsMsg": "Отсутствуют файлы GEO-ресурсов. Измените активные ресурсы или загрузите выбранные ресурсы в настройках.",
"invalidConfigOptions": "Недопустимые параметры конфигурации",
"invalidConfig": "Недопустимая конфигурация",
"create": "Ошибка создания сервиса",
"start": "Ошибка запуска сервиса"
},
"connectivity": {
"unexpected": "Непредвиденный сбой",
"missingVpnPermission": "Отсутствует разрешение VPN",
"missingNotificationPermission": "Отсутствует разрешение на уведомления",
"core": "Ошибка ядра"
},
"profiles": {
"unexpected": "Непредвиденная ошибка",
"notFound": "Профиль не найден",
"invalidConfig": "Недопустимая конфигурация",
"invalidUrl": "Недопустимый URL"
},
"connection": {
"unexpected": "Непредвиденная ошибка подключения",
"timeout": "Тайм-аут подключения",
"badResponse": "Некорректный ответ",
"connectionError": "Ошибка подключения",
"badCertificate": "Недействительный сертификат"
},
"geoAssets": {
"unexpected": "Непредвиденная ошибка",
"notUpdate": "Нет доступных обновлений",
"activeNotFound": "Активные GEO-ресурсы не найдены"
}
},
"userInfo": {
"title": "Моя информация",
"bindingTip": "Email/телефон не привязаны",
"myAccount": "Мой аккаунт",
"balance": "Баланс",
"noValidSubscription": "Нет активной подписки",
"subscribeNow": "Подписаться сейчас",
"shortcuts": "Ярлыки",
"adBlock": "Блокировка рекламы",
"dnsUnlock": "Разблокировка DNS",
"contactUs": "Связаться с нами",
"others": "Прочее",
"logout": "Выйти",
"logoutConfirmTitle": "Выйти",
"logoutConfirmMessage": "Вы уверены, что хотите выйти?",
"logoutCancel": "Отмена",
"vpnWebsite": "Сайт VPN",
"telegram": "Telegram",
"mail": "Email",
"phone": "Телефон",
"customerService": "Служба поддержки",
"workOrder": "Создать заявку",
"pleaseLogin": "Пожалуйста, войдите",
"subscriptionValid": "Подписка активна",
"startTime": "Время начала:",
"expireTime": "Срок действия:",
"loginNow": "Войти сейчас",
"trialPeriod": "Добро пожаловать в пробную версию Premium",
"remainingTime": "Осталось времени",
"trialExpired": "Пробный период истёк, соединение разорвано",
"subscriptionExpired": "Подписка истекла, соединение разорвано",
"switchSubscription": "Сменить подписку",
"resetTrafficTitle": "Сброс трафика",
"resetTrafficMessage": "Пример сброса трафика месячного плана: сброс трафика следующего цикла ежемесячно, и срок действия подписки будет перенесен с {currentTime} на {newTime}",
"reset": "Сбросить",
"trafficUsage": "Использовано: {used} / {total}",
"trafficProgress": {
"title": "Использование трафика",
"unlimited": "Безлимитный трафик",
"limited": "Использованный трафик"
},
"deviceLimit": "Лимит устройств: {count}",
"copySuccess": "Скопировано успешно",
"notAvailable": "Недоступно",
"willBeDeleted": "Будет удалено",
"deleteAccountWarning": "Удаление аккаунта необратимо. После удаления аккаунта вы не сможете использовать все функции. Продолжить?",
"requestDelete": "Запросить удаление",
"trialStatus": "Статус пробного периода",
"trialing": "Пробный период",
"trialEndMessage": "После окончания пробного периода использование будет невозможно",
"lastDaySubscriptionStatus": "Подписка скоро истечет",
"lastDaySubscriptionMessage": "Скоро истечет",
"subscriptionEndMessage": "После окончания подписки использование будет невозможно",
"trialTimeWithDays": "{days}д {hours}ч {minutes}м {seconds}с",
"trialTimeWithHours": "{hours}ч {minutes}м {seconds}с",
"trialTimeWithMinutes": "{minutes}м {seconds}с",
"refreshLatency": "Обновить задержку",
"testLatency": "Тест задержки",
"testing": "Тестирование задержки",
"refreshLatencyDesc": "Обновить задержку всех узлов",
"testAllNodesLatency": "Тест сетевой задержки всех узлов",
"autoSelect": "Автоматический выбор",
"selected": "Выбрано"
},
"setting": {
"title": "Настройки",
"countrySelector": "Выбрать страну",
"vpnConnection": "VPN-подключение",
"general": "Общие",
"autoConnect": "Автоподключение",
"routeRule": "Правила маршрутизации",
"appearance": "Внешний вид",
"notifications": "Уведомления",
"helpImprove": "Помогите нам улучшить",
"helpImproveSubtitle": "Подзаголовок помощи в улучшении",
"requestDeleteAccount": "Запросить удаление аккаунта",
"goToDelete": "Перейти к удалению",
"rateUs": "Оцените нас в App Store",
"iosRating": "Оценка iOS",
"version": "Версия",
"switchLanguage": "Сменить язык",
"system": "Система",
"light": "Светлая",
"dark": "Тёмная",
"vpnModeSmart": "Умный режим",
"mode": "Исходящий режим",
"connectionTypeGlobal": "Глобальный прокси",
"connectionTypeGlobalRemark": "При включении весь трафик проходит через прокси",
"connectionTypeRule": "Умный прокси",
"connectionTypeRuleRemark": "Когда [Исходящий режим] установлен на [Умный прокси], система автоматически разделяет внутренний и международный трафик в соответствии с выбранной страной: внутренние IP/домены подключаются напрямую, а зарубежные запросы проходят через прокси",
"connectionTypeDirect": "Прямое подключение",
"connectionTypeDirectRemark": "При включении весь трафик обходит прокси",
"smartMode": "Умный режим",
"secureMode": "Безопасный режим"
},
"statistics": {
"title": "Статистика",
"vpnStatus": "Статус VPN",
"ipAddress": "IP-адрес",
"connectionTime": "Время подключения",
"protocol": "Протокол",
"weeklyProtectionTime": "Время защиты за неделю",
"currentStreak": "Текущая серия",
"highestStreak": "Рекорд",
"longestConnection": "Самое долгое подключение",
"days": "{days} дн.",
"daysOfWeek": {
"monday": "Пн",
"tuesday": "Вт",
"wednesday": "Ср",
"thursday": "Чт",
"friday": "Пт",
"saturday": "Сб",
"sunday": "Вс"
}
},
"message": {
"title": "Уведомления",
"system": "Системные сообщения",
"promotion": "Рекламные сообщения"
},
"invite": {
"title": "Пригласить друзей",
"progress": "Прогресс приглашений",
"inviteStats": "Статистика приглашений",
"registers": "Зарегистрировано",
"totalCommission": "Общая комиссия",
"rewardDetails": "Детали вознаграждения >",
"steps": "Шаги Приглашения",
"inviteFriend": "Пригласить Друзей",
"acceptInvite": "Друзья принимают приглашение",
"getReward": "Получить Вознаграждение",
"shareLink": "Поделиться Ссылкой",
"shareQR": "Поделиться QR-кодом",
"rules": "Правила Приглашения",
"rule1": "1. Вы можете приглашать друзей присоединиться к нам, поделившись своей уникальной ссылкой или кодом приглашения.",
"rule2": "2. После того, как друзья завершат регистрацию и войдут в систему, вознаграждения за приглашение будут автоматически зачислены на ваш счет.",
"pending": "В Ожидании",
"processing": "В Обработке",
"success": "Успешно",
"expired": "Истекло",
"myInviteCode": "Мой Код Приглашения",
"inviteCodeCopied": "Код приглашения скопирован в буфер обмена",
"close": "Закрыть",
"saveQRCode": "Сохранить QR-код",
"qrCodeSaved": "QR-код сохранен",
"copiedToClipboard": "Скопировано в буфер обмена",
"getInviteCodeFailed": "Не удалось получить код приглашения, попробуйте позже",
"generateQRCodeFailed": "Не удалось сгенерировать QR-код, попробуйте позже",
"generateShareLinkFailed": "Не удалось сгенерировать ссылку для общего доступа, попробуйте позже"
},
"purchaseMembership": {
"purchasePackage": "Купить Пакет",
"noData": "Нет доступных пакетов",
"myAccount": "Мой Аккаунт",
"selectPackage": "Выбрать Пакет",
"packageDescription": "Описание Пакета",
"paymentMethod": "Способ Оплаты",
"cancelAnytime": "Вы можете отменить в любое время в приложении",
"startSubscription": "Начать Подписку",
"subscriptionPrivacyInfo": "Информация о Подписке и Конфиденциальности",
"month": "{months} Месяц(ев)",
"year": "{years} Год(а)",
"day": "{days} день",
"unlimitedTraffic": "Безлимитный трафик",
"unlimitedDevices": "Безлимитные устройства",
"devices": "{count} устройств",
"trafficLimit": "Ограничение трафика",
"deviceLimit": "Лимит устройств: {count}",
"features": "Функции пакета",
"expand": "Развернуть",
"collapse": "Свернуть",
"confirmPurchase": "Подтвердить покупку",
"confirmPurchaseDesc": "Вы уверены, что хотите приобрести этот пакет?"
},
"orderStatus": {
"title": "Статус заказа",
"pending": {
"title": "Ожидание оплаты",
"description": "Пожалуйста, завершите оплату"
},
"paid": {
"title": "Оплата получена",
"description": "Обработка вашего заказа"
},
"success": {
"title": "Поздравляем! Оплата успешна",
"description": "Ваш пакет успешно приобретен"
},
"closed": {
"title": "Заказ закрыт",
"description": "Пожалуйста, сделайте новый заказ"
},
"failed": {
"title": "Ошибка оплаты",
"description": "Пожалуйста, попробуйте оплату снова"
},
"unknown": {
"title": "Неизвестный статус",
"description": "Пожалуйста, свяжитесь со службой поддержки"
},
"checkFailed": {
"title": "Ошибка проверки",
"description": "Пожалуйста, попробуйте позже"
},
"initial": {
"title": "Обработка оплаты",
"description": "Пожалуйста, подождите, пока мы обрабатываем вашу оплату"
}
},
"home": {
"welcome": "Добро пожаловать в BearVPN",
"disconnected": "Не подключено",
"connecting": "Подключение",
"connected": "Подключено",
"disconnecting": "Отключение",
"currentConnectionTitle": "Текущее подключение",
"switchNode": "Сменить узел",
"timeout": "Тайм-аут",
"loading": "Загрузка...",
"error": "Ошибка загрузки",
"checkNetwork": "Проверьте подключение к сети и повторите попытку",
"retry": "Повторить",
"connectionSectionTitle": "Способ подключения",
"dedicatedServers": "Выделенные серверы",
"countryRegion": "Страна/Регион",
"serverListTitle": "Группы выделенных серверов",
"nodeListTitle": "Все узлы",
"countryListTitle": "Список стран/регионов",
"noServers": "Нет доступных серверов",
"noNodes": "Нет доступных узлов",
"noRegions": "Нет доступных регионов",
"subscriptionDescription": "Подпишитесь для доступа к глобальной высокоскоростной сети",
"subscribe": "Подписаться сейчас",
"trialPeriod": "Добро пожаловать в пробную версию Premium",
"remainingTime": "Осталось времени",
"trialExpired": "Пробный период истёк, соединение разорвано",
"subscriptionExpired": "Подписка истекла, соединение разорвано",
"subscriptionUpdated": "Подписка обновлена",
"subscriptionUpdatedMessage": "Ваша информация о подписке обновлена, пожалуйста, обновите страницу для просмотра последнего статуса",
"trialStatus": "Статус пробного периода",
"trialing": "Пробный период",
"trialEndMessage": "После окончания пробного периода сервис будет недоступен",
"lastDaySubscriptionStatus": "Подписка скоро истекает",
"lastDaySubscriptionMessage": "Скоро истекает",
"subscriptionEndMessage": "После окончания подписки сервис будет недоступен",
"trialTimeWithDays": "{days}д {hours}ч {minutes}м {seconds}с",
"trialTimeWithHours": "{hours}ч {minutes}м {seconds}с",
"trialTimeWithMinutes": "{minutes}м {seconds}с",
"testLatency": "Тест задержки",
"testing": "Тестирование задержки",
"refreshLatency": "Обновить задержку",
"refreshLatencyDesc": "Обновить задержку всех узлов",
"testAllNodesLatency": "Тест сетевой задержки всех узлов",
"autoSelect": "Автоматический выбор",
"selected": "Выбрано"
},
"dialog": {
"confirm": "Подтвердить",
"cancel": "Отмена",
"ok": "OK",
"iKnow": "Я понял"
},
"update": {
"title": "Доступна новая версия",
"content": "Хотите обновить сейчас?",
"updateNow": "Обновить сейчас",
"later": "Позже",
"defaultContent": "1. Оптимизация производительности приложения\n2. Исправление известных проблем\n3. Улучшение пользовательского опыта"
},
"country": {
"cn": "Китай",
"ir": "Иран",
"af": "Афганистан",
"ru": "Россия",
"id": "Индонезия",
"tr": "Турция",
"br": "Бразилия"
},
"error": {
"200": "Успех",
"500": "Внутренняя ошибка сервера",
"10001": "Ошибка запроса базы данных",
"10002": "Ошибка обновления базы данных",
"10003": "Ошибка вставки в базу данных",
"10004": "Ошибка удаления из базы данных",
"20001": "Пользователь уже существует",
"20002": "Пользователь не существует",
"20003": "Ошибка пароля пользователя",
"20004": "Пользователь отключен",
"20005": "Недостаточно средств",
"20006": "Регистрация остановлена",
"20007": "Telegram не привязан",
"20008": "Пользователь не привязал метод OAuth",
"20009": "Ошибка кода приглашения",
"30001": "Узел уже существует",
"30002": "Узел не существует",
"30003": "Группа узлов уже существует",
"30004": "Группа узлов не существует",
"30005": "Группа узлов не пуста",
"400": "Ошибка параметра",
"40002": "Токен пользователя пуст",
"40003": "Токен пользователя недействителен",
"40004": "Срок действия токена пользователя истек",
"40005": "Недопустимый доступ",
"401": "Слишком много запросов",
"50001": "Купон не существует",
"50002": "Купон был использован",
"50003": "Купон не совпадает",
"60001": "Подписка истекла",
"60002": "Подписка недоступна",
"60003": "У пользователя уже есть подписка",
"60004": "Подписка уже использована",
"60005": "Режим одной подписки превышает лимит",
"60006": "Лимит квоты подписки",
"70001": "Ошибка кода подтверждения",
"80001": "Ошибка постановки в очередь",
"90001": "Режим отладки включен",
"90002": "Ошибка отправки SMS",
"90003": "SMS не включены",
"90004": "Электронная почта не включена",
"90005": "Неподдерживаемый метод входа",
"90006": "Аутентификатор не поддерживает этот метод",
"90007": "Код телефонного региона пуст",
"90008": "Пароль пуст",
"90009": "Код региона пуст",
"90010": "Требуется пароль или код подтверждения",
"90011": "Электронная почта уже существует",
"90012": "Телефон уже существует",
"90013": "Устройство уже существует",
"90014": "Ошибка номера телефона",
"90015": "Этот аккаунт достиг лимита отправки на сегодня",
"90017": "Устройство не существует",
"90018": "ID пользователя не совпадает",
"61001": "Заказ не существует",
"61002": "Способ оплаты не найден",
"61003": "Ошибка статуса заказа",
"61004": "Недостаточный период сброса",
"61005": "Существует неиспользованный трафик"
},
"tray": {
"open_dashboard": "Открыть панель",
"copy_to_terminal": "Копировать в терминал",
"exit_app": "Выйти из приложения"
},
"splash": {
"appName": "BearVPN",
"slogan": "Высокоскоростная глобальная сеть",
"initializing": "Инициализация...",
"networkConnectionFailure": "Ошибка подключения к сети, проверьте и повторите попытку",
"retry": "Повторить",
"networkPermissionFailed": "Не удалось получить разрешение на использование сети",
"initializationFailed": "Ошибка инициализации"
},
"network": {
"status": {
"connected": "Подключено",
"disconnected": "Отключено",
"connecting": "Подключение...",
"disconnecting": "Отключение...",
"reconnecting": "Переподключение...",
"failed": "Ошибка подключения"
},
"permission": {
"title": "Разрешение сети",
"description": "Требуется разрешение на использование сети для предоставления VPN-сервиса",
"goToSettings": "Перейти в настройки",
"cancel": "Отмена"
}
}
}

View File

@ -53,6 +53,7 @@
"codeSent": "已向 {account} 发送6位数代码。请在接下来的 30 分钟内输入。",
"back": "返回",
"enterEmailOrPhone": "输入邮箱或者手机号",
"enterEmail": "请输入电子邮箱",
"enterCode": "请输入验证码",
"enterPassword": "请输入密码",
"reenterPassword": "请再次输入密码",

View File

@ -6,6 +6,7 @@
"codeSent": "已向 {account} 發送6位數代碼。請在接下來的 30 分鐘內輸入。",
"back": "返回",
"enterEmailOrPhone": "輸入郵箱或者手機號",
"enterEmail": "請輸入電子郵箱",
"enterCode": "請輸入驗證碼",
"enterPassword": "請輸入密碼",
"reenterPassword": "請再次輸入密碼",

View File

@ -0,0 +1,426 @@
{
"login": {
"welcome": "歡迎使用 BearVPN!",
"verifyPhone": "驗證您的手機號",
"verifyEmail": "驗證您的郵箱",
"codeSent": "已向 {account} 發送6位數代碼。請在接下來的 30 分鐘內輸入。",
"back": "返回",
"enterEmailOrPhone": "輸入郵箱或者手機號",
"enterCode": "請輸入驗證碼",
"enterPassword": "請輸入密碼",
"reenterPassword": "請再次輸入密碼",
"forgotPassword": "忘記密碼",
"codeLogin": "驗證碼登錄",
"passwordLogin": "密碼登錄",
"agreeTerms": "登錄/創建賬戶,即表示我同意",
"termsOfService": "服務條款",
"privacyPolicy": "隱私政策",
"next": "下一步",
"registerNow": "立即註冊",
"setAndLogin": "設置並登錄",
"enterAccount": "請輸入賬戶",
"passwordMismatch": "兩次密碼輸入不一致",
"sendCode": "發送驗證碼",
"codeSentCountdown": "驗證碼已發送 {seconds}s",
"and": "和",
"enterInviteCode": "請輸入邀請碼(選填)",
"registerSuccess": "註冊成功"
},
"failure": {
"unexpected": "意外錯誤",
"clash": {
"unexpected": "意外錯誤",
"core": "Clash 錯誤 ${reason}"
},
"singbox": {
"unexpected": "意外服務錯誤",
"serviceNotRunning": "服務未運行",
"missingPrivilege": "缺少權限",
"missingPrivilegeMsg": "VPN 模式需要管理員權限。以管理員身份重新啟動應用程序或更改服務模式",
"missingGeoAssets": "缺失 GEO 資源文件",
"missingGeoAssetsMsg": "缺失 GEO 資源文件。請考慮更改激活的資源文件或在設置中下載所選資源文件。",
"invalidConfigOptions": "配置選項無效",
"invalidConfig": "無效配置",
"create": "服務創建錯誤",
"start": "服務啟動錯誤"
},
"connectivity": {
"unexpected": "意外失敗",
"missingVpnPermission": "缺少 VPN 權限",
"missingNotificationPermission": "缺少通知權限",
"core": "核心錯誤"
},
"profiles": {
"unexpected": "意外錯誤",
"notFound": "未找到配置文件",
"invalidConfig": "無效配置",
"invalidUrl": "網址無效"
},
"connection": {
"unexpected": "意外連接錯誤",
"timeout": "連接超時",
"badResponse": "錯誤響應",
"connectionError": "連接錯誤",
"badCertificate": "證書無效"
},
"geoAssets": {
"unexpected": "意外錯誤",
"notUpdate": "無可用更新",
"activeNotFound": "未找到激活的 GEO 資源文件"
}
},
"userInfo": {
"title": "我的信息",
"bindingTip": "未綁定郵箱/手機號",
"myAccount": "我的賬號",
"balance": "餘額",
"noValidSubscription": "您沒有有效的訂閱",
"subscribeNow": "立即訂閱",
"shortcuts": "快捷鍵",
"adBlock": "廣告攔截",
"dnsUnlock": "DNS 解鎖",
"contactUs": "聯繫我們",
"others": "其他",
"logout": "退出登錄",
"logoutConfirmTitle": "退出登錄",
"logoutConfirmMessage": "確定要退出登錄嗎?",
"logoutCancel": "取消",
"vpnWebsite": "VPN 官網",
"telegram": "Telegram",
"mail": "郵箱",
"phone": "電話",
"customerService": "人工客服",
"workOrder": "填寫工單",
"pleaseLogin": "請先登錄賬號",
"subscriptionValid": "訂閱有效",
"startTime": "開始時間:",
"expireTime": "到期時間:",
"loginNow": "立即登錄",
"trialPeriod": "歡迎使用高級試用版",
"remainingTime": "剩餘時間",
"trialExpired": "試用期已結束,連接已斷開",
"subscriptionExpired": "訂閱已過期,連接已斷開",
"copySuccess": "複製成功",
"notAvailable": "暫無",
"willBeDeleted": "將被刪除",
"deleteAccountWarning": "賬號刪除是永久性的。一旦您的賬號被刪除,您將無法使用任何功能。是否繼續?",
"requestDelete": "請求刪除",
"switchSubscription": "切換訂閱",
"resetTrafficTitle": "重置流量",
"resetTrafficMessage": "月付套餐流量重置示例:將下一個週期的流量按月重置,訂閱有效期將從{currentTime}提前至{newTime}",
"reset": "重置",
"trafficUsage": "已用: {used} / {total}",
"trafficProgress": {
"title": "流量使用情況",
"unlimited": "不限流量",
"limited": "已用流量"
},
"deviceLimit": "設備限制: {count}"
},
"setting": {
"title": "設置",
"vpnConnection": "VPN連接",
"general": "通用",
"autoConnect": "自動連接",
"routeRule": "路由規則",
"countrySelector": "選擇國家",
"appearance": "外觀",
"notifications": "通知",
"helpImprove": "幫助我們改進",
"helpImproveSubtitle": "幫助我們改進的副標題",
"requestDeleteAccount": "請求刪除賬號",
"goToDelete": "去刪除",
"rateUs": "在 App Store 上為我們評分",
"iosRating": "iOS評分",
"version": "版本",
"switchLanguage": "切換語言",
"system": "系統",
"light": "淺色",
"dark": "深色",
"vpnModeSmart": "智能模式",
"mode": "出站模式",
"connectionTypeGlobal": "全域代理",
"connectionTypeGlobalRemark": "啟用後,所有流量均通過代理伺服器轉發",
"connectionTypeRule": "智能代理",
"connectionTypeRuleRemark": "當[出站模式]設置為[智能代理]時根據所選國家系統自動分流國內IP/域名直連,境外請求透過代理訪問",
"connectionTypeDirect": "直連",
"connectionTypeDirectRemark": "啟用後,所有流量均不經代理直接訪問",
"smartMode": "智能模式",
"secureMode": "安全模式",
"deviceLimit": "設備限制: {count}"
},
"statistics": {
"title": "統計",
"vpnStatus": "VPN 狀態",
"ipAddress": "IP地址",
"connectionTime": "連接時間",
"protocol": "協議",
"weeklyProtectionTime": "每週保護時間",
"currentStreak": "當前連續記錄",
"highestStreak": "最高記錄",
"longestConnection": "最長連接時間",
"days": "{days}天",
"daysOfWeek": {
"monday": "週\n一",
"tuesday": "週\n二",
"wednesday": "週\n三",
"thursday": "週\n四",
"friday": "週\n五",
"saturday": "週\n六",
"sunday": "週\n日"
}
},
"message": {
"title": "通知",
"system": "系統消息",
"promotion": "促銷消息"
},
"invite": {
"title": "邀請好友",
"progress": "邀請進度",
"inviteStats": "邀請統計",
"registers": "已註冊",
"totalCommission": "總佣金",
"rewardDetails": "獎勵明細 >",
"steps": "邀請步驟",
"inviteFriend": "邀請好友",
"acceptInvite": "好友接受邀請\n下單並註冊",
"getReward": "獲得獎勵",
"shareLink": "分享連結",
"shareQR": "分享二維碼",
"rules": "邀請規則",
"rule1": "1、您可以通過分享專屬邀請連結或邀請碼給好友邀請他們加入我們。",
"rule2": "2、好友完成註冊並登錄後邀請獎勵將自動發放至您的賬戶。",
"pending": "待下載",
"processing": "在路上",
"success": "已成功",
"expired": "已失效",
"myInviteCode": "我的邀請碼",
"inviteCodeCopied": "邀請碼已複製到剪貼板",
"close": "關閉",
"saveQRCode": "保存二維碼",
"qrCodeSaved": "二維碼已保存",
"copiedToClipboard": "已複製到剪貼板",
"getInviteCodeFailed": "獲取邀請碼失敗,請稍後重試",
"generateQRCodeFailed": "生成二維碼失敗,請稍後重試",
"generateShareLinkFailed": "生成分享連結失敗,請稍後重試"
},
"purchaseMembership": {
"purchasePackage": "購買套餐",
"noData": "暫無可用套餐",
"myAccount": "我的賬號",
"selectPackage": "選擇套餐",
"packageDescription": "套餐描述",
"paymentMethod": "支付方式",
"cancelAnytime": "您可以隨時在APP上取消",
"startSubscription": "開始訂閱",
"renewNow": "立即續訂",
"month": "{months}個月",
"year": "{years}年",
"day": "{days}天",
"unlimitedTraffic": "不限流量",
"unlimitedDevices": "不限設備",
"devices": "{count}台",
"trafficLimit": "流量限制",
"deviceLimit": "設備限制",
"features": "套餐特性",
"expand": "展開",
"collapse": "收起",
"confirmPurchase": "確認購買",
"confirmPurchaseDesc": "您確定要購買此套餐嗎?"
},
"home": {
"welcome": "歡迎使用 BearVPN",
"disconnected": "已斷開連接",
"connecting": "正在連接",
"connected": "已連接",
"disconnecting": "正在斷開連接",
"currentConnectionTitle": "當前連接",
"switchNode": "切換節點",
"timeout": "超時",
"loading": "載入中...",
"error": "載入失敗",
"checkNetwork": "請檢查網絡連接並重試",
"retry": "重試",
"connectionSectionTitle": "連接方式",
"dedicatedServers": "專用伺服器",
"countryRegion": "國家/地區",
"serverListTitle": "專用伺服器群組",
"nodeListTitle": "所有節點",
"countryListTitle": "國家/地區列表",
"noServers": "暫無可用伺服器",
"noNodes": "暫無可用節點",
"noRegions": "暫無可用地區",
"subscriptionDescription": "訂閱會員,暢享全球高速網絡",
"subscribe": "立即訂閱",
"trialPeriod": "歡迎使用 Premium 試用版",
"remainingTime": "剩餘時間",
"trialExpired": "試用期已結束,已斷開連接",
"subscriptionExpired": "訂閱已過期,已斷開連接",
"subscriptionUpdated": "訂閱已更新",
"subscriptionUpdatedMessage": "您的訂閱信息已更新,請刷新頁面查看最新狀態",
"trialStatus": "試用狀態",
"trialing": "試用中",
"trialEndMessage": "試用期結束後將無法使用",
"lastDaySubscriptionStatus": "訂閱即將到期",
"lastDaySubscriptionMessage": "即將到期",
"subscriptionEndMessage": "訂閱到期後將無法使用",
"trialTimeWithDays": "{days}天 {hours}時 {minutes}分 {seconds}秒",
"trialTimeWithHours": "{hours}時 {minutes}分 {seconds}秒",
"trialTimeWithMinutes": "{minutes}分 {seconds}秒",
"refreshLatency": "刷新延遲",
"testLatency": "測試延遲",
"testing": "正在測試延遲",
"refreshLatencyDesc": "刷新所有節點的延遲",
"testAllNodesLatency": "測試所有節點的網絡延遲",
"autoSelect": "自動選擇",
"selected": "已選擇"
},
"dialog": {
"confirm": "確認",
"cancel": "取消",
"ok": "我知道了"
},
"update": {
"title": "發現新版本",
"content": "是否立即更新?",
"updateNow": "立即更新",
"updateLater": "稍後",
"defaultContent": "1. 優化應用性能\n2. 修復已知問題\n3. 改進用戶體驗"
},
"orderStatus": {
"title": "訂單狀態",
"pending": {
"title": "待支付",
"description": "請完成支付"
},
"paid": {
"title": "已支付",
"description": "正在處理您的訂單"
},
"success": {
"title": "恭喜你!支付成功",
"description": "您的套餐已經購買成功了"
},
"closed": {
"title": "訂單已關閉",
"description": "請重新下單"
},
"failed": {
"title": "支付失敗",
"description": "請重新嘗試支付"
},
"unknown": {
"title": "未知狀態",
"description": "請聯繫客服"
},
"checkFailed": {
"title": "檢查失敗",
"description": "請稍後重試"
},
"initial": {
"title": "支付中",
"description": "請稍候,正在處理您的支付"
}
},
"country": {
"cn": "中國",
"ir": "伊朗",
"af": "阿富汗",
"ru": "俄羅斯",
"id": "印尼",
"tr": "土耳其",
"br": "巴西"
},
"error": {
"200": "成功",
"500": "服務器內部錯誤",
"10001": "數據庫查詢錯誤",
"10002": "數據庫更新錯誤",
"10003": "數據庫插入錯誤",
"10004": "數據庫刪除錯誤",
"20001": "用戶已存在",
"20002": "用戶不存在",
"20003": "用戶密碼錯誤",
"20004": "用戶已被禁用",
"20005": "餘額不足",
"20006": "註冊已暫停",
"20007": "未綁定 Telegram",
"20008": "用戶未綁定 OAuth",
"20009": "邀請碼錯誤",
"30001": "節點已存在",
"30002": "節點不存在",
"30003": "節點群組已存在",
"30004": "節點群組不存在",
"30005": "節點群組不為空",
"400": "參數錯誤",
"40002": "用戶令牌為空",
"40003": "用戶令牌無效",
"40004": "用戶令牌已過期",
"40005": "未登錄",
"401": "請求過多",
"50001": "優惠券不存在",
"50002": "優惠券已使用",
"50003": "優惠券不匹配",
"60001": "訂閱已過期",
"60002": "訂閱不可用",
"60003": "用戶已有訂閱",
"60004": "訂閱已使用",
"60005": "單次訂閱模式超出限制",
"60006": "訂閱配額限制",
"70001": "驗證碼錯誤",
"80001": "加入隊列錯誤",
"90001": "調試模式已啟用",
"90002": "發送短信錯誤",
"90003": "短信功能未啟用",
"90004": "郵件功能未啟用",
"90005": "不支持的登錄方式",
"90006": "驗證器不支持此方式",
"90007": "電話國家代碼為空",
"90008": "密碼為空",
"90009": "國家代碼為空",
"90010": "需要密碼或驗證碼",
"90011": "郵箱已存在",
"90012": "手機號已存在",
"90013": "設備已存在",
"90014": "手機號錯誤",
"90015": "該賬號今日已達到發送限制",
"90017": "設備不存在",
"90018": "用戶 ID 不匹配",
"61001": "訂閱不存在",
"61002": "未找到支付方式",
"61003": "訂閱狀態錯誤",
"61004": "重置期不足",
"61005": "存在未使用流量"
},
"tray": {
"open_dashboard": "打開面板",
"copy_to_terminal": "複製到終端",
"exit_app": "退出應用"
},
"splash": {
"appName": "BearVPN",
"slogan": "暢享全球高速網絡",
"initializing": "正在初始化...",
"networkConnectionFailure": "網絡連接失敗,請檢查並重試",
"retry": "重試",
"networkPermissionFailed": "獲取網絡權限失敗",
"initializationFailed": "初始化失敗"
},
"network": {
"status": {
"connected": "已連接",
"disconnected": "已斷開連接",
"connecting": "正在連接...",
"disconnecting": "正在斷開連接...",
"reconnecting": "正在重新連接...",
"failed": "連接失敗"
},
"permission": {
"title": "網絡權限",
"description": "需要網絡權限以提供 VPN 服務",
"goToSettings": "前往設置",
"cancel": "取消"
}
}
}

View File

@ -0,0 +1,427 @@
{
"login": {
"welcome": "歡迎使用 BearVPN!",
"verifyPhone": "驗證您的手機號",
"verifyEmail": "驗證您的郵箱",
"codeSent": "已向 {account} 發送6位數代碼。請在接下來的 30 分鐘內輸入。",
"back": "返回",
"enterEmailOrPhone": "輸入郵箱或者手機號",
"enterEmail": "Please enter email address",
"enterCode": "請輸入驗證碼",
"enterPassword": "請輸入密碼",
"reenterPassword": "請再次輸入密碼",
"forgotPassword": "忘記密碼",
"codeLogin": "驗證碼登錄",
"passwordLogin": "密碼登錄",
"agreeTerms": "登錄/創建賬戶,即表示我同意",
"termsOfService": "服務條款",
"privacyPolicy": "隱私政策",
"next": "下一步",
"registerNow": "立即註冊",
"setAndLogin": "設置並登錄",
"enterAccount": "請輸入賬戶",
"passwordMismatch": "兩次密碼輸入不一致",
"sendCode": "發送驗證碼",
"codeSentCountdown": "驗證碼已發送 {seconds}s",
"and": "和",
"enterInviteCode": "請輸入邀請碼(選填)",
"registerSuccess": "註冊成功"
},
"failure": {
"unexpected": "意外錯誤",
"clash": {
"unexpected": "意外錯誤",
"core": "Clash 錯誤 ${reason}"
},
"singbox": {
"unexpected": "意外服務錯誤",
"serviceNotRunning": "服務未運行",
"missingPrivilege": "缺少權限",
"missingPrivilegeMsg": "VPN 模式需要管理員權限。以管理員身份重新啟動應用程序或更改服務模式",
"missingGeoAssets": "缺失 GEO 資源文件",
"missingGeoAssetsMsg": "缺失 GEO 資源文件。請考慮更改激活的資源文件或在設置中下載所選資源文件。",
"invalidConfigOptions": "配置選項無效",
"invalidConfig": "無效配置",
"create": "服務創建錯誤",
"start": "服務啟動錯誤"
},
"connectivity": {
"unexpected": "意外失敗",
"missingVpnPermission": "缺少 VPN 權限",
"missingNotificationPermission": "缺少通知權限",
"core": "核心錯誤"
},
"profiles": {
"unexpected": "意外錯誤",
"notFound": "未找到配置文件",
"invalidConfig": "無效配置",
"invalidUrl": "網址無效"
},
"connection": {
"unexpected": "意外連接錯誤",
"timeout": "連接超時",
"badResponse": "錯誤響應",
"connectionError": "連接錯誤",
"badCertificate": "證書無效"
},
"geoAssets": {
"unexpected": "意外錯誤",
"notUpdate": "無可用更新",
"activeNotFound": "未找到激活的 GEO 資源文件"
}
},
"userInfo": {
"title": "我的信息",
"bindingTip": "未綁定郵箱/手機號",
"myAccount": "我的賬號",
"balance": "餘額",
"noValidSubscription": "您沒有有效的訂閱",
"subscribeNow": "立即訂閱",
"shortcuts": "快捷鍵",
"adBlock": "廣告攔截",
"dnsUnlock": "DNS 解鎖",
"contactUs": "聯繫我們",
"others": "其他",
"logout": "退出登錄",
"logoutConfirmTitle": "退出登錄",
"logoutConfirmMessage": "確定要退出登錄嗎?",
"logoutCancel": "取消",
"vpnWebsite": "VPN 官網",
"telegram": "Telegram",
"mail": "郵箱",
"phone": "電話",
"customerService": "人工客服",
"workOrder": "填寫工單",
"pleaseLogin": "請先登錄賬號",
"subscriptionValid": "訂閱有效",
"startTime": "開始時間:",
"expireTime": "到期時間:",
"loginNow": "立即登錄",
"trialPeriod": "歡迎使用高級試用版",
"remainingTime": "剩餘時間",
"trialExpired": "試用期已結束,連接已斷開",
"subscriptionExpired": "訂閱已過期,連接已斷開",
"copySuccess": "複製成功",
"notAvailable": "暫無",
"willBeDeleted": "將被刪除",
"deleteAccountWarning": "賬號刪除是永久性的。一旦您的賬號被刪除,您將無法使用任何功能。是否繼續?",
"requestDelete": "請求刪除",
"switchSubscription": "切換訂閱",
"resetTrafficTitle": "重置流量",
"resetTrafficMessage": "月付套餐流量重置示例:將下一個週期的流量按月重置,訂閱有效期將從{currentTime}提前至{newTime}",
"reset": "重置",
"trafficUsage": "已用: {used} / {total}",
"trafficProgress": {
"title": "流量使用情況",
"unlimited": "不限流量",
"limited": "已用流量"
},
"deviceLimit": "設備限制: {count}"
},
"setting": {
"title": "設置",
"vpnConnection": "VPN連接",
"general": "通用",
"autoConnect": "自動連接",
"routeRule": "路由規則",
"countrySelector": "選擇國家",
"appearance": "外觀",
"notifications": "通知",
"helpImprove": "幫助我們改進",
"helpImproveSubtitle": "幫助我們改進的副標題",
"requestDeleteAccount": "請求刪除賬號",
"goToDelete": "去刪除",
"rateUs": "在 App Store 上為我們評分",
"iosRating": "iOS評分",
"version": "版本",
"switchLanguage": "切換語言",
"system": "系統",
"light": "淺色",
"dark": "深色",
"vpnModeSmart": "智能模式",
"mode": "出站模式",
"connectionTypeGlobal": "全域代理",
"connectionTypeGlobalRemark": "啟用後,所有流量均通過代理伺服器轉發",
"connectionTypeRule": "智能代理",
"connectionTypeRuleRemark": "當[出站模式]設置為[智能代理]時根據所選國家系統自動分流國內IP/域名直連,境外請求透過代理訪問",
"connectionTypeDirect": "直連",
"connectionTypeDirectRemark": "啟用後,所有流量均不經代理直接訪問",
"smartMode": "智能模式",
"secureMode": "安全模式",
"deviceLimit": "設備限制: {count}"
},
"statistics": {
"title": "統計",
"vpnStatus": "VPN 狀態",
"ipAddress": "IP地址",
"connectionTime": "連接時間",
"protocol": "協議",
"weeklyProtectionTime": "每週保護時間",
"currentStreak": "當前連續記錄",
"highestStreak": "最高記錄",
"longestConnection": "最長連接時間",
"days": "{days}天",
"daysOfWeek": {
"monday": "週\n一",
"tuesday": "週\n二",
"wednesday": "週\n三",
"thursday": "週\n四",
"friday": "週\n五",
"saturday": "週\n六",
"sunday": "週\n日"
}
},
"message": {
"title": "通知",
"system": "系統消息",
"promotion": "促銷消息"
},
"invite": {
"title": "邀請好友",
"progress": "邀請進度",
"inviteStats": "邀請統計",
"registers": "已註冊",
"totalCommission": "總佣金",
"rewardDetails": "獎勵明細 >",
"steps": "邀請步驟",
"inviteFriend": "邀請好友",
"acceptInvite": "好友接受邀請\n下單並註冊",
"getReward": "獲得獎勵",
"shareLink": "分享連結",
"shareQR": "分享二維碼",
"rules": "邀請規則",
"rule1": "1、您可以通過分享專屬邀請連結或邀請碼給好友邀請他們加入我們。",
"rule2": "2、好友完成註冊並登錄後邀請獎勵將自動發放至您的賬戶。",
"pending": "待下載",
"processing": "在路上",
"success": "已成功",
"expired": "已失效",
"myInviteCode": "我的邀請碼",
"inviteCodeCopied": "邀請碼已複製到剪貼板",
"close": "關閉",
"saveQRCode": "保存二維碼",
"qrCodeSaved": "二維碼已保存",
"copiedToClipboard": "已複製到剪貼板",
"getInviteCodeFailed": "獲取邀請碼失敗,請稍後重試",
"generateQRCodeFailed": "生成二維碼失敗,請稍後重試",
"generateShareLinkFailed": "生成分享連結失敗,請稍後重試"
},
"purchaseMembership": {
"purchasePackage": "購買套餐",
"noData": "暫無可用套餐",
"myAccount": "我的賬號",
"selectPackage": "選擇套餐",
"packageDescription": "套餐描述",
"paymentMethod": "支付方式",
"cancelAnytime": "您可以隨時在APP上取消",
"startSubscription": "開始訂閱",
"renewNow": "立即續訂",
"month": "{months}個月",
"year": "{years}年",
"day": "{days}天",
"unlimitedTraffic": "不限流量",
"unlimitedDevices": "不限設備",
"devices": "{count}台",
"trafficLimit": "流量限制",
"deviceLimit": "設備限制",
"features": "套餐特性",
"expand": "展開",
"collapse": "收起",
"confirmPurchase": "確認購買",
"confirmPurchaseDesc": "您確定要購買此套餐嗎?"
},
"home": {
"welcome": "歡迎使用 BearVPN",
"disconnected": "已斷開連接",
"connecting": "正在連接",
"connected": "已連接",
"disconnecting": "正在斷開連接",
"currentConnectionTitle": "當前連接",
"switchNode": "切換節點",
"timeout": "超時",
"loading": "載入中...",
"error": "載入失敗",
"checkNetwork": "請檢查網絡連接並重試",
"retry": "重試",
"connectionSectionTitle": "連接方式",
"dedicatedServers": "專用伺服器",
"countryRegion": "國家/地區",
"serverListTitle": "專用伺服器群組",
"nodeListTitle": "所有節點",
"countryListTitle": "國家/地區列表",
"noServers": "暫無可用伺服器",
"noNodes": "暫無可用節點",
"noRegions": "暫無可用地區",
"subscriptionDescription": "訂閱會員,暢享全球高速網絡",
"subscribe": "立即訂閱",
"trialPeriod": "歡迎使用 Premium 試用版",
"remainingTime": "剩餘時間",
"trialExpired": "試用期已結束,已斷開連接",
"subscriptionExpired": "訂閱已過期,已斷開連接",
"subscriptionUpdated": "訂閱已更新",
"subscriptionUpdatedMessage": "您的訂閱信息已更新,請刷新頁面查看最新狀態",
"trialStatus": "試用狀態",
"trialing": "試用中",
"trialEndMessage": "試用期結束後將無法使用",
"lastDaySubscriptionStatus": "訂閱即將到期",
"lastDaySubscriptionMessage": "即將到期",
"subscriptionEndMessage": "訂閱到期後將無法使用",
"trialTimeWithDays": "{days}天 {hours}時 {minutes}分 {seconds}秒",
"trialTimeWithHours": "{hours}時 {minutes}分 {seconds}秒",
"trialTimeWithMinutes": "{minutes}分 {seconds}秒",
"refreshLatency": "刷新延遲",
"testLatency": "測試延遲",
"testing": "正在測試延遲",
"refreshLatencyDesc": "刷新所有節點的延遲",
"testAllNodesLatency": "測試所有節點的網絡延遲",
"autoSelect": "自動選擇",
"selected": "已選擇"
},
"dialog": {
"confirm": "確認",
"cancel": "取消",
"ok": "我知道了"
},
"update": {
"title": "發現新版本",
"content": "是否立即更新?",
"updateNow": "立即更新",
"updateLater": "稍後",
"defaultContent": "1. 優化應用性能\n2. 修復已知問題\n3. 改進用戶體驗"
},
"orderStatus": {
"title": "訂單狀態",
"pending": {
"title": "待支付",
"description": "請完成支付"
},
"paid": {
"title": "已支付",
"description": "正在處理您的訂單"
},
"success": {
"title": "恭喜你!支付成功",
"description": "您的套餐已經購買成功了"
},
"closed": {
"title": "訂單已關閉",
"description": "請重新下單"
},
"failed": {
"title": "支付失敗",
"description": "請重新嘗試支付"
},
"unknown": {
"title": "未知狀態",
"description": "請聯繫客服"
},
"checkFailed": {
"title": "檢查失敗",
"description": "請稍後重試"
},
"initial": {
"title": "支付中",
"description": "請稍候,正在處理您的支付"
}
},
"country": {
"cn": "中國",
"ir": "伊朗",
"af": "阿富汗",
"ru": "俄羅斯",
"id": "印尼",
"tr": "土耳其",
"br": "巴西"
},
"error": {
"200": "成功",
"500": "服務器內部錯誤",
"10001": "數據庫查詢錯誤",
"10002": "數據庫更新錯誤",
"10003": "數據庫插入錯誤",
"10004": "數據庫刪除錯誤",
"20001": "用戶已存在",
"20002": "用戶不存在",
"20003": "用戶密碼錯誤",
"20004": "用戶已被禁用",
"20005": "餘額不足",
"20006": "註冊已暫停",
"20007": "未綁定 Telegram",
"20008": "用戶未綁定 OAuth",
"20009": "邀請碼錯誤",
"30001": "節點已存在",
"30002": "節點不存在",
"30003": "節點群組已存在",
"30004": "節點群組不存在",
"30005": "節點群組不為空",
"400": "參數錯誤",
"40002": "用戶令牌為空",
"40003": "用戶令牌無效",
"40004": "用戶令牌已過期",
"40005": "未登錄",
"401": "請求過多",
"50001": "優惠券不存在",
"50002": "優惠券已使用",
"50003": "優惠券不匹配",
"60001": "訂閱已過期",
"60002": "訂閱不可用",
"60003": "用戶已有訂閱",
"60004": "訂閱已使用",
"60005": "單次訂閱模式超出限制",
"60006": "訂閱配額限制",
"70001": "驗證碼錯誤",
"80001": "加入隊列錯誤",
"90001": "調試模式已啟用",
"90002": "發送短信錯誤",
"90003": "短信功能未啟用",
"90004": "郵件功能未啟用",
"90005": "不支持的登錄方式",
"90006": "驗證器不支持此方式",
"90007": "電話國家代碼為空",
"90008": "密碼為空",
"90009": "國家代碼為空",
"90010": "需要密碼或驗證碼",
"90011": "郵箱已存在",
"90012": "手機號已存在",
"90013": "設備已存在",
"90014": "手機號錯誤",
"90015": "該賬號今日已達到發送限制",
"90017": "設備不存在",
"90018": "用戶 ID 不匹配",
"61001": "訂閱不存在",
"61002": "未找到支付方式",
"61003": "訂閱狀態錯誤",
"61004": "重置期不足",
"61005": "存在未使用流量"
},
"tray": {
"open_dashboard": "打開面板",
"copy_to_terminal": "複製到終端",
"exit_app": "退出應用"
},
"splash": {
"appName": "BearVPN",
"slogan": "暢享全球高速網絡",
"initializing": "正在初始化...",
"networkConnectionFailure": "網絡連接失敗,請檢查並重試",
"retry": "重試",
"networkPermissionFailed": "獲取網絡權限失敗",
"initializationFailed": "初始化失敗"
},
"network": {
"status": {
"connected": "已連接",
"disconnected": "已斷開連接",
"connecting": "正在連接...",
"disconnecting": "正在斷開連接...",
"reconnecting": "正在重新連接...",
"failed": "連接失敗"
},
"permission": {
"title": "網絡權限",
"description": "需要網絡權限以提供 VPN 服務",
"goToSettings": "前往設置",
"cancel": "取消"
}
}
}

View File

@ -1071,6 +1071,21 @@ class AppConfig {
/// ID
String kr_website_id = "";
///
///
/// /v1/app/user/info
///
///
///
/// 00.00
static const int kr_userBalance = 0;
///
///
static const String kr_userReferCode = "";
///
///
bool kr_is_daytime = true;

View File

@ -13,6 +13,7 @@ import 'package:kaer_with_panels/app/utils/kr_device_util.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import '../services/api_service/kr_api.user.dart';
import '../services/kr_announcement_service.dart';
import '../utils/kr_event_bus.dart';
class KRAppRunData {
@ -23,11 +24,11 @@ class KRAppRunData {
/// token
String? kr_token;
///
String? kr_account;
/// 使便 UI
final Rx<String?> kr_account = Rx<String?>(null);
/// ID
String? kr_userId;
/// ID使便 UI
final Rx<int?> kr_userId = Rx<int?>(null);
///
KRLoginType? kr_loginType;
@ -46,17 +47,77 @@ class KRAppRunData {
return _instance;
}
///
bool isDeviceLogin() {
// "device_设备ID"
return kr_account.value != null && kr_account.value!.startsWith('device_');
}
/// JWT token中解析userId
int? _kr_parseUserIdFromToken(String token) {
try {
// JWT格式: header.payload.signature
final parts = token.split('.');
if (parts.length != 3) {
KRLogUtil.kr_e('JWT token格式错误', tag: 'AppRunData');
return null;
}
// payload部分base64
String payload = parts[1];
// paddingbase64要求长度是4的倍数
switch (payload.length % 4) {
case 0:
break; // padding
case 2:
payload += '==';
break;
case 3:
payload += '=';
break;
default:
KRLogUtil.kr_e('JWT payload长度无效', tag: 'AppRunData');
return null;
}
final decodedBytes = base64.decode(payload);
final decodedString = utf8.decode(decodedBytes);
// JSON
final Map<String, dynamic> payloadMap = jsonDecode(decodedString);
// UserId
if (payloadMap.containsKey('UserId')) {
final userId = payloadMap['UserId'];
KRLogUtil.kr_i('从JWT解析出userId: $userId', tag: 'AppRunData');
return userId is int ? userId : int.tryParse(userId.toString());
}
return null;
} catch (e) {
KRLogUtil.kr_e('解析JWT token失败: $e', tag: 'AppRunData');
return null;
}
}
///
Future<void> kr_saveUserInfo(
String token, String account, KRLoginType loginType, String? areaCode) async {
String token,
String account,
KRLoginType loginType,
String? areaCode) async {
KRLogUtil.kr_i('开始保存用户信息', tag: 'AppRunData');
try {
//
kr_token = token;
kr_account = account;
kr_account.value = account;
kr_loginType = loginType;
kr_areaCode = areaCode;
// JWT token中解析userId
kr_userId.value = _kr_parseUserIdFromToken(token);
KRLogUtil.kr_i('从JWT解析userId: ${kr_userId.value}', tag: 'AppRunData');
final Map<String, dynamic> userInfo = {
'token': token,
@ -81,15 +142,13 @@ class KRAppRunData {
}
KRLogUtil.kr_i('用户信息保存成功设置登录状态为true', tag: 'AppRunData');
//
kr_isLogin.value = true;
// Socket
_iniUserInfo().catchError((error) {
KRLogUtil.kr_e('获取用户信息失败: $error', tag: 'AppRunData');
// 使
});
//
// Socket
KRLogUtil.kr_i('用户信息已保存,跳过用户信息接口调用', tag: 'AppRunData');
} catch (e) {
KRLogUtil.kr_e('保存用户信息失败: $e', tag: 'AppRunData');
@ -103,20 +162,23 @@ class KRAppRunData {
Future<void> kr_loginOut() async {
// false
kr_isLogin.value = false;
// Socket
await _kr_disconnectSocket();
//
kr_token = null;
kr_account = null;
kr_userId = null;
kr_account.value = null;
kr_userId.value = null;
kr_loginType = null;
kr_areaCode = null;
//
await KRSecureStorage().kr_deleteData(key: _keyUserInfo);
//
KRAnnouncementService().kr_reset();
//
Get.find<KRMainController>().kr_setPage(0);
}
@ -135,7 +197,7 @@ class KRAppRunData {
try {
final Map<String, dynamic> userInfo = jsonDecode(userInfoString);
kr_token = userInfo['token'];
kr_account = userInfo['account'];
kr_account.value = userInfo['account'];
final loginTypeValue = userInfo['loginType'];
kr_loginType = KRLoginType.values.firstWhere(
(e) => e.value == loginTypeValue,
@ -143,18 +205,21 @@ class KRAppRunData {
);
kr_areaCode = userInfo['areaCode'] ?? "";
KRLogUtil.kr_i('解析用户信息成功: token=${kr_token != null}, account=$kr_account', tag: 'AppRunData');
// token中解析userId
if (kr_token != null && kr_token!.isNotEmpty) {
kr_userId.value = _kr_parseUserIdFromToken(kr_token!);
}
KRLogUtil.kr_i('解析用户信息成功: token=${kr_token != null}, account=${kr_account.value}', tag: 'AppRunData');
// token有效性
if (kr_token != null && kr_token!.isNotEmpty) {
KRLogUtil.kr_i('设置登录状态为true', tag: 'AppRunData');
kr_isLogin.value = true;
//
_iniUserInfo().catchError((error) {
KRLogUtil.kr_e('获取用户信息失败: $error', tag: 'AppRunData');
//
});
//
// ID将从订阅信息或其他途径获取
KRLogUtil.kr_i('已登录,跳过用户信息接口调用', tag: 'AppRunData');
} else {
KRLogUtil.kr_w('Token为空设置为未登录状态', tag: 'AppRunData');
kr_isLogin.value = false;
@ -175,20 +240,6 @@ class KRAppRunData {
KRLogUtil.kr_i('用户信息初始化完成,登录状态: ${kr_isLogin.value}', tag: 'AppRunData');
}
/// Socket
Future<void> _iniUserInfo() async {
final either0 = await KRUserApi().kr_getUserInfo();
either0.fold(
(error) {
KRLogUtil.kr_e(error.msg, tag: 'AppRunData');
},
(userInfo) async {
kr_userId = userInfo.id.toString();
_kr_connectSocket(kr_userId!);
},
);
}
/// Socket
Future<void> _kr_connectSocket(String userId) async {
//

View File

@ -123,6 +123,9 @@ class AppTranslationsLogin {
///
String get enterEmailOrPhone => 'login.enterEmailOrPhone'.tr;
///
String get enterEmail => 'login.enterEmail'.tr;
///
String get enterCode => 'login.enterCode'.tr;

View File

@ -45,7 +45,24 @@ class KROutboundItem {
city = nodeListItem.city; //
country = nodeListItem.country; //
final json = jsonDecode(nodeListItem.config);
// config
// API格式config为空使
// API格式config包含JSON配置
if (nodeListItem.config.isEmpty) {
print(' 节点 ${nodeListItem.name} 使用直接字段构建配置');
_buildConfigFromFields(nodeListItem);
return;
}
late Map<String, dynamic> json;
try {
json = jsonDecode(nodeListItem.config) as Map<String, dynamic>;
} catch (e) {
print('❌ 节点 ${nodeListItem.name} 的 config 解析失败: $e,尝试使用直接字段');
print('📄 Config 内容: ${nodeListItem.config}');
_buildConfigFromFields(nodeListItem);
return;
}
switch (nodeListItem.protocol) {
case "vless":
final securityConfig =
@ -208,4 +225,95 @@ class KROutboundItem {
return {};
}
}
/// API格式
void _buildConfigFromFields(KrNodeListItem nodeListItem) {
switch (nodeListItem.protocol) {
case "shadowsocks":
config = {
"type": "shadowsocks",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"method": "chacha20-ietf-poly1305", //
"password": nodeListItem.uuid
};
print('✅ Shadowsocks 节点配置构建成功: ${nodeListItem.name}');
break;
case "vless":
config = {
"type": "vless",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"uuid": nodeListItem.uuid,
"tls": {
"enabled": true,
"server_name": nodeListItem.serverAddr,
"insecure": false,
"utls": {
"enabled": true,
"fingerprint": "chrome"
}
}
};
print('✅ VLESS 节点配置构建成功: ${nodeListItem.name}');
break;
case "vmess":
config = {
"type": "vmess",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"uuid": nodeListItem.uuid,
"alter_id": 0,
"security": "auto",
"tls": {
"enabled": true,
"server_name": nodeListItem.serverAddr,
"insecure": false,
"utls": {"enabled": true, "fingerprint": "chrome"}
}
};
print('✅ VMess 节点配置构建成功: ${nodeListItem.name}');
break;
case "trojan":
config = {
"type": "trojan",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"password": nodeListItem.uuid,
"tls": {
"enabled": true,
"server_name": nodeListItem.serverAddr,
"insecure": false,
"utls": {"enabled": true, "fingerprint": "chrome"}
}
};
print('✅ Trojan 节点配置构建成功: ${nodeListItem.name}');
break;
case "hysteria2":
config = {
"type": "hysteria2",
"tag": nodeListItem.name,
"server": nodeListItem.serverAddr,
"server_port": nodeListItem.port,
"password": nodeListItem.uuid,
"up_mbps": 100,
"down_mbps": 100,
"tls": {
"enabled": true,
"server_name": nodeListItem.serverAddr,
"insecure": false,
"alpn": ["h3"]
}
};
print('✅ Hysteria2 节点配置构建成功: ${nodeListItem.name}');
break;
default:
print('⚠️ 不支持的协议类型: ${nodeListItem.protocol}');
config = {};
}
}
}

View File

@ -45,8 +45,15 @@ class KrOutboundsList {
}
final KROutboundItem item = KROutboundItem(element);
// type
if (item.config.isEmpty || !item.config.containsKey('type')) {
print('⚠️ 跳过无效节点: ${element.name},配置为空或缺少 type 字段');
continue; //
}
allList.add(item);
//
for (var tag in element.tags) {
tagGroups.putIfAbsent(tag, () => []);

View File

@ -5,23 +5,39 @@ class KRNodeList {
final String subscribeId;
final String startTime;
final String expireTime;
final bool isTryOut; //
const KRNodeList({
required this.list,
this.subscribeId = "0",
this.startTime = "",
this.expireTime = "",
this.isTryOut = false,
});
factory KRNodeList.fromJson(Map<String, dynamic> json) {
try {
final List<dynamic>? jsonList= json['list'] as List<dynamic>?;
// API : {"list": [{"id": 24, "is_try_out": true, "nodes": [...]}]}
final List<dynamic>? listData = json['list'] as List<dynamic>?;
if (listData == null || listData.isEmpty) {
KRLogUtil.kr_w('节点列表为空', tag: 'NodeList');
return const KRNodeList(list: []);
}
//
final subscribeData = listData[0] as Map<String, dynamic>;
final bool isTryOut = subscribeData['is_try_out'] as bool? ?? false;
final List<dynamic>? nodesData = subscribeData['nodes'] as List<dynamic>?;
KRLogUtil.kr_i('节点列表解析: is_try_out=$isTryOut, 节点数=${nodesData?.length ?? 0}', tag: 'NodeList');
return KRNodeList(
list: jsonList?.map((e) => KrNodeListItem.fromJson(e as Map<String, dynamic>)).toList() ?? [],
subscribeId: json['id']?.toString() ?? "0",
startTime: json['start_time']?.toString() ?? "",
expireTime: json['expire_time']?.toString() ?? "",
list: nodesData?.map((e) => KrNodeListItem.fromJson(e as Map<String, dynamic>)).toList() ?? [],
subscribeId: subscribeData['id']?.toString() ?? "0",
startTime: subscribeData['start_time']?.toString() ?? "",
expireTime: subscribeData['expire_time']?.toString() ?? "",
isTryOut: isTryOut,
);
} catch (err) {
KRLogUtil.kr_e('KRNodeList解析错误: $err', tag: 'NodeList');
@ -38,6 +54,7 @@ class KrNodeListItem {
final String relayMode;
final String relayNode;
final String serverAddr;
final int port; //
final int speedLimit;
final List<String> tags;
final int traffic;
@ -63,6 +80,7 @@ class KrNodeListItem {
this.relayMode = '',
this.relayNode = '',
required this.serverAddr,
this.port = 0, //
required this.speedLimit,
required this.tags,
required this.traffic,
@ -83,6 +101,12 @@ class KrNodeListItem {
factory KrNodeListItem.fromJson(Map<String, dynamic> json) {
try {
// API格式
// : address, port
// : server_addr, config port
final serverAddr = json['address']?.toString() ?? json['server_addr']?.toString() ?? '';
final port = _parseIntSafely(json['port']);
return KrNodeListItem(
id: _parseIntSafely(json['id']),
name: json['name']?.toString() ?? '',
@ -90,7 +114,8 @@ class KrNodeListItem {
protocol: json['protocol']?.toString() ?? '',
relayMode: json['relay_mode']?.toString() ?? '',
relayNode: json['relay_node']?.toString() ?? '',
serverAddr: json['server_addr']?.toString() ?? '',
serverAddr: serverAddr,
port: port,
speedLimit: _parseIntSafely(json['speed_limit']),
tags: _parseStringList(json['tags']),
traffic: _parseIntSafely(json['traffic']),
@ -116,6 +141,7 @@ class KrNodeListItem {
uuid: '',
protocol: '',
serverAddr: '',
port: 0,
speedLimit: 0,
tags: [],
traffic: 0,

View File

@ -10,6 +10,7 @@ class KRUserAvailableSubscribeItem {
final String startTime;
final String expireTime;
final List<dynamic> list;
final bool isTryOut; // true=false=
const KRUserAvailableSubscribeItem({
this.id = 0,
@ -21,19 +22,35 @@ class KRUserAvailableSubscribeItem {
this.startTime = '',
this.expireTime = '',
this.list = const [],
this.isTryOut = false,
});
factory KRUserAvailableSubscribeItem.fromJson(Map<String, dynamic> json) {
// subscribe
final subscribe = json['subscribe'] as Map<String, dynamic>?;
// int () String (ISO 8601)
String convertTime(dynamic value) {
if (value == null) return '';
if (value is String) return value;
if (value is int) {
// ISO 8601
return DateTime.fromMillisecondsSinceEpoch(value).toIso8601String();
}
return value.toString();
}
return KRUserAvailableSubscribeItem(
id: json['id'] as int? ?? 0,
name: json['name'] as String? ?? '',
deviceLimit: json['device_limit'] as int? ?? 0,
name: subscribe?['name'] as String? ?? '',
deviceLimit: subscribe?['device_limit'] as int? ?? 0,
download: json['download'] as int? ?? 0,
upload: json['upload'] as int? ?? 0,
traffic: json['traffic'] as int? ?? 0,
startTime: json['start_time'] as String? ?? '',
expireTime: json['expire_time'] as String? ?? '',
startTime: convertTime(json['start_time']),
expireTime: convertTime(json['expire_time']),
list: (json['list'] as List<dynamic>?) ?? const [],
isTryOut: subscribe?['is_try_out'] as bool? ?? false,
);
}
@ -48,6 +65,7 @@ class KRUserAvailableSubscribeItem {
'start_time': startTime,
'expire_time': expireTime,
'list': list,
'is_try_out': isTryOut,
};
}
}

View File

@ -65,7 +65,7 @@ class KRCrispController extends GetxController {
try {
final appData = KRAppRunData();
final currentLanguage = KRLanguageUtils.getCurrentLanguageCode();
final userEmail = appData.kr_account ?? '';
final userEmail = appData.kr_account.value ?? '';
// ID
final deviceId = await KRDeviceUtil().kr_getDeviceId();

View File

@ -43,23 +43,17 @@ class KRDeleteAccountController extends GetxController {
super.onClose();
}
//
//
Future<void> kr_sendCode() async {
final account = KRAppRunData.getInstance().kr_account;
final account = KRAppRunData.getInstance().kr_account.value;
if (account == null || account.isEmpty) {
KRCommonUtil.kr_showToast('账号不能为空');
return;
}
//
final isEmail = KRAppRunData.getInstance().kr_loginType == KRLoginType.kr_email;
final type = isEmail ? KRLoginType.kr_email : KRLoginType.kr_telephone;
//
// API email type
final result = await _authApi.kr_sendCode(
type,
account,
KRAppRunData.getInstance().kr_areaCode, //
account, //
2, //
);
@ -70,7 +64,7 @@ class KRDeleteAccountController extends GetxController {
(success) {
kr_canSendCode.value = false;
kr_countdown.value = 60;
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (kr_countdown.value > 0) {
kr_countdown.value--;
@ -89,11 +83,12 @@ class KRDeleteAccountController extends GetxController {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.sendCode);
return;
}
final result = await _authApi.kr_deleteAccount(
KRAppRunData.getInstance().kr_loginType ?? KRLoginType.kr_telephone,
// API code
final result = await _authApi.kr_deleteAccount(
kr_codeController.text,
);
result.fold(
(error) {
KRCommonUtil.kr_showToast(error.msg);
@ -103,7 +98,5 @@ class KRDeleteAccountController extends GetxController {
KRAppRunData.getInstance().kr_loginOut();
},
);
// TODO:
}
}

View File

@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/kr_device_management_controller.dart';
class KRDeviceManagementBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<KRDeviceManagementController>(
() => KRDeviceManagementController(),
);
}
}

View File

@ -0,0 +1,294 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import 'package:kaer_with_panels/app/utils/kr_device_util.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_api.user.dart';
import 'package:kaer_with_panels/app/widgets/dialogs/kr_dialog.dart';
import 'package:kaer_with_panels/app/common/app_run_data.dart';
import 'package:kaer_with_panels/app/services/api_service/kr_auth_api.dart';
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
import 'package:kaer_with_panels/app/services/kr_device_info_service.dart';
import 'package:kaer_with_panels/app/services/kr_subscribe_service.dart';
import 'package:kaer_with_panels/app/model/enum/kr_request_type.dart';
import 'dart:io';
import 'dart:math';
class KRDeviceManagementController extends GetxController {
//
final RxList<Map<String, dynamic>> devices = <Map<String, dynamic>>[].obs;
//
final RxBool isLoading = true.obs;
// ID
String? currentDeviceId;
@override
void onInit() {
super.onInit();
_initDeviceId();
loadDeviceList();
}
/// ID
Future<void> _initDeviceId() async {
try {
currentDeviceId = await KRDeviceUtil().kr_getDeviceId();
KRLogUtil.kr_i('当前设备ID: $currentDeviceId', tag: 'DeviceManagement');
} catch (e) {
KRLogUtil.kr_e('获取设备ID失败: $e', tag: 'DeviceManagement');
}
}
///
Future<void> loadDeviceList() async {
try {
isLoading.value = true;
KRLogUtil.kr_i('开始加载设备列表', tag: 'DeviceManagement');
// API获取设备列表
final result = await KRUserApi().kr_getUserDevices();
result.fold(
(error) {
KRLogUtil.kr_e('加载设备列表失败: ${error.msg}', tag: 'DeviceManagement');
Get.snackbar('错误', error.msg);
},
(deviceList) {
KRLogUtil.kr_i('获取到 ${deviceList.length} 个设备', tag: 'DeviceManagement');
//
devices.value = deviceList.map((device) {
final identifier = device['identifier']?.toString() ?? '';
final isCurrent = identifier == currentDeviceId;
return {
'id': device['id']?.toString() ?? '',
'identifier': identifier,
'device_name': device['user_agent'] ?? '未知设备',
'ip': device['ip'] ?? '',
'last_login': device['updated_at'] ?? device['created_at'] ?? '',
'is_current': isCurrent,
'enabled': device['enabled'] ?? true,
'online': device['online'] ?? false,
};
}).toList();
},
);
} catch (e, stackTrace) {
KRLogUtil.kr_e('加载设备列表异常: $e', tag: 'DeviceManagement');
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
Get.snackbar('错误', '加载设备列表失败');
} finally {
isLoading.value = false;
}
}
///
Future<void> deleteDevice(String id) async {
try {
//
final device = devices.firstWhere(
(d) => d['id'] == id,
orElse: () => {},
);
if (device.isEmpty) return;
final isCurrent = device['is_current'] ?? false;
// 使
bool? confirmed;
//
await KRDialog.show(
title: '确认删除',
message: isCurrent
? '确定要删除本机设备吗?删除后将使用设备登录自动重新登录。'
: '确定要删除此设备吗?删除后该设备将被强制下线。',
icon: Container(
width: 56,
height: 56,
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.warning_rounded,
color: Colors.red,
size: 32,
),
),
confirmText: '删除',
cancelText: '取消',
onConfirm: () {
confirmed = true;
},
onCancel: () {
confirmed = false;
},
);
if (confirmed != true) return;
KRLogUtil.kr_i('开始解绑设备 - id: $id, isCurrent: $isCurrent', tag: 'DeviceManagement');
// API解绑设备
final result = await KRUserApi().kr_unbindUserDevice(id);
result.fold(
(error) {
KRLogUtil.kr_e('删除设备失败: ${error.msg}', tag: 'DeviceManagement');
Get.snackbar('错误', '删除失败:${error.msg}');
},
(_) async {
KRLogUtil.kr_i('设备删除成功', tag: 'DeviceManagement');
if (isCurrent) {
//
KRLogUtil.kr_i('本机设备已删除,准备重新登录', tag: 'DeviceManagement');
//
Get.back();
//
await _reloginWithDevice();
} else {
//
devices.removeWhere((device) => device['id'] == id);
Get.snackbar('成功', '设备已删除');
}
},
);
} catch (e, stackTrace) {
KRLogUtil.kr_e('删除设备异常: $e', tag: 'DeviceManagement');
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
Get.snackbar('错误', '删除失败:$e');
}
}
/// 使
Future<void> _reloginWithDevice() async {
try {
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'DeviceManagement');
KRLogUtil.kr_i('开始重新进行设备登录', tag: 'DeviceManagement');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'DeviceManagement');
// kr_loginOut
final appRunData = KRAppRunData.getInstance();
appRunData.kr_isLogin.value = false;
appRunData.kr_token = null;
appRunData.kr_account.value = null;
appRunData.kr_userId.value = null;
//
final siteConfigService = KRSiteConfigService();
final isDeviceLoginEnabled = siteConfigService.isDeviceLoginEnabled();
if (!isDeviceLoginEnabled) {
KRLogUtil.kr_w('设备登录未启用,执行完整退出登录', tag: 'DeviceManagement');
Get.snackbar('提示', '设备登录未启用,请手动登录');
await appRunData.kr_loginOut();
return;
}
KRLogUtil.kr_i('设备登录已启用,开始调用设备登录接口', tag: 'DeviceManagement');
//
await KRDeviceInfoService().initialize();
//
final authApi = KRAuthApi();
final result = await authApi.kr_deviceLogin();
result.fold(
(error) {
//
KRLogUtil.kr_e('设备登录失败: ${error.msg}', tag: 'DeviceManagement');
Get.snackbar('错误', '自动登录失败:${error.msg},请手动登录');
// 退
appRunData.kr_loginOut();
},
(token) async {
//
KRLogUtil.kr_i('✅ 设备登录成功!', tag: 'DeviceManagement');
KRLogUtil.kr_i('🎫 Token: ${token.substring(0, min(20, token.length))}...', tag: 'DeviceManagement');
//
final deviceId = KRDeviceInfoService().deviceId ?? 'unknown';
await appRunData.kr_saveUserInfo(
token,
'device_$deviceId',
KRLoginType.kr_email,
null,
);
KRLogUtil.kr_i('✅ 设备重新登录成功,已更新用户信息', tag: 'DeviceManagement');
//
await Future.delayed(const Duration(milliseconds: 300));
//
KRLogUtil.kr_i('🔄 开始刷新订阅信息...', tag: 'DeviceManagement');
try {
await KRSubscribeService().kr_refreshAll();
KRLogUtil.kr_i('✅ 订阅信息刷新成功', tag: 'DeviceManagement');
} catch (e) {
KRLogUtil.kr_e('订阅信息刷新失败: $e', tag: 'DeviceManagement');
}
Get.snackbar('成功', '已自动重新登录');
},
);
} catch (e, stackTrace) {
KRLogUtil.kr_e('设备重新登录异常: $e', tag: 'DeviceManagement');
KRLogUtil.kr_e('堆栈跟踪: $stackTrace', tag: 'DeviceManagement');
Get.snackbar('错误', '自动登录失败,请手动登录');
// 退
await KRAppRunData.getInstance().kr_loginOut();
}
}
///
Map<String, dynamic> getDeviceTypeInfo(String userAgent) {
String deviceType = '未知设备';
String iconName = 'devices';
if (userAgent.contains('Android') || userAgent.toLowerCase().contains('android')) {
deviceType = '安卓设备';
iconName = 'phone_android';
} else if (userAgent.contains('iOS') || userAgent.contains('iPhone') || userAgent.toLowerCase().contains('ios')) {
deviceType = 'iOS 设备';
iconName = 'phone_iphone';
} else if (userAgent.contains('iPad')) {
deviceType = 'iPad';
iconName = 'tablet';
} else if (userAgent.contains('macOS') || userAgent.contains('Mac') || userAgent.toLowerCase().contains('mac')) {
deviceType = 'macOS';
iconName = 'desktop_mac';
} else if (userAgent.contains('Windows') || userAgent.toLowerCase().contains('windows')) {
deviceType = 'Windows';
iconName = 'computer';
} else if (userAgent.contains('Linux') || userAgent.toLowerCase().contains('linux')) {
deviceType = 'Linux';
iconName = 'computer';
}
return {
'type': deviceType,
'icon': iconName,
};
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
super.onClose();
}
}

View File

@ -0,0 +1,313 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:kaer_with_panels/app/widgets/kr_app_text_style.dart';
import '../controllers/kr_device_management_controller.dart';
class KRDeviceManagementView extends GetView<KRDeviceManagementController> {
const KRDeviceManagementView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
backgroundColor: Theme.of(context).primaryColor,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color.fromRGBO(23, 151, 255, 0.15),
Color.fromRGBO(23, 151, 255, 0.05),
],
stops: [0.0, 0.28],
),
),
child: Column(
children: [
//
AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: Icon(
Icons.arrow_back_ios,
color: Theme.of(context).iconTheme.color,
size: 20.w,
),
onPressed: () => Get.back(),
),
title: Text(
'设备管理',
style: KrAppTextStyle(
color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
//
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return Center(
child: CircularProgressIndicator(),
);
}
if (controller.devices.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.devices_other,
size: 64.w,
color: Theme.of(context).textTheme.bodySmall?.color,
),
SizedBox(height: 16.w),
Text(
'暂无登录设备',
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
);
}
return RefreshIndicator(
onRefresh: () => controller.loadDeviceList(),
child: ListView.builder(
padding: EdgeInsets.all(16.w),
itemCount: controller.devices.length,
itemBuilder: (context, index) {
return _buildDeviceItem(
context,
controller.devices[index],
);
},
),
);
}),
),
],
),
),
);
}
///
Widget _buildDeviceItem(
BuildContext context, Map<String, dynamic> device) {
final id = device['id'] ?? '';
final identifier = device['identifier'] ?? '';
final userAgent = device['device_name'] ?? '未知设备';
final isCurrent = device['is_current'] ?? false;
final ip = device['ip'] ?? '';
final lastLoginRaw = device['last_login'];
final String lastLogin = lastLoginRaw?.toString() ?? '';
//
final deviceInfo = controller.getDeviceTypeInfo(userAgent);
final deviceType = deviceInfo['type'] as String;
final iconName = deviceInfo['icon'] as String;
return Container(
margin: EdgeInsets.only(bottom: 12.w),
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: BorderRadius.circular(12.w),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 10.w,
offset: Offset(0, 2.w),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Row(
children: [
//
Container(
width: 48.w,
height: 48.w,
decoration: BoxDecoration(
color: const Color(0xFF1797FF).withOpacity(0.1),
borderRadius: BorderRadius.circular(8.w),
),
child: Icon(
_getIconData(iconName),
color: const Color(0xFF1797FF),
size: 24.w,
),
),
SizedBox(width: 12.w),
//
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
deviceType,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
if (isCurrent) ...[
SizedBox(width: 8.w),
Container(
padding: EdgeInsets.symmetric(
horizontal: 8.w,
vertical: 2.w,
),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(4.w),
),
child: Text(
'本机',
style: KrAppTextStyle(
fontSize: 10,
color: Colors.green,
),
),
),
],
],
),
SizedBox(height: 4.w),
Text(
'ID: ${identifier.substring(0, identifier.length > 12 ? 12 : identifier.length)}...',
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
],
),
),
//
TextButton(
onPressed: () => controller.deleteDevice(id),
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w),
),
child: Text(
'删除',
style: KrAppTextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.error,
),
),
),
],
),
// 线
if (ip.isNotEmpty || lastLogin.isNotEmpty) ...[
SizedBox(height: 12.w),
Divider(height: 1, color: Theme.of(context).dividerColor),
SizedBox(height: 12.w),
],
//
if (ip.isNotEmpty)
_buildInfoRow(
context,
'IP地址',
ip,
),
if (ip.isNotEmpty && lastLogin.isNotEmpty) SizedBox(height: 8.w),
if (lastLogin.isNotEmpty)
_buildInfoRow(
context,
'最后登录',
_formatDateTime(lastLoginRaw),
),
],
),
);
}
///
Widget _buildInfoRow(BuildContext context, String label, String value) {
return Row(
children: [
Text(
'$label: ',
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
Expanded(
child: Text(
value,
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
overflow: TextOverflow.ellipsis,
),
),
],
);
}
///
String _formatDateTime(dynamic timestamp) {
if (timestamp == null) return '未知';
try {
DateTime dateTime;
if (timestamp is int) {
// 1013
if (timestamp > 9999999999) {
//
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
} else {
//
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
}
} else if (timestamp is String) {
dateTime = DateTime.parse(timestamp);
} else {
return '未知';
}
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
} catch (e) {
return '未知';
}
}
///
IconData _getIconData(String iconName) {
switch (iconName) {
case 'phone_android':
return Icons.phone_android;
case 'phone_iphone':
return Icons.phone_iphone;
case 'tablet':
return Icons.tablet_mac;
case 'desktop_mac':
return Icons.desktop_mac;
case 'computer':
return Icons.computer;
default:
return Icons.devices;
}
}
}

View File

@ -183,10 +183,7 @@ class KRHomeController extends GetxController {
if (isValidLogin) {
kr_currentViewStatus.value = KRHomeViewsStatus.kr_loggedIn;
KRLogUtil.kr_i('设置为已登录状态', tag: 'HomeController');
//
KRAnnouncementService().kr_checkAnnouncement();
//
_kr_ensureSubscribeServiceInitialized();
} else {
@ -255,7 +252,15 @@ class KRHomeController extends GetxController {
} else {
kr_currentViewStatus.value = KRHomeViewsStatus.kr_notLoggedIn;
KRLogUtil.kr_i('登录状态变化:设置为未登录', tag: 'HomeController');
//
kr_currentListStatus.value = KRHomeViewsListStatus.kr_none;
// 退
kr_subscribeService.kr_logout();
//
kr_updateBottomPanelHeight();
}
} catch (e) {
KRLogUtil.kr_e('处理登录状态变化失败: $e', tag: 'HomeController');

View File

@ -26,30 +26,13 @@ class KRHomeConnectionOptionsView extends GetView<KRHomeController> {
),
),
SizedBox(height: 8.w),
Row(
children: [
Flexible(
child: _buildConnectionOption(
"home_server",
AppTranslations.kr_home.dedicatedServers,
context,
onTap: () {
controller.kr_switchListStatus(KRHomeViewsListStatus.kr_serverList);
},
),
),
SizedBox(width: 12.w),
Flexible(
child: _buildConnectionOption(
"home_ct",
AppTranslations.kr_home.countryRegion,
context,
onTap: () {
controller.kr_switchListStatus(KRHomeViewsListStatus.kr_countrySubscribeList);
},
),
),
],
_buildConnectionOption(
"home_ct",
AppTranslations.kr_home.countryRegion,
context,
onTap: () {
controller.kr_switchListStatus(KRHomeViewsListStatus.kr_countrySubscribeList);
},
),
],
);

View File

@ -31,28 +31,32 @@ class KRHomeView extends GetView<KRHomeController> {
//
const KRHomeMapView(),
//
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
spreadRadius: 0,
blurRadius: 10,
offset: const Offset(0, -2),
),
],
Align(
alignment: Alignment.bottomCenter,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Container(
width: double.infinity,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Theme.of(context).cardColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
spreadRadius: 0,
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: const KRLoginView(),
),
child: const KRLoginView(),
),
),
],

View File

@ -71,16 +71,15 @@ class KRInviteController extends GetxController {
}
}
// kr_getUserInfo
// 使 AppConfig
Future<void> _kr_fetchUserInfo() async {
try {
kr_isLoading.value = true;
final either = await KRUserApi().kr_getUserInfo();
either.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(userInfo) {
kr_referCode.value = userInfo.referCode;
},
);
// 使 AppConfig
kr_referCode.value = AppConfig.kr_userReferCode;
} catch (e) {
KRCommonUtil.kr_showToast(e.toString());
} finally {

View File

@ -9,6 +9,7 @@ import 'package:kaer_with_panels/app/model/kr_area_code.dart';
import 'package:kaer_with_panels/app/utils/kr_common_util.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/utils/kr_event_bus.dart';
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
import '../../../localization/kr_language_utils.dart';
@ -125,9 +126,9 @@ class KRLoginController extends GetxController
String kr_getNextBtnText() {
switch (kr_loginStatus.value) {
case KRLoginProgressStatus.kr_check:
return AppTranslations.kr_login.next;
return AppTranslations.kr_login.passwordLogin; // "登录"
case KRLoginProgressStatus.kr_loginByCode:
return AppTranslations.kr_login.codeLogin;
return AppTranslations.kr_login.passwordLogin; //
case KRLoginProgressStatus.kr_loginByPsd:
return AppTranslations.kr_login.passwordLogin;
case KRLoginProgressStatus.kr_registerSendCode:
@ -198,25 +199,17 @@ class KRLoginController extends GetxController
}
});
// accountController
// accountController
accountController.addListener(() {
String input = accountController.text.trim();
//
//
Future.microtask(() {
final isNumeric = _isNumeric(input);
if (isNumeric && kr_loginType.value != KRLoginType.kr_telephone) {
kr_loginType.value = KRLoginType.kr_telephone;
kr_emailList.clear();
kr_removeOverlay();
} else if (!isNumeric && kr_loginType.value != KRLoginType.kr_email) {
kr_loginType.value = KRLoginType.kr_email;
}
//
kr_loginType.value = KRLoginType.kr_email;
//
if (!isNumeric) {
kr_emailList.value = kr_generateAndSortEmailList(input);
}
//
kr_emailList.value = kr_generateAndSortEmailList(input);
kr_accountHasText.value = input.isNotEmpty;
});
@ -259,40 +252,29 @@ class KRLoginController extends GetxController
return numericRegex.hasMatch(input);
}
///
///
void kr_check() async {
if (accountController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount);
return;
}
final either = await KRAuthApi().kr_isRegister(
kr_loginType.value,
accountController.text,
kr_loginType == KRLoginType.kr_telephone
? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode
: null);
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
}, (r) async {
kr_isRegistered.value = r;
kr_loginStatus.value = r
? KRLoginProgressStatus.kr_loginByPsd
: KRLoginProgressStatus.kr_registerSendCode;
});
if (psdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
return;
}
//
kr_login();
}
///
///
void kr_sendCode() async {
final either = await KRAuthApi().kr_sendCode(
kr_loginType.value,
accountController.text,
kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode,
kr_loginStatus.value == KRLoginProgressStatus.kr_registerSendCode
? 1
: kr_loginStatus.value == KRLoginProgressStatus.kr_forgetPsdSendCode
? 2
: 2);
? 2 // 2
: 3); // 3
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
}, (r) async {
@ -301,28 +283,15 @@ class KRLoginController extends GetxController
});
}
///
/// +
void kr_login() async {
if (kr_loginStatus == KRLoginProgressStatus.kr_loginByCode &&
codeController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterCode);
return;
}
if (kr_loginStatus == KRLoginProgressStatus.kr_loginByPsd &&
psdController.text.isEmpty) {
if (psdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
return;
}
final either = await KRAuthApi().kr_login(
kr_loginType.value,
kr_loginStatus.value == KRLoginProgressStatus.kr_loginByPsd,
accountController.text,
kr_loginType == KRLoginType.kr_telephone
? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode
: null,
codeController.text,
psdController.text);
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
@ -331,8 +300,14 @@ class KRLoginController extends GetxController
});
}
///
///
void kr_register() async {
//
if (accountController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterAccount);
return;
}
if (psdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
return;
@ -346,20 +321,26 @@ class KRLoginController extends GetxController
return;
}
//
final siteConfig = KRSiteConfigService();
final needVerification = siteConfig.isEmailVerificationEnabled() ||
siteConfig.isRegisterVerificationEnabled();
if (needVerification && codeController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterCode);
return;
}
final either = await KRAuthApi().kr_register(
kr_loginType.value,
accountController.text,
kr_loginType == KRLoginType.kr_telephone
? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode
: null,
codeController.text,
psdController.text,
inviteCode: inviteCodeController.text);
code: codeController.text.isEmpty ? null : codeController.text,
inviteCode: inviteCodeController.text.isEmpty ? null : inviteCodeController.text);
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
}, (r) async {
_saveLoginData(r);
KRCommonUtil.kr_showToast(AppTranslations.kr_login.registerSuccess);
KRCommonUtil.kr_showToast(AppTranslations.kr_login.registerSuccess);
});
}
@ -390,20 +371,17 @@ class KRLoginController extends GetxController
}
}
///
///
void kr_checkVerificationCode(KRLoginProgressStatus status) async {
final either = await KRAuthApi().kr_checkVerificationCode(
kr_loginType.value,
accountController.text,
kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode,
codeController.text,
kr_loginStatus.value == KRLoginProgressStatus.kr_registerSendCode
? 1
: 2);
? 2 // 2
: 3); // 3
either.fold((l) {
KRCommonUtil.kr_showToast(l.msg);
}, (r) async {
if (status == KRLoginProgressStatus.kr_registerSendCode) {
kr_loginStatus.value = KRLoginProgressStatus.kr_registerSetPsd;
} else if (status == KRLoginProgressStatus.kr_forgetPsdSendCode) {
@ -412,7 +390,7 @@ class KRLoginController extends GetxController
});
}
/// ---
/// -
void kr_setNewPsdByForgetPsd() async {
if (psdController.text.isEmpty) {
KRCommonUtil.kr_showToast(AppTranslations.kr_login.enterPassword);
@ -428,11 +406,7 @@ class KRLoginController extends GetxController
}
final either = await KRAuthApi().kr_setNewPsdByForgetPsd(
kr_loginType.value,
accountController.text,
kr_loginType == KRLoginType.kr_telephone
? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode
: null,
codeController.text,
psdController.text);
either.fold((l) {
@ -464,15 +438,13 @@ class KRLoginController extends GetxController
});
}
///
///
void _saveLoginData(String token) {
KRAppRunData.getInstance().kr_saveUserInfo(
token,
accountController.text,
kr_loginType.value,
kr_loginType == KRLoginType.kr_telephone
? kr_areaCodeList[kr_cutSeleteCodeIndex.value].kr_dialCode
: null);
KRLoginType.kr_email,
null);
kr_loginStatus.value = KRLoginProgressStatus.kr_check;
// /
@ -480,6 +452,9 @@ class KRLoginController extends GetxController
Future.delayed(Duration(milliseconds: 100), () {
KREventBus().kr_sendMessage(KRMessageType.kr_payment);
});
//
Get.back();
}
///
@ -611,6 +586,14 @@ class KRLoginController extends GetxController
// }
}
@override
void onReady() {
super.onReady();
//
// "登录/注册"
kr_loginStatus.value = KRLoginProgressStatus.kr_check;
}
@override
void onClose() {
kr_removeOverlay();
@ -640,13 +623,9 @@ class KRLoginController extends GetxController
void _updateInputState() {
String input = accountController.text.trim();
if (_isNumeric(input)) {
kr_loginType.value = KRLoginType.kr_telephone;
kr_emailList.clear();
} else {
kr_loginType.value = KRLoginType.kr_email;
kr_emailList.value = kr_generateAndSortEmailList(input);
}
//
kr_loginType.value = KRLoginType.kr_email;
kr_emailList.value = kr_generateAndSortEmailList(input);
}
//

View File

@ -14,6 +14,7 @@ import '../controllers/kr_login_controller.dart';
import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/services/api_service/api.dart';
import 'package:kaer_with_panels/app/common/app_config.dart';
import 'package:kaer_with_panels/app/services/kr_site_config_service.dart';
class KRLoginView extends GetView<KRLoginController> {
const KRLoginView({super.key});
@ -21,15 +22,16 @@ class KRLoginView extends GetView<KRLoginController> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return GestureDetector(
onTap: () {
if (!FocusScope.of(context).hasPrimaryFocus) {
FocusScope.of(context).unfocus();
}
_hideDropdown();
},
child: Container(
color: theme.scaffoldBackgroundColor,
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
body: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
if (!FocusScope.of(context).hasPrimaryFocus) {
FocusScope.of(context).unfocus();
}
_hideDropdown();
},
child: SingleChildScrollView(
padding: EdgeInsets.symmetric(horizontal: 20.w),
child: Obx(() {
@ -58,16 +60,18 @@ class KRLoginView extends GetView<KRLoginController> {
}
Widget _buildCheckView(BuildContext context) {
//
// -
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Visibility(
visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check,
child: _buildBackButton(Theme.of(context)),
)),
// kr_check
_buildBackButton(Theme.of(context)),
_buildHeaderSection(Theme.of(context)),
//
_buildInputSection(context, Theme.of(context)),
SizedBox(height: 8.w),
//
_buildPasswordInput(Theme.of(context)),
_buildDynamicContent(Theme.of(context)),
_buildNextButton(controller.kr_getNextBtnText(), Theme.of(context)),
SizedBox(height: _getBottomPadding()),
@ -75,6 +79,63 @@ class KRLoginView extends GetView<KRLoginController> {
);
}
/// kr_check
Widget _buildPasswordInput(ThemeData theme) {
return Container(
width: double.infinity,
height: 52.w,
decoration: ShapeDecoration(
color: theme.cardColor,
shape: RoundedRectangleBorder(
side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)),
borderRadius: BorderRadius.circular(10.r),
),
),
child: Row(
children: [
Padding(
padding: EdgeInsets.only(left: 12.w),
child: _buildIcon("login_psd"),
),
SizedBox(width: 8.w),
Expanded(
child: Obx(() => TextField(
controller: controller.psdController,
obscureText: controller.kr_obscureText.value,
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: '请输入密码',
hintStyle: theme.textTheme.bodySmall?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16.w),
suffixIcon: controller.kr_psdHasText.value
? IconButton(
icon: Icon(
controller.kr_obscureText.value
? Icons.visibility_off
: Icons.visibility,
color: const Color(0xFF999999),
),
onPressed: () {
controller.kr_obscureText.value = !controller.kr_obscureText.value;
},
)
: null,
),
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
)),
),
],
),
);
}
///
double _getBottomPadding() {
switch (controller.kr_loginStatus.value) {
@ -98,10 +159,7 @@ class KRLoginView extends GetView<KRLoginController> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Visibility(
visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check,
child: _buildBackButton(Theme.of(context)),
)),
_buildBackButton(Theme.of(context)),
_buildHeaderSection(Theme.of(context)),
_buildInputSection(context, Theme.of(context)),
_buildDynamicContent(Theme.of(context)),
@ -116,10 +174,7 @@ class KRLoginView extends GetView<KRLoginController> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Visibility(
visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check,
child: _buildBackButton(Theme.of(context)),
)),
_buildBackButton(Theme.of(context)),
_buildHeaderSection(Theme.of(context)),
_buildInputSection(context, Theme.of(context)),
_buildDynamicContent(Theme.of(context)),
@ -130,18 +185,29 @@ class KRLoginView extends GetView<KRLoginController> {
}
Widget _buildRegisterSendCodeView(BuildContext context) {
//
// -
final theme = Theme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Visibility(
visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check,
child: _buildBackButton(Theme.of(context)),
)),
_buildHeaderSection(Theme.of(context)),
_buildInputSection(context, Theme.of(context)),
_buildDynamicContent(Theme.of(context)),
_buildNextButton(controller.kr_getNextBtnText(), Theme.of(context)),
_buildBackButton(theme),
_buildHeaderSection(theme),
//
_buildRegistrationEmailInput(theme),
SizedBox(height: 8.w),
//
_buildRegistrationCodeInputWithSpacing(theme),
//
_buildRegistrationPasswordInput(theme),
SizedBox(height: 8.w),
//
_buildRegistrationConfirmPasswordInput(theme),
SizedBox(height: 8.w),
//
_buildInviteCodeInput(theme),
SizedBox(height: 17.w),
//
_buildNextButton('注册账号', theme),
SizedBox(height: _getBottomPadding()),
],
);
@ -152,10 +218,7 @@ class KRLoginView extends GetView<KRLoginController> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Visibility(
visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check,
child: _buildBackButton(Theme.of(context)),
)),
_buildBackButton(Theme.of(context)),
_buildHeaderSection(Theme.of(context)),
_buildInputSection(context, Theme.of(context)),
Column(
@ -178,10 +241,7 @@ class KRLoginView extends GetView<KRLoginController> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Visibility(
visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check,
child: _buildBackButton(Theme.of(context)),
)),
_buildBackButton(Theme.of(context)),
_buildHeaderSection(Theme.of(context)),
_buildInputSection(context, Theme.of(context)),
_buildDynamicContent(Theme.of(context)),
@ -196,10 +256,7 @@ class KRLoginView extends GetView<KRLoginController> {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Obx(() => Visibility(
visible: controller.kr_loginStatus.value != KRLoginProgressStatus.kr_check,
child: _buildBackButton(Theme.of(context)),
)),
_buildBackButton(Theme.of(context)),
_buildHeaderSection(Theme.of(context)),
_buildInputSection(context, Theme.of(context)),
if (controller.kr_loginStatus.value == KRLoginProgressStatus.kr_forgetPsdSetPsd)
@ -670,7 +727,8 @@ class KRLoginView extends GetView<KRLoginController> {
Widget? content;
switch (controller.kr_loginStatus.value) {
case KRLoginProgressStatus.kr_check:
content = _buildAgreementText(theme);
// "忘记密码"
content = _buildLoginBtns(theme);
case KRLoginProgressStatus.kr_loginByCode:
case KRLoginProgressStatus.kr_loginByPsd:
content = _buildLoginBtns(theme);
@ -744,6 +802,34 @@ class KRLoginView extends GetView<KRLoginController> {
///
Widget _buildLoginBtns(ThemeData theme) {
// kr_check
if (controller.kr_loginStatus.value == KRLoginProgressStatus.kr_check) {
return SizedBox(
height: 24.w,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildHoverableTextButton(
text: 'login.forgotPassword'.tr,
onTap: () => controller.kr_loginStatus.value =
KRLoginProgressStatus.kr_forgetPsdSendCode,
theme: theme,
),
SizedBox(width: 16.w),
_buildHoverableTextButton(
text: '点我注册',
onTap: () {
//
controller.kr_loginStatus.value = KRLoginProgressStatus.kr_registerSendCode;
},
theme: theme,
),
],
),
);
}
//
return SizedBox(
height: 24.w,
child: Row(
@ -802,7 +888,8 @@ class KRLoginView extends GetView<KRLoginController> {
controller.kr_login();
break;
case KRLoginProgressStatus.kr_registerSendCode:
controller.kr_checkCode();
//
controller.kr_register();
break;
case KRLoginProgressStatus.kr_registerSetPsd:
controller.kr_register();
@ -981,8 +1068,21 @@ class KRLoginView extends GetView<KRLoginController> {
Widget _buildBackButton(ThemeData theme) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
controller.kr_back();
print('👆 返回按钮被点击了!');
print('👆 当前登录状态: ${controller.kr_loginStatus.value}');
// kr_check
//
if (controller.kr_loginStatus.value == KRLoginProgressStatus.kr_check) {
print('👆 调用 Get.back() 返回主页');
Get.back();
print('👆 Get.back() 调用完成');
} else {
//
print('👆 调用 controller.kr_back() 返回上一步');
controller.kr_back();
}
},
child: Padding(
padding: EdgeInsets.fromLTRB(0, 20.w, 0, 0),
@ -1095,4 +1195,293 @@ class KRLoginView extends GetView<KRLoginController> {
),
);
}
///
Widget _buildHoverableTextButton({
required String text,
required VoidCallback onTap,
required ThemeData theme,
}) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: StatefulBuilder(
builder: (context, setState) {
bool isHovering = false;
return MouseRegion(
onEnter: (_) => setState(() => isHovering = true),
onExit: (_) => setState(() => isHovering = false),
child: GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Text(
text,
style: theme.textTheme.bodySmall?.copyWith(
fontSize: 13.sp,
color: isHovering ? const Color(0xFF1796FF) : const Color(0xFF666666),
fontWeight: FontWeight.w500,
fontFamily: 'AlibabaPuHuiTi-Medium',
),
),
),
);
},
),
);
}
/// ========== ==========
///
Widget _buildRegistrationEmailInput(ThemeData theme) {
return Container(
width: double.infinity,
height: 52.w,
decoration: ShapeDecoration(
color: theme.cardColor,
shape: RoundedRectangleBorder(
side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)),
borderRadius: BorderRadius.circular(10.r),
),
),
child: Row(
children: [
Padding(
padding: EdgeInsets.only(left: 12.w),
child: _buildIcon("login_account"),
),
SizedBox(width: 8.w),
Expanded(
child: TextField(
controller: controller.accountController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
hintText: 'login.enterEmail'.tr,
hintStyle: theme.textTheme.bodySmall?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16.w),
),
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
),
),
Obx(() => Visibility(
visible: controller.kr_accountHasText.value,
child: GestureDetector(
onTap: () => controller.accountController.clear(),
child: Container(
height: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: _buildIcon("login_close"),
),
),
)),
],
),
);
}
///
Widget _buildRegistrationCodeInputWithSpacing(ThemeData theme) {
// Obx
final siteConfig = KRSiteConfigService();
final needVerification = siteConfig.isEmailVerificationEnabled() ||
siteConfig.isRegisterVerificationEnabled();
//
if (!needVerification) {
return SizedBox.shrink();
}
//
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildRegistrationCodeInput(theme),
SizedBox(height: 8.w),
],
);
}
///
Widget _buildRegistrationCodeInput(ThemeData theme) {
//
final siteConfig = KRSiteConfigService();
final needVerification = siteConfig.isEmailVerificationEnabled() ||
siteConfig.isRegisterVerificationEnabled();
//
if (!needVerification) {
return SizedBox.shrink();
}
//
return Container(
width: double.infinity,
height: 52.w,
decoration: ShapeDecoration(
color: theme.cardColor,
shape: RoundedRectangleBorder(
side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)),
borderRadius: BorderRadius.circular(10.r),
),
),
child: Row(
children: [
Padding(
padding: EdgeInsets.only(left: 12.w),
child: _buildIcon("login_code"),
),
SizedBox(width: 8.w),
Expanded(
child: TextField(
controller: controller.codeController,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: 'login.enterCode'.tr,
hintStyle: theme.textTheme.bodySmall?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16.w),
),
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
),
),
if (controller.kr_codeHasText.value)
GestureDetector(
onTap: () => controller.codeController.clear(),
child: Container(
height: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 8.w),
child: _buildIcon("login_close"),
),
),
_buildSendCodeButton(theme),
],
),
);
}
///
Widget _buildRegistrationPasswordInput(ThemeData theme) {
return Container(
width: double.infinity,
height: 52.w,
decoration: ShapeDecoration(
color: theme.cardColor,
shape: RoundedRectangleBorder(
side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)),
borderRadius: BorderRadius.circular(10.r),
),
),
child: Row(
children: [
Padding(
padding: EdgeInsets.only(left: 12.w),
child: _buildIcon("login_psd"),
),
SizedBox(width: 8.w),
Expanded(
child: Obx(() => TextField(
controller: controller.psdController,
obscureText: controller.kr_obscureText.value,
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: 'login.enterPassword'.tr,
hintStyle: theme.textTheme.bodySmall?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16.w),
suffixIcon: controller.kr_psdHasText.value
? IconButton(
icon: Icon(
controller.kr_obscureText.value
? Icons.visibility_off
: Icons.visibility,
color: const Color(0xFF999999),
),
onPressed: () {
controller.kr_obscureText.value = !controller.kr_obscureText.value;
},
)
: null,
),
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
)),
),
],
),
);
}
///
Widget _buildRegistrationConfirmPasswordInput(ThemeData theme) {
return Container(
width: double.infinity,
height: 52.w,
decoration: ShapeDecoration(
color: theme.cardColor,
shape: RoundedRectangleBorder(
side: BorderSide(width: 0.5, color: const Color(0xFFD2D2D2)),
borderRadius: BorderRadius.circular(10.r),
),
),
child: Row(
children: [
Padding(
padding: EdgeInsets.only(left: 12.w),
child: _buildIcon("login_psd"),
),
SizedBox(width: 8.w),
Expanded(
child: Obx(() => TextField(
controller: controller.agPsdController,
obscureText: controller.kr_obscureText.value,
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: 'login.reenterPassword'.tr,
hintStyle: theme.textTheme.bodySmall?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
border: InputBorder.none,
contentPadding: EdgeInsets.symmetric(vertical: 16.w),
suffixIcon: controller.kr_agPsdHasText.value
? IconButton(
icon: Icon(
controller.kr_obscureText.value
? Icons.visibility_off
: Icons.visibility,
color: const Color(0xFF999999),
),
onPressed: () {
controller.kr_obscureText.value = !controller.kr_obscureText.value;
},
)
: null,
),
style: theme.textTheme.bodyMedium?.copyWith(
fontSize: 14.sp,
fontFamily: 'AlibabaPuHuiTi-Regular',
),
)),
),
],
),
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:kaer_with_panels/app/localization/app_translations.dart';
import 'package:kaer_with_panels/app/utils/kr_log_util.dart';
import '../../../common/app_run_data.dart';
import '../../../common/app_config.dart';
import '../../../model/response/kr_already_subscribe.dart';
import '../../../model/response/kr_payment_methods.dart';
import '../../../routes/app_pages.dart';
@ -109,15 +110,20 @@ class KRPurchaseMembershipController extends GetxController {
///
Future<void> _iniUserInfo() async {
final either0 = await KRUserApi().kr_getUserInfo();
either0.fold(
(error) {
KRLogUtil.kr_e(error.msg, tag: 'AppRunData');
},
(userInfo) async {
_kr_balance = userInfo.balance;
},
);
// kr_getUserInfo
// 使 AppConfig
// final either0 = await KRUserApi().kr_getUserInfo();
// either0.fold(
// (error) {
// KRLogUtil.kr_e(error.msg, tag: 'AppRunData');
// },
// (userInfo) async {
// _kr_balance = userInfo.balance;
// },
// );
// 使 AppConfig
_kr_balance = AppConfig.kr_userBalance;
}
///

View File

@ -8,6 +8,7 @@ import '../controllers/kr_setting_controller.dart';
import '../../../themes/kr_theme_service.dart';
import '../../../localization/app_translations.dart';
import '../../../routes/app_pages.dart';
import '../../../common/app_run_data.dart';
class KRSettingView extends GetView<KRSettingController> {
const KRSettingView({Key? key}) : super(key: key);
@ -235,12 +236,33 @@ class KRSettingView extends GetView<KRSettingController> {
onChanged: (value) => controller.kr_helpImprove.value = value,
),
_kr_buildDivider(),
_kr_buildActionTile(
context,
title: AppTranslations.kr_userInfo.myAccount,
trailing: AppTranslations.kr_setting.goToDelete,
onTap: controller.kr_deleteAccount,
),
Obx(() {
final appRunData = KRAppRunData.getInstance();
final isLoggedIn = appRunData.kr_isLogin.value;
final isDeviceLogin = appRunData.isDeviceLogin();
if (!isLoggedIn) {
//
return SizedBox.shrink();
} else if (isDeviceLogin) {
// "点击这里登录/注册"
return _kr_buildActionTile(
context,
title: "登录/注册",
trailing: "",
onTap: () => Get.toNamed(Routes.MR_LOGIN),
);
} else {
//
final userEmail = appRunData.kr_account.value ?? AppTranslations.kr_userInfo.myAccount;
return _kr_buildActionTile(
context,
title: userEmail,
trailing: AppTranslations.kr_setting.goToDelete,
onTap: controller.kr_deleteAccount,
);
}
}),
_kr_buildDivider(),
// _kr_buildTitleTile(
// context,
@ -407,7 +429,7 @@ class KRSettingView extends GetView<KRSettingController> {
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
trailing: Text(

View File

@ -241,13 +241,29 @@ class KRSplashController extends GetxController {
}
//
await Future.delayed(const Duration(milliseconds: 200));
await Future.delayed(const Duration(milliseconds: 500));
//
final loginStatus = KRAppRunData.getInstance().kr_isLogin.value;
KRLogUtil.kr_i('启动完成,最终登录状态: $loginStatus', tag: 'SplashController');
final token = KRAppRunData.getInstance().kr_token;
final hasToken = token != null && token.isNotEmpty;
//
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
print('🎯 准备进入主页');
print('📊 最终登录状态: $loginStatus');
print('🎫 Token存在: $hasToken');
if (hasToken) {
print('🎫 Token前缀: ${token.substring(0, min(20, token.length))}...');
}
print('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
KRLogUtil.kr_i('🎯 准备进入主页', tag: 'SplashController');
KRLogUtil.kr_i('📊 最终登录状态: $loginStatus', tag: 'SplashController');
KRLogUtil.kr_i('🎫 Token存在: $hasToken', tag: 'SplashController');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SplashController');
//
Get.offAllNamed(Routes.KR_MAIN);
} catch (e) {
//

View File

@ -102,13 +102,15 @@ class KRStatisticsController extends GetxController {
return;
}
final either0 = await KRUserApi().kr_getUserInfo();
either0.fold(
(error) => KRCommonUtil.kr_showToast(error.msg),
(userInfo) {
// kr_homeController.kr_userId.value = userInfo.id.toString();
},
);
// kr_getUserInfo
// ID
// final either0 = await KRUserApi().kr_getUserInfo();
// either0.fold(
// (error) => KRCommonUtil.kr_showToast(error.msg),
// (userInfo) {
// // kr_homeController.kr_userId.value = userInfo.id.toString();
// },
// );
//
final DateTime now = DateTime.now();

View File

@ -199,15 +199,21 @@ class KRUserInfoController extends GetxController with KRAppBarOpacityMixin {
if (!KRAppRunData.getInstance().kr_isLogin.value) {
return;
}
final either0 = await KRUserApi().kr_getUserInfo();
either0.fold(
(error) {
KRLogUtil.kr_e(error.msg, tag: 'AppRunData');
},
(userInfo) async {
kr_balance.value = userInfo.balance.toDouble() / 100;
},
);
// kr_getUserInfo
// 使 AppConfig
// final either0 = await KRUserApi().kr_getUserInfo();
// either0.fold(
// (error) {
// KRLogUtil.kr_e(error.msg, tag: 'AppRunData');
// },
// (userInfo) async {
// kr_balance.value = userInfo.balance.toDouble() / 100;
// },
// );
// 使 AppConfig
kr_balance.value = AppConfig.kr_userBalance.toDouble() / 100;
}
/// 退

View File

@ -99,64 +99,115 @@ class KRUserInfoView extends GetView<KRUserInfoController> {
//
Widget _kr_buildBindingTip(BuildContext context) {
return Obx(() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
return Obx(() {
final appRunData = KRAppRunData.getInstance();
final isLoggedIn = appRunData.kr_isLogin.value;
final isDeviceLogin = appRunData.isDeviceLogin();
//
String displayText;
Color displayColor;
IconData displayIcon;
bool shouldShowLoginPrompt = false;
if (!isLoggedIn) {
//
displayText = AppTranslations.kr_userInfo.bindingTip;
displayColor = Theme.of(context).colorScheme.error;
displayIcon = Icons.info_outline;
shouldShowLoginPrompt = false;
} else if (isDeviceLogin) {
//
displayText = "登录/注册";
displayColor = const Color(0xFF1797FF); // 使
displayIcon = Icons.touch_app;
shouldShowLoginPrompt = true;
} else {
//
displayText = "${AppTranslations.kr_userInfo.myAccount} ${appRunData.kr_account.value}";
displayColor = Theme.of(context).textTheme.bodyMedium?.color ?? Colors.black;
displayIcon = Icons.info;
shouldShowLoginPrompt = false;
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: shouldShowLoginPrompt
? () {
//
Get.toNamed(Routes.MR_LOGIN);
}
: null,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 12.w),
decoration: shouldShowLoginPrompt
? BoxDecoration(
color: const Color(0xFF1797FF).withOpacity(0.05),
borderRadius: BorderRadius.circular(8.w),
)
: null,
child: Row(
children: [
Icon(
KRAppRunData.getInstance().kr_isLogin.value
? Icons.info
: Icons.info_outline,
color: !KRAppRunData.getInstance().kr_isLogin.value
? Theme.of(context).colorScheme.error
: Theme.of(context).textTheme.bodyMedium?.color,
displayIcon,
color: displayColor,
size: 16.w,
),
SizedBox(width: 8.w),
Expanded(
child: Text(
displayText,
style: KrAppTextStyle(
color: displayColor,
fontSize: 12,
fontWeight: shouldShowLoginPrompt ? FontWeight.w600 : FontWeight.normal,
),
),
),
if (shouldShowLoginPrompt)
Icon(
Icons.arrow_forward_ios,
size: 14.w,
color: displayColor,
),
],
),
),
),
// ID
Visibility(
visible: KRAppRunData.getInstance().kr_isLogin.value &&
AppConfig.getInstance().kr_is_daytime,
child: Padding(
padding: EdgeInsets.only(left: 16.w, bottom: 16.w),
child: Row(
children: [
Icon(
appRunData.isDeviceLogin()
? Icons.person_outline
: Icons.account_balance_wallet_outlined,
color: Theme.of(context).textTheme.bodyMedium?.color,
size: 16.w,
),
SizedBox(width: 8.w),
Text(
KRAppRunData.getInstance().kr_isLogin.value
? "${AppTranslations.kr_userInfo.myAccount} ${KRAppRunData().kr_account}"
: AppTranslations.kr_userInfo.bindingTip,
appRunData.isDeviceLogin()
? "游客ID${(appRunData.kr_userId.value ?? 0) + 10000}"
: "${AppTranslations.kr_userInfo.balance} ${controller.kr_balance.value.toString()}",
style: KrAppTextStyle(
color: !KRAppRunData.getInstance().kr_isLogin.value
? Theme.of(context).colorScheme.error
: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 12,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
),
//
Visibility(
visible: KRAppRunData.getInstance().kr_isLogin.value &&
AppConfig.getInstance().kr_is_daytime,
child: Padding(
padding: EdgeInsets.only(left: 16.w, bottom: 16.w),
child: Row(
children: [
Icon(
Icons.account_balance_wallet_outlined,
color: Theme.of(context).textTheme.bodyMedium?.color,
size: 16.w,
),
SizedBox(width: 8.w),
Text(
"${AppTranslations.kr_userInfo.balance} ${controller.kr_balance.value.toString()}",
style: KrAppTextStyle(
fontSize: 12,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
],
),
),
),
],
));
),
],
);
});
}
//
@ -574,49 +625,67 @@ class KRUserInfoView extends GetView<KRUserInfoController> {
//
Widget _kr_buildShortcutSection(BuildContext context) {
return Container(
margin: EdgeInsets.fromLTRB(16.w, 24.w, 16.w, 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_userInfo.shortcuts,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
return Obx(() {
final appRunData = KRAppRunData.getInstance();
final isLoggedIn = appRunData.kr_isLogin.value;
final isDeviceLogin = appRunData.isDeviceLogin();
//
final showDeviceManagement = isLoggedIn && !isDeviceLogin;
return Container(
margin: EdgeInsets.fromLTRB(16.w, 24.w, 16.w, 16.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
AppTranslations.kr_userInfo.shortcuts,
style: KrAppTextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).textTheme.bodyMedium?.color,
),
),
),
SizedBox(height: 12.w),
Column(
children: [
_kr_buildShortcutContainer(
icon: "my_ads",
title: AppTranslations.kr_userInfo.adBlock,
value: controller.kr_isAdBlockEnabled,
onChanged: controller.kr_toggleAdBlock,
context: context,
),
_kr_buildShortcutContainer(
icon: "my_dns",
title: AppTranslations.kr_userInfo.ndsUnlock,
value: controller.kr_isNDSUnlockEnabled,
onChanged: controller.kr_toggleNDSUnlock,
context: context,
),
_kr_buildShortcutContainer(
icon: "my_cn_us",
title: AppTranslations.kr_userInfo.contactUs,
onTap: () {
Get.toNamed(Routes.KR_CRISP);
},
context: context,
),
],
),
],
),
);
SizedBox(height: 12.w),
Column(
children: [
_kr_buildShortcutContainer(
icon: "my_ads",
title: AppTranslations.kr_userInfo.adBlock,
value: controller.kr_isAdBlockEnabled,
onChanged: controller.kr_toggleAdBlock,
context: context,
),
_kr_buildShortcutContainer(
icon: "my_dns",
title: AppTranslations.kr_userInfo.ndsUnlock,
value: controller.kr_isNDSUnlockEnabled,
onChanged: controller.kr_toggleNDSUnlock,
context: context,
),
if (showDeviceManagement)
_kr_buildShortcutContainer(
icon: "my_dns",
title: "设备管理",
onTap: () {
Get.toNamed(Routes.KR_DEVICE_MANAGEMENT);
},
context: context,
),
_kr_buildShortcutContainer(
icon: "my_cn_us",
title: AppTranslations.kr_userInfo.contactUs,
onTap: () {
Get.toNamed(Routes.KR_CRISP);
},
context: context,
),
],
),
],
),
);
});
}
//

View File

@ -34,6 +34,10 @@ class BaseResponse<T> {
final decrypted = KRAesUtil.decryptData(cipherText, nonce, AppConfig.kr_encryptionKey);
body = jsonDecode(decrypted);
KRLogUtil.kr_i('✅ 解密成功', tag: 'BaseResponse');
// 便
final bodyStr = jsonEncode(body);
KRLogUtil.kr_i('📦 解密后数据(完整): $bodyStr', tag: 'BaseResponse');
} catch (e) {
KRLogUtil.kr_e('❌ 解密失败: $e,使用原始数据', tag: 'BaseResponse');
body = dataMap;

View File

@ -25,7 +25,7 @@ import '../utils/kr_log_util.dart';
// import 'package:video/app/utils/log_util.dart';
///
enum HttpMethod { GET, POST, DELETE }
enum HttpMethod { GET, POST, DELETE, PUT }
///
class HttpUtil {
@ -167,6 +167,15 @@ class HttpUtil {
headers: headers, //
),
);
} else if (method == HttpMethod.PUT) {
responseTemp = await _dio.put<Map<String, dynamic>>(
path,
data: map,
options: Options(
contentType: "application/json",
headers: headers, //
),
);
} else {
responseTemp = await _dio.post<Map<String, dynamic>>(
path,

View File

@ -32,6 +32,8 @@ import '../modules/kr_order_status/bindings/kr_order_status_binding.dart';
import '../modules/kr_order_status/views/kr_order_status_view.dart';
import '../modules/kr_splash/bindings/kr_splash_binding.dart';
import '../modules/kr_splash/views/kr_splash_view.dart';
import '../modules/kr_device_management/bindings/kr_device_management_binding.dart';
import '../modules/kr_device_management/views/kr_device_management_view.dart';
part 'app_routes.dart';
@ -121,5 +123,10 @@ class AppPages {
page: () => const KRCrispView(),
binding: KRCrispBinding(),
),
GetPage(
name: _Paths.KR_DEVICE_MANAGEMENT,
page: () => const KRDeviceManagementView(),
binding: KRDeviceManagementBinding(),
),
];
}

View File

@ -20,6 +20,7 @@ abstract class Routes {
static const KR_WEBVIEW = _Paths.KR_WEBVIEW;
static const KR_ORDER_STATUS = '/kr-order-status';
static const KR_CRISP = _Paths.KR_CRISP;
static const KR_DEVICE_MANAGEMENT = _Paths.KR_DEVICE_MANAGEMENT;
}
abstract class _Paths {
@ -40,4 +41,5 @@ abstract class _Paths {
static const KR_DELETE_ACCOUNT = '/kr-delete-account';
static const KR_WEBVIEW = '/kr_webview';
static const KR_CRISP = '/kr-crisp';
static const KR_DEVICE_MANAGEMENT = '/kr-device-management';
}

View File

@ -3,21 +3,17 @@ abstract class Api {
///
static const String kr_isRegister = "/v1/app/auth/check";
/// 1024
static const String kr_register = "/v1/app/auth/register";
///
static const String kr_register = "/v1/auth/register";
///
static const String kr_checkVerificationCode =
"/v1/common/check_verification_code";
static const String kr_checkVerificationCode = "/v1/auth/check-code";
///
static const String kr_sendPhoneCode = "/v1/common/send_sms_code";
///
static const String kr_sendEmailCode = "/v1/common/send_code";
///
static const String kr_sendCode = "/v1/auth/send-code";
///
static const String kr_login = "/v1/app/auth/login";
static const String kr_login = "/v1/auth/login";
///
/// OmnOem ppanel.json
@ -29,8 +25,8 @@ abstract class Api {
/// -
static const String kr_setNewPsdByForgetPsd = "/v1/app/auth/reset_password";
///
static const String kr_nodeList = "/v1/app/node/list";
/// /
static const String kr_nodeList = "/v1/public/subscribe/node/list";
///
static const String kr_nodeGroupList = "/v1/app/node/rule_group_list";
@ -48,15 +44,15 @@ abstract class Api {
static const String kr_checkout = "/v1/app/order/checkout";
///
static const String kr_getPackageList = "/v1/app/subscribe/list";
static const String kr_getPackageList = "/v1/public/subscribe/list";
///
///
static const String kr_getAlreadySubscribe =
"/v1/app/subscribe/user/already_subscribe";
"/v1/public/user/subscribe";
///
/// OmnOem
static const String kr_userAvailableSubscribe =
"/v1/app/subscribe/user/available_subscribe";
"/v1/public/user/subscribe";
///
static const String kr_renewal = "/v1/app/order/renewal";
@ -66,7 +62,7 @@ abstract class Api {
static const String kr_orderDetail = "/v1/app/order/detail";
///
static const String kr_getMessageList = "/v1/app/announcement/list";
static const String kr_getMessageList = "/v1/public/announcement/list";
///
// static const String kr_getInviteData = "/v1/public/invite/code";
@ -74,9 +70,6 @@ abstract class Api {
///
static const String kr_config = "/v1/app/auth/config";
///
static const String kr_getUserInfo = "/v1/app/user/info";
/// 线
static const String kr_getUserOnlineTimeStatistics =
"/v1/app/user/online_time/statistics";
@ -96,4 +89,10 @@ abstract class Api {
///
static const String kr_resetSubscribePeriod =
"/v1/app/subscribe/reset/period";
///
static const String kr_getUserDevices = "/v1/public/user/devices";
///
static const String kr_unbindUserDevice = "/v1/public/user/unbind_device";
}

View File

@ -83,21 +83,22 @@ class KRUserApi {
return right(baseResponse.model);
}
Future<Either<HttpError, KRUserInfo>> kr_getUserInfo() async {
final Map<String, dynamic> data = <String, dynamic>{};
BaseResponse<KRUserInfo> baseResponse =
await HttpUtil.getInstance().request<KRUserInfo>(
Api.kr_getUserInfo,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(baseResponse.model);
}
//
// Future<Either<HttpError, KRUserInfo>> kr_getUserInfo() async {
// final Map<String, dynamic> data = <String, dynamic>{};
// BaseResponse<KRUserInfo> baseResponse =
// await HttpUtil.getInstance().request<KRUserInfo>(
// Api.kr_getUserInfo,
// data,
// method: HttpMethod.GET,
// isShowLoading: false,
// );
// if (!baseResponse.isSuccess) {
// return left(
// HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
// }
// return right(baseResponse.model);
// }
Future<Either<HttpError, KRAffiliateCount>> kr_getAffiliateCount() async {
final Map<String, dynamic> data = <String, dynamic>{};
@ -132,4 +133,64 @@ class KRUserApi {
return right(baseResponse.model);
}
///
Future<Either<HttpError, List<Map<String, dynamic>>>> kr_getUserDevices() async {
final Map<String, dynamic> data = <String, dynamic>{};
BaseResponse<dynamic> baseResponse =
await HttpUtil.getInstance().request<dynamic>(
Api.kr_getUserDevices,
data,
method: HttpMethod.GET,
isShowLoading: false,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
//
try {
// : { data: { list: [...], total: N } }
final responseData = baseResponse.model;
final List<Map<String, dynamic>> devices =
(responseData['list'] as List)
.map((item) => item as Map<String, dynamic>)
.toList();
return right(devices);
} catch (e) {
KRLogUtil.kr_e('解析设备列表失败: $e', tag: 'KRUserApi');
return left(HttpError(msg: '数据解析失败', code: -1));
}
}
///
Future<Either<HttpError, void>> kr_unbindUserDevice(String deviceId) async {
final Map<String, dynamic> data = <String, dynamic>{};
// ID
try {
data['id'] = int.parse(deviceId);
} catch (e) {
KRLogUtil.kr_e('设备ID格式错误: $deviceId', tag: 'KRUserApi');
return left(HttpError(msg: '设备ID格式错误', code: -1));
}
BaseResponse<dynamic> baseResponse =
await HttpUtil.getInstance().request<dynamic>(
Api.kr_unbindUserDevice,
data,
method: HttpMethod.PUT,
isShowLoading: true,
);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
return right(null);
}
}

View File

@ -23,22 +23,14 @@ import '../../common/app_config.dart';
import 'package:dio/dio.dart' as dio;
class KRAuthApi {
///
Future<Either<HttpError, bool>> kr_isRegister(
KRLoginType tpye, String account, String? areaCode) async {
///
Future<Either<HttpError, bool>> kr_isRegister(String email) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['method'] = tpye.value;
data['account'] = account;
final deviceId = await KRDeviceUtil().kr_getDeviceId();
KRLogUtil.kr_i('设备ID: $deviceId', tag: 'KRAuthApi');
data["identifier"] = deviceId;
data['email'] = email;
data["user_agent"] = _kr_getUserAgent();
data["os"] = _kr_getUserAgent();
if (areaCode != null) {
data['area_code'] = areaCode.toString();
}
final deviceId = await KRDeviceUtil().kr_getDeviceId();
KRLogUtil.kr_i('设备ID: $deviceId', tag: 'KRAuthApi');
data["identifier"] = deviceId;
BaseResponse<KRIsRegister> baseResponse = await HttpUtil.getInstance()
.request<KRIsRegister>(Api.kr_isRegister, data,
@ -52,29 +44,26 @@ class KRAuthApi {
return right(baseResponse.model.kr_isRegister);
}
///
/// +
Future<Either<HttpError, String>> kr_register(
KRLoginType tpye,
String account,
String? areaCode,
String? code,
String? password,
{String? inviteCode}) async {
String email,
String password,
{String? code,
String? inviteCode}) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['method'] = tpye.value;
data['account'] = account;
data['email'] = email;
data['password'] = password;
data["code"] = code;
data["identifier"] = await KRDeviceUtil().kr_getDeviceId();
data["os"] = _kr_getUserAgent();
//
if (code != null && code.isNotEmpty) {
data["code"] = code;
}
//
if (inviteCode != null && inviteCode.isNotEmpty) {
data["invite"] = inviteCode;
}
data["user_agent"] = _kr_getUserAgent();
if (tpye == KRLoginType.kr_telephone) {
data['area_code'] = areaCode;
}
BaseResponse<KRLoginData> baseResponse = await HttpUtil.getInstance()
.request<KRLoginData>(Api.kr_register, data,
@ -87,20 +76,14 @@ class KRAuthApi {
return right(baseResponse.model.kr_token.toString());
}
///
Future<Either<HttpError, bool>> kr_checkVerificationCode( KRLoginType tpye,
String account, String? areaCode, String code, int type) async {
///
Future<Either<HttpError, bool>> kr_checkVerificationCode(
String email, String code, int type) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['method'] = tpye.value;
if(tpye == KRLoginType.kr_telephone){
data['account'] = areaCode.toString() + account;
}else{
data['account'] = account;
}
data['email'] = email;
data['code'] = code;
data['type'] = type;
BaseResponse<KRIsRegister> baseResponse = await HttpUtil.getInstance()
.request<KRIsRegister>(Api.kr_checkVerificationCode, data,
method: HttpMethod.POST, isShowLoading: true);
@ -108,37 +91,23 @@ class KRAuthApi {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
if(baseResponse.model.kr_isRegister){
if (baseResponse.model.kr_isRegister) {
return right(true);
}else{
} else {
return left(HttpError(msg: "error.70001".tr, code: 70001));
}
}
///
Future<Either<HttpError, String>> kr_login(KRLoginType tpye, bool isPsd,
String account, String? areaCode, String? code, String? password) async {
/// +
Future<Either<HttpError, String>> kr_login(
String email, String password) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['method'] = tpye.value;
data['account'] = account;
data['email'] = email;
data['password'] = password;
final deviceId = await KRDeviceUtil().kr_getDeviceId();
final deviceId = await KRDeviceUtil().kr_getDeviceId();
KRLogUtil.kr_i('设备ID: $deviceId', tag: 'KRAuthApi');
data["identifier"] = deviceId;
data["user_agent"] = _kr_getUserAgent();
data["os"] = _kr_getUserAgent();
if (tpye == KRLoginType.kr_telephone) {
data['area_code'] = areaCode;
}
if (isPsd) {
data['password'] = password;
} else {
data["code"] = code;
}
data["identifier"] = deviceId;
BaseResponse<KRLoginData> baseResponse = await HttpUtil.getInstance()
.request<KRLoginData>(Api.kr_login, data,
@ -151,45 +120,30 @@ class KRAuthApi {
return right(baseResponse.model.kr_token.toString());
}
/// type 1 2
Future<Either<HttpError, bool>> kr_sendCode(
KRLoginType tpye, String account, String? areaCode, int type) async {
///
/// type: 1=, 2=, 3=
Future<Either<HttpError, bool>> kr_sendCode(String email, int type) async {
final Map<String, dynamic> data = <String, dynamic>{};
if (tpye == KRLoginType.kr_email) {
data['email'] = account;
} else {
data['telephone'] = account;
data['telephone_area_code'] = areaCode.toString();
}
data['email'] = email;
data['type'] = type;
BaseResponse<dynamic> baseResponse = await HttpUtil.getInstance()
.request<dynamic>(
tpye == KRLoginType.kr_email
? Api.kr_sendEmailCode
: Api.kr_sendPhoneCode,
data,
method: HttpMethod.POST,
isShowLoading: true);
.request<dynamic>(Api.kr_sendCode, data,
method: HttpMethod.POST, isShowLoading: true);
if (!baseResponse.isSuccess) {
return left(
HttpError(msg: baseResponse.retMsg, code: baseResponse.retCode));
}
// KRCommonUtil.kr_showToast(baseResponse.model.toString());
// KRIsRegister model = (baseResponse..model) as KRIsRegister;
return right(true);
}
///
Future<Either<HttpError, String>> kr_deleteAccount(KRLoginType tpye,
String code) async {
Future<Either<HttpError, String>> kr_deleteAccount(String code) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['method'] = tpye.value;
data['code'] = code;
BaseResponse<dynamic> baseResponse = await HttpUtil.getInstance()
BaseResponse<dynamic> baseResponse = await HttpUtil.getInstance()
.request<dynamic>(Api.kr_deleteAccount, data,
method: HttpMethod.DELETE, isShowLoading: true);
if (!baseResponse.isSuccess) {
@ -198,23 +152,16 @@ class KRAuthApi {
}
return right("");
}
/// -
Future<Either<HttpError, String>> kr_setNewPsdByForgetPsd(KRLoginType tpye,
String account, String? areaCode, String? code, String? password) async {
/// -
Future<Either<HttpError, String>> kr_setNewPsdByForgetPsd(
String email, String code, String password) async {
final Map<String, dynamic> data = <String, dynamic>{};
data['method'] = tpye.value;
data['account'] = account;
data['email'] = email;
data['password'] = password;
data["code"] = code;
data["identifier"] = await KRDeviceUtil().kr_getDeviceId();
data["user_agent"] = _kr_getUserAgent();
data["os"] = _kr_getUserAgent();
if (tpye == KRLoginType.kr_telephone) {
data['area_code'] = areaCode;
}
data["identifier"] = await KRDeviceUtil().kr_getDeviceId();
BaseResponse<KRLoginData> baseResponse = await HttpUtil.getInstance()
.request<KRLoginData>(Api.kr_setNewPsdByForgetPsd, data,

View File

@ -21,6 +21,11 @@ class KRAnnouncementService {
KRAnnouncementService._internal();
// 退
void kr_reset() {
_kr_hasShownAnnouncement = false;
}
//
Future<void> kr_checkAnnouncement() async {
if (_kr_hasShownAnnouncement) {

View File

@ -72,6 +72,9 @@ class KRSubscribeService {
///
final RxBool kr_isTrial = false.obs;
///
final RxBool kr_hasTrialNodes = false.obs;
///
final RxList<KRAlreadySubscribe> kr_alreadySubscribe =
<KRAlreadySubscribe>[].obs;
@ -247,9 +250,13 @@ class KRSubscribeService {
result.fold((error) {
kr_currentStatus.value = KRSubscribeServiceStatus.kr_error;
}, (nodes) {
//
//
kr_hasTrialNodes.value = nodes.isTryOut;
KRLogUtil.kr_i('切换订阅 - 节点列表包含试用节点: ${kr_hasTrialNodes.value}', tag: 'SubscribeService');
// 使
final listModel = KrOutboundsList();
listModel.processOutboundItems(nodes.list, kr_nodeGroups);
listModel.processOutboundItems(nodes.list, []);
// UI数据
groupOutboundList.value = listModel.groupOutboundList;
@ -277,24 +284,45 @@ class KRSubscribeService {
_kr_trialTimer?.cancel();
_kr_subscriptionTimer?.cancel();
//
final bool kr_isSubscribed = kr_currentSubscribe.value != null &&
kr_alreadySubscribe.any((subscribe) =>
kr_currentSubscribe.value?.id == subscribe.userSubscribeId);
if (kr_currentSubscribe.value == null) {
kr_isTrial.value = false;
return;
}
KRLogUtil.kr_i('当前订阅状态: ${kr_isSubscribed ? "已订阅" : "未订阅"}',
tag: 'SubscribeService');
KRLogUtil.kr_i('当前订阅ID: ${kr_currentSubscribe.value?.id}',
tag: 'SubscribeService');
KRLogUtil.kr_i(
'已订阅记录: ${kr_alreadySubscribe.map((s) => s.userSubscribeId).join(', ')}',
tag: 'SubscribeService');
// 使 API isTryOut
final currentSubscribe = kr_currentSubscribe.value!;
//
kr_isTrial.value = kr_currentSubscribe.value != null && !kr_isSubscribed;
// 1. 使 API isTryOut
kr_isTrial.value = currentSubscribe.isTryOut;
KRLogUtil.kr_i('步骤1 - API isTryOut 字段: ${currentSubscribe.isTryOut}', tag: 'SubscribeService');
KRLogUtil.kr_i('试用状态: ${kr_isTrial.value ? "" : ""}',
tag: 'SubscribeService');
// 2. API
if (!kr_isTrial.value) {
final bool kr_isSubscribed = kr_alreadySubscribe.any(
(subscribe) => currentSubscribe.id == subscribe.userSubscribeId
);
KRLogUtil.kr_i('步骤2 - 检查购买记录: $kr_isSubscribed', tag: 'SubscribeService');
//
if (!kr_isSubscribed) {
kr_isTrial.value = true;
KRLogUtil.kr_i('步骤2 - 没有购买记录,判定为试用', tag: 'SubscribeService');
}
}
// 3. "试用"
if (!kr_isTrial.value && currentSubscribe.name.contains('试用')) {
kr_isTrial.value = true;
KRLogUtil.kr_i('步骤3 - 订阅名称包含"试用"关键字,判定为试用', tag: 'SubscribeService');
}
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SubscribeService');
KRLogUtil.kr_i('当前订阅: ${currentSubscribe.name}(${currentSubscribe.id})', tag: 'SubscribeService');
KRLogUtil.kr_i('API isTryOut: ${currentSubscribe.isTryOut}', tag: 'SubscribeService');
KRLogUtil.kr_i('已订阅记录数: ${kr_alreadySubscribe.length}', tag: 'SubscribeService');
KRLogUtil.kr_i('已订阅ID列表: ${kr_alreadySubscribe.map((s) => s.userSubscribeId).join(', ')}', tag: 'SubscribeService');
KRLogUtil.kr_i('✅ 最终试用状态: ${kr_isTrial.value ? "试用" : "付费"}', tag: 'SubscribeService');
KRLogUtil.kr_i('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━', tag: 'SubscribeService');
if (kr_isTrial.value) {
//
@ -445,15 +473,10 @@ class KRSubscribeService {
},
);
final result = await kr_subscribeApi.kr_nodeGroupList();
result.fold(
(error) {
throw Exception('获取节点分组失败: ${error.msg}');
},
(groups) {
kr_nodeGroups.value = groups;
},
);
// 🔧 kr_nodeGroupList
// 使 is_try_out /
kr_nodeGroups.clear();
KRLogUtil.kr_i('已取消节点分组,将直接使用节点列表', tag: 'SubscribeService');
//
final currentSubscribeID = kr_currentSubscribe.value?.id;
@ -502,9 +525,26 @@ class KRSubscribeService {
}
}
// 2.
// 2.
if (selectedSubscribe == null) {
KRLogUtil.kr_i('开始查找已购买的套餐...', tag: 'SubscribeService');
KRLogUtil.kr_i('开始查找试用套餐...', tag: 'SubscribeService');
for (var subscribe in subscribes) {
KRLogUtil.kr_i('检查订阅: ${subscribe.name}(${subscribe.id}), 是否试用: ${subscribe.isTryOut}',
tag: 'SubscribeService');
if (subscribe.isTryOut) {
selectedSubscribe = subscribe;
KRLogUtil.kr_i('✅ 找到试用套餐,默认选择: ${selectedSubscribe.name}',
tag: 'SubscribeService');
break;
}
}
}
// 3.
if (selectedSubscribe == null) {
KRLogUtil.kr_i('没有试用套餐,查找已购买的套餐...', tag: 'SubscribeService');
KRLogUtil.kr_i('已订阅记录: ${kr_alreadySubscribe.map((s) => s.userSubscribeId).join(', ')}',
tag: 'SubscribeService');
@ -524,10 +564,10 @@ class KRSubscribeService {
}
}
// 3.
// 4.
if (selectedSubscribe == null) {
selectedSubscribe = subscribes.first;
KRLogUtil.kr_i('没有已购买的套餐,选择第一个: ${selectedSubscribe.name}',
KRLogUtil.kr_i('没有找到匹配的套餐,选择第一个: ${selectedSubscribe.name}',
tag: 'SubscribeService');
}
@ -545,9 +585,13 @@ class KRSubscribeService {
(nodes) => nodes,
);
//
//
kr_hasTrialNodes.value = nodes.isTryOut;
KRLogUtil.kr_i('节点列表包含试用节点: ${kr_hasTrialNodes.value}', tag: 'SubscribeService');
// 使
final listModel = KrOutboundsList();
listModel.processOutboundItems(nodes.list, kr_nodeGroups);
listModel.processOutboundItems(nodes.list, []);
// UI数据
groupOutboundList.value = listModel.groupOutboundList;
@ -596,6 +640,7 @@ class KRSubscribeService {
Future<void> kr_clearCutNodeData() async {
kr_isLastDayOfSubscription.value = false;
kr_isTrial.value = false;
kr_hasTrialNodes.value = false;
kr_subscriptionRemainingTime.value = '';
kr_trialRemainingTime.value = '';
@ -618,4 +663,45 @@ class KRSubscribeService {
///
KRUserAvailableSubscribeItem? get kr_getCurrentSubscribe =>
kr_currentSubscribe.value;
///
///
List<KROutboundItem> get kr_freeNodes {
if (!kr_hasTrialNodes.value) {
return [];
}
//
// kr_isTrial=false kr_hasTrialNodes=true
if (kr_isTrial.value) {
KRLogUtil.kr_i('返回免费节点: ${allList.length}', tag: 'SubscribeService');
return allList.toList();
} else {
KRLogUtil.kr_i('试用已结束,不显示免费节点', tag: 'SubscribeService');
return [];
}
}
///
///
///
List<KROutboundItem> get kr_paidNodes {
if (kr_isTrial.value) {
KRLogUtil.kr_i('当前是试用状态,不显示付费节点', tag: 'SubscribeService');
return [];
} else {
KRLogUtil.kr_i('返回付费节点: ${allList.length}', tag: 'SubscribeService');
return allList.toList();
}
}
///
bool get kr_shouldShowFreeTab {
return kr_hasTrialNodes.value && kr_isTrial.value;
}
///
bool get kr_shouldShowPaidTab {
return !kr_isTrial.value;
}
}