This commit is contained in:
rustmailer
2026-05-10 03:43:50 +08:00
parent 2f8ba5ad40
commit 41322c8357
24 changed files with 217 additions and 116 deletions
+1 -1
View File
@@ -62,7 +62,7 @@ export function MailFilterPopover() {
size="sm"
variant="outline"
className={cn(
'h-6 rounded-none px-3 gap-1.5 transition-colors',
'h-6 rounded-none px-3 gap-1.5 transition-colors border-l-0',
activeCount > 0 && 'bg-primary/10 text-primary hover:bg-primary/20'
)}
>
+1 -1
View File
@@ -271,7 +271,7 @@ export function MailboxPopover() {
variant="outline"
disabled={disabled}
className={cn(
'h-6 rounded-none px-3 gap-1.5 transition-colors',
'h-6 rounded-none px-3 gap-1.5 transition-colors border-l-0',
selectedMailboxIds.length > 0 && 'bg-primary/10 text-primary border-primary/20'
)}
>
@@ -98,7 +98,7 @@ export function TagFilterPopover() {
size="sm"
variant="outline"
className={cn(
'h-6 gap-1.5 px-3 rounded-none',
'h-6 gap-1.5 px-3 rounded-none border-l-0',
selectedTags.length > 0 &&
'bg-primary/10 border-primary text-primary'
)}
+6 -3
View File
@@ -25,6 +25,7 @@ import { useCurrentUser } from '@/hooks/use-current-user'
import { PermissionsDialog } from './permissions-dialog'
import Logo from '@/assets/logo.svg'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import LongText from '@/components/long-text'
export function AccountAccessList() {
@@ -70,13 +71,15 @@ export function AccountAccessList() {
if (!email) return null
return (
<Card key={accountId} className="group hover:bg-accent/40 transition-all h-fit">
<Card key={accountId} className="group hover:bg-accent/40 transition-all h-fit min-w-0">
<CardHeader className="flex flex-row items-center gap-3 space-y-0 pb-3">
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-primary/10 text-primary text-sm font-bold shrink-0">
{email.charAt(0).toUpperCase()}
</div>
<div className="flex flex-col min-w-0">
<CardTitle className="text-sm font-semibold truncate">{email}</CardTitle>
<div className="flex flex-col min-w-0 flex-1">
<CardTitle className="text-sm font-semibold">
<LongText className="max-w-[248px]">{email}</LongText>
</CardTitle>
<CardDescription className="text-[10px] font-mono">
{t('settings.profile.account.id', { id: accountId })}
</CardDescription>
+126 -110
View File
@@ -16,6 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import * as React from "react"
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
@@ -35,9 +36,9 @@ import { useQuery } from "@tanstack/react-query"
import { useTranslation } from "react-i18next"
const formatMB = (bytes?: number) => {
if (!bytes) return "—";
return `${(bytes / 1024 / 1024).toFixed(0)} MB`;
};
if (!bytes) return "—"
return `${(bytes / 1024 / 1024).toFixed(0)} MB`
}
function BooleanBadge({ value }: { value: boolean }) {
const { t } = useTranslation()
@@ -58,13 +59,13 @@ function SettingRow({
description?: string
}) {
return (
<div className="py-1.5 border-b border-border/40 last:border-0">
<div className="grid grid-cols-[1fr_auto] items-center gap-3">
<div className="text-sm font-medium text-foreground/80 leading-tight font-mono">{label}</div>
<div className="text-sm text-right break-all leading-tight font-medium">{value}</div>
<div className="py-2.5 border-b border-border/40 last:border-0">
<div className="grid grid-cols-[1fr_auto] items-center gap-4">
<div className="text-xs font-medium text-muted-foreground font-mono">{label}</div>
<div className="text-sm text-right font-medium">{value}</div>
</div>
{description && (
<div className="mt-0.5 text-[11px] leading-tight text-muted-foreground">{description}</div>
<div className="mt-1 text-[11px] text-muted-foreground">{description}</div>
)}
</div>
)
@@ -82,31 +83,36 @@ function SettingsCard({
children: React.ReactNode
}) {
return (
<Card className="h-full shadow-sm">
<CardHeader className="flex flex-row items-center gap-2 py-3 bg-muted/20">
<Icon className="h-4 w-4 text-primary/70" />
<Card className="h-full">
<CardHeader className="flex flex-row items-center gap-3 py-4 px-5 border-b bg-muted/20">
<div className="p-1.5 rounded-md bg-primary/10">
<Icon className="h-3.5 w-3.5 text-primary" />
</div>
<div className="space-y-0.5">
<CardTitle className="text-sm font-semibold">{title}</CardTitle>
{description && <CardDescription className="text-[11px] leading-none">{description}</CardDescription>}
{description && (
<CardDescription className="text-[11px]">{description}</CardDescription>
)}
</div>
</CardHeader>
<CardContent className="pt-2 pb-1 px-4">{children}</CardContent>
<CardContent className="px-5 py-1">{children}</CardContent>
</Card>
)
}
function PageSkeleton() {
return (
<div className="p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{Array.from({ length: 6 }).map((_, i) => (
<Card key={i}>
<CardHeader className="py-3">
<CardHeader className="py-4 px-5 border-b">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-3 w-56" />
</CardHeader>
<CardContent className="space-y-2">
<CardContent className="px-5 py-3 space-y-3">
<Skeleton className="h-3 w-full" />
<Skeleton className="h-3 w-5/6" />
<Skeleton className="h-3 w-4/6" />
</CardContent>
</Card>
))}
@@ -121,109 +127,119 @@ export default function ServerConfigurationsPage() {
queryFn: get_system_configurations,
})
if (isLoading) return <ScrollArea className="h-full"><PageSkeleton /></ScrollArea>
if (isError || !data) {
return <div className="p-8 text-center text-sm text-destructive">{t("systemConfig.fields.loadError")}</div>
if (isError || (!isLoading && !data)) {
return (
<div className="p-8 text-center text-sm text-destructive">
{t("systemConfig.fields.loadError")}
</div>
)
}
return (
<div className="w-full max-w-6xl mx-auto">
<div className="w-full">
<ScrollArea className="h-full w-full">
<div className="px-6 pt-6 pb-2">
<div className="flex items-start gap-4 p-5 rounded-xl border bg-gradient-to-br from-secondary/50 to-background shadow-inner">
<div className="p-2 bg-primary/10 rounded-lg">
<InfoIcon className="h-5 w-5 text-primary" />
</div>
<div className="max-w-5xl mx-auto px-6 py-8 space-y-6">
<div className="flex items-start gap-3 p-4 rounded-xl border bg-muted/30">
<InfoIcon className="h-4 w-4 text-muted-foreground mt-0.5 shrink-0" />
<div>
<h4 className="text-base font-bold text-foreground">
{t("systemConfig.pageTitle")}
</h4>
<p className="text-xs text-muted-foreground mt-1 max-w-2xl leading-relaxed">
<h4 className="text-sm font-semibold">{t("systemConfig.pageTitle")}</h4>
<p className="text-xs text-muted-foreground mt-0.5 leading-relaxed">
{t("systemConfig.pageDescription")}
</p>
</div>
</div>
</div>
<div className="p-6 grid grid-cols-1 lg:grid-cols-2 gap-6">
<SettingsCard
icon={Server}
title={t("systemConfig.sections.network.title")}
description={t("systemConfig.sections.network.desc")}
>
<SettingRow label="BICHON_BIND_IP" value={data.bichon_bind_ip ?? "0.0.0.0"} />
<SettingRow label="BICHON_HTTP_PORT" value={data.bichon_http_port} />
<SettingRow label="BICHON_BASE_URL" value={data.bichon_base_url} />
<SettingRow label="BICHON_PUBLIC_URL" value={data.bichon_public_url} />
<SettingRow
label="BICHON_ENABLE_REST_HTTPS"
value={<BooleanBadge value={data.bichon_enable_rest_https} />}
/>
</SettingsCard>
<SettingsCard
icon={Mail}
title={t("systemConfig.sections.smtp.title", "SMTP Server")}
description={t("systemConfig.sections.smtp.desc", "Incoming mail reception settings")}
>
<SettingRow label="BICHON_ENABLE_SMTP" value={<BooleanBadge value={data.bichon_enable_smtp} />} />
<SettingRow label="BICHON_SMTP_PORT" value={data.bichon_smtp_port} />
<SettingRow label="BICHON_SMTP_ENCRYPTION" value={data.bichon_smtp_encryption} />
<SettingRow label="BICHON_SMTP_AUTH_REQUIRED" value={<BooleanBadge value={data.bichon_smtp_auth_required} />} />
</SettingsCard>
<SettingsCard
icon={Zap}
title={t("systemConfig.sections.performance.title")}
description={t("systemConfig.sections.performance.desc")}
>
<SettingRow label="BICHON_SYNC_CONCURRENCY" value={data.bichon_sync_concurrency ?? t("systemConfig.status.auto")} />
<SettingRow
label="BICHON_HTTP_COMPRESSION_ENABLED"
value={<BooleanBadge value={data.bichon_http_compression_enabled} />}
/>
</SettingsCard>
<SettingsCard
icon={Database}
title={t("systemConfig.sections.storage.title")}
description={t("systemConfig.sections.storage.desc")}
>
<SettingRow label="BICHON_ROOT_DIR" value={<span className="font-mono">{data.bichon_root_dir}</span>} />
<SettingRow label="BICHON_DATA_DIR" value={data.bichon_data_dir ? <span className="font-mono">{data.bichon_data_dir}</span> : "—"} />
<SettingRow label="BICHON_INDEX_DIR" value={data.bichon_index_dir ? <span className="font-mono">{data.bichon_index_dir}</span> : "—"} />
<SettingRow label="BICHON_METADATA_CACHE_SIZE" value={formatMB(data.bichon_metadata_cache_size)} />
<SettingRow label="BICHON_ENVELOPE_CACHE_SIZE" value={formatMB(data.bichon_envelope_cache_size)} />
</SettingsCard>
<SettingsCard
icon={ShieldCheck}
title={t("systemConfig.sections.security.title")}
description={t("systemConfig.sections.security.desc")}
>
<SettingRow
label="BICHON_ENCRYPT_PASSWORD_SET"
value={
data.bichon_encrypt_password_set ? (
<Badge variant="secondary" className="bg-emerald-500/10 text-emerald-600 border-emerald-500/20">
{t("systemConfig.status.configured")}
</Badge>
) : (
<Badge variant="destructive">{t("systemConfig.status.missing")}</Badge>
)
}
/>
<SettingRow
label="BICHON_WEBUI_TOKEN_EXPIRATION_HOURS"
value={`${data.bichon_webui_token_expiration_hours}h`}
/>
</SettingsCard>
<SettingsCard
icon={Activity}
title={t("systemConfig.sections.logging.title")}
description={t("systemConfig.sections.logging.desc")}
>
<SettingRow label="BICHON_LOG_LEVEL" value={<Badge variant="outline" className="uppercase">{data.bichon_log_level}</Badge>} />
<SettingRow label="BICHON_ANSI_LOGS" value={<BooleanBadge value={data.bichon_ansi_logs} />} />
<SettingRow label="BICHON_JSON_LOGS" value={<BooleanBadge value={data.bichon_json_logs} />} />
<SettingRow label="BICHON_LOG_TO_FILE" value={<BooleanBadge value={data.bichon_log_to_file} />} />
<SettingRow label="BICHON_MAX_SERVER_LOG_FILES" value={data.bichon_max_server_log_files} />
</SettingsCard>
{isLoading ? (
<PageSkeleton />
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
<SettingsCard
icon={Server}
title={t("systemConfig.sections.network.title")}
description={t("systemConfig.sections.network.desc")}
>
<SettingRow label="BICHON_BIND_IP" value={data!.bichon_bind_ip ?? "0.0.0.0"} />
<SettingRow label="BICHON_HTTP_PORT" value={data!.bichon_http_port} />
<SettingRow label="BICHON_BASE_URL" value={data!.bichon_base_url} />
<SettingRow label="BICHON_PUBLIC_URL" value={data!.bichon_public_url} />
<SettingRow
label="BICHON_ENABLE_REST_HTTPS"
value={<BooleanBadge value={data!.bichon_enable_rest_https} />}
/>
</SettingsCard>
<SettingsCard
icon={Mail}
title={t("systemConfig.sections.smtp.title")}
description={t("systemConfig.sections.smtp.desc")}
>
<SettingRow label="BICHON_ENABLE_SMTP" value={<BooleanBadge value={data!.bichon_enable_smtp} />} />
<SettingRow label="BICHON_SMTP_PORT" value={data!.bichon_smtp_port} />
<SettingRow label="BICHON_SMTP_ENCRYPTION" value={data!.bichon_smtp_encryption} />
<SettingRow label="BICHON_SMTP_AUTH_REQUIRED" value={<BooleanBadge value={data!.bichon_smtp_auth_required} />} />
</SettingsCard>
<SettingsCard
icon={Zap}
title={t("systemConfig.sections.performance.title")}
description={t("systemConfig.sections.performance.desc")}
>
<SettingRow label="BICHON_SYNC_CONCURRENCY" value={data!.bichon_sync_concurrency ?? t("systemConfig.status.auto")} />
<SettingRow
label="BICHON_HTTP_COMPRESSION_ENABLED"
value={<BooleanBadge value={data!.bichon_http_compression_enabled} />}
/>
</SettingsCard>
<SettingsCard
icon={Database}
title={t("systemConfig.sections.storage.title")}
description={t("systemConfig.sections.storage.desc")}
>
<SettingRow label="BICHON_ROOT_DIR" value={<span className="font-mono">{data!.bichon_root_dir}</span>} />
<SettingRow label="BICHON_DATA_DIR" value={data!.bichon_data_dir ? <span className="font-mono">{data!.bichon_data_dir}</span> : "—"} />
<SettingRow label="BICHON_INDEX_DIR" value={data!.bichon_index_dir ? <span className="font-mono">{data!.bichon_index_dir}</span> : "—"} />
<SettingRow label="BICHON_METADATA_CACHE_SIZE" value={formatMB(data!.bichon_metadata_cache_size)} />
<SettingRow label="BICHON_ENVELOPE_CACHE_SIZE" value={formatMB(data!.bichon_envelope_cache_size)} />
</SettingsCard>
<SettingsCard
icon={ShieldCheck}
title={t("systemConfig.sections.security.title")}
description={t("systemConfig.sections.security.desc")}
>
<SettingRow
label="BICHON_ENCRYPT_PASSWORD_SET"
value={
data!.bichon_encrypt_password_set ? (
<Badge variant="secondary" className="bg-emerald-500/10 text-emerald-600 border-emerald-500/20">
{t("systemConfig.status.configured")}
</Badge>
) : (
<Badge variant="destructive">{t("systemConfig.status.missing")}</Badge>
)
}
/>
<SettingRow
label="BICHON_WEBUI_TOKEN_EXPIRATION_HOURS"
value={`${data!.bichon_webui_token_expiration_hours}h`}
/>
</SettingsCard>
<SettingsCard
icon={Activity}
title={t("systemConfig.sections.logging.title")}
description={t("systemConfig.sections.logging.desc")}
>
<SettingRow label="BICHON_LOG_LEVEL" value={<Badge variant="outline" className="uppercase">{data!.bichon_log_level}</Badge>} />
<SettingRow label="BICHON_ANSI_LOGS" value={<BooleanBadge value={data!.bichon_ansi_logs} />} />
<SettingRow label="BICHON_JSON_LOGS" value={<BooleanBadge value={data!.bichon_json_logs} />} />
<SettingRow label="BICHON_LOG_TO_FILE" value={<BooleanBadge value={data!.bichon_log_to_file} />} />
<SettingRow label="BICHON_MAX_SERVER_LOG_FILES" value={data!.bichon_max_server_log_files} />
</SettingsCard>
</div>
)}
</div>
</ScrollArea>
+10
View File
@@ -24,6 +24,7 @@ import { KeyRound, Palette, SettingsIcon, ShieldCheck, UserCog, Waypoints } from
import { FixedHeader } from '@/components/layout/fixed-header'
import { useCurrentUser } from '@/hooks/use-current-user'
import { useTranslation } from 'react-i18next'
import { Separator } from '@/components/ui/separator'
export default function Settings() {
const { t } = useTranslation()
@@ -68,6 +69,15 @@ export default function Settings() {
<>
<FixedHeader />
<Main>
<div className='space-y-0.5'>
<h1 className='text-2xl font-bold tracking-tight md:text-3xl'>
{t('settings.header.title')}
</h1>
<p className='text-muted-foreground'>
{t('settings.header.description')}
</p>
</div>
<Separator className='my-4 lg:my-6' />
<div className='flex flex-1 flex-col space-y-2 md:space-y-2 overflow-hidden lg:flex-row lg:space-x-12 lg:space-y-0'>
<aside className='top-0 lg:sticky lg:w-1/5'>
<SidebarNav items={sidebarNavItems} />
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "تصفية الوكيل...",
"general": "عام",
"generateNewToken": "إنشاء رمز مميز جديد، القديم تنتهي صلاحيته على الفور.",
"header": {
"description": "إدارة ملفك الشخصي وعرض تكوينات النظام.",
"title": "الإعدادات"
},
"host": "المُضيف",
"id": "المعرّف",
"invalidPassword": "كلمة مرور غير صالحة",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "Filtrer Proxy...",
"general": "Generelt",
"generateNewToken": "Generer nyt token, det gamle udløber øjeblikkeligt.",
"header": {
"description": "Administrer din profil og se systemkonfigurationer.",
"title": "Indstillinger"
},
"host": "Vært",
"id": "Id",
"invalidPassword": "Ugyldig adgangskode",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "Proxys filtern...",
"general": "Allgemein",
"generateNewToken": "Einen neuen Token generieren, der alte läuft sofort ab.",
"header": {
"description": "Profil verwalten und Systemkonfigurationen anzeigen.",
"title": "Einstellungen"
},
"host": "Host",
"id": "Id",
"invalidPassword": "Ungültiges Passwort",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "Filter Proxy...",
"general": "General",
"generateNewToken": "Generate new token, old one expires immediately.",
"header": {
"description": "Manage your account profile and view system configurations.",
"title": "Settings"
},
"host": "Host",
"id": "Id",
"invalidPassword": "Invalid password",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "Filtrar proxies...",
"general": "General",
"generateNewToken": "Generar un nuevo token, el antiguo caducará inmediatamente.",
"header": {
"description": "Administra tu perfil y consulta las configuraciones del sistema.",
"title": "Configuración"
},
"host": "Host",
"id": "Id",
"invalidPassword": "Contraseña inválida",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "Suodata välityspalvelimet...",
"general": "Yleinen",
"generateNewToken": "Luo uusi tunnus, vanha vanhenee välittömästi.",
"header": {
"description": "Hallitse profiiliasi ja tarkastele järjestelmän määrityksiä.",
"title": "Asetukset"
},
"host": "Isäntä",
"id": "Id",
"invalidPassword": "Virheellinen salasana",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "Filtrer les Proxys...",
"general": "Général",
"generateNewToken": "Génère un nouveau jeton, l'ancien expirera immédiatement.",
"header": {
"description": "Gérez votre profil et consultez les configurations système.",
"title": "Paramètres"
},
"host": "Hôte",
"id": "Id",
"invalidPassword": "Mot de passe non valide",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "Filtra Proxy...",
"general": "Generale",
"generateNewToken": "Genera un nuovo token, quello vecchio scadrà immediatamente.",
"header": {
"description": "Gestisci il profilo e visualizza le configurazioni di sistema.",
"title": "Impostazioni"
},
"host": "Host",
"id": "Id",
"invalidPassword": "Password non valida",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "プロキシをフィルター...",
"general": "一般",
"generateNewToken": "新しいトークンを生成します。古いトークンは直ちに失効します。",
"header": {
"description": "プロフィールの管理とシステム設定の確認。",
"title": "設定"
},
"host": "ホスト",
"id": "ID",
"invalidPassword": "無効なパスワード",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "프록시 필터링...",
"general": "일반",
"generateNewToken": "새 토큰을 생성합니다. 이전 토큰은 즉시 무효화됩니다.",
"header": {
"description": "프로필 관리 및 시스템 설정 확인.",
"title": "설정"
},
"host": "호스트",
"id": "ID",
"invalidPassword": "유효하지 않은 비밀번호",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "Filter Proxy...",
"general": "Algemeen",
"generateNewToken": "Genereer nieuw token, het oude verloopt onmiddellijk.",
"header": {
"description": "Beheer uw profiel en bekijk systeemconfiguraties.",
"title": "Instellingen"
},
"host": "Host",
"id": "Id",
"invalidPassword": "Ongeldig wachtwoord",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "Filtrer Proxy...",
"general": "Generelt",
"generateNewToken": "Generer nytt token, det gamle utløper umiddelbart.",
"header": {
"description": "Administrer profilen din og se systemkonfigurasjoner.",
"title": "Innstillinger"
},
"host": "Vert",
"id": "Id",
"invalidPassword": "Ugyldig passord",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "Filtr Proxy...",
"general": "Ogólne",
"generateNewToken": "Wygeneruj nowy token, stary wygaśnie natychmiast.",
"header": {
"description": "Zarządzaj profilem i przeglądaj konfiguracje systemu.",
"title": "Ustawienia"
},
"host": "Host",
"id": "Id",
"invalidPassword": "Błędne hasło",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "Filtrar Proxy...",
"general": "Geral",
"generateNewToken": "Gera um novo token. O token antigo é imediatamente invalidado.",
"header": {
"description": "Gerencie seu perfil e visualize as configurações do sistema.",
"title": "Configurações"
},
"host": "Host",
"id": "ID",
"invalidPassword": "Senha Inválida",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "Фильтр прокси...",
"general": "Общие",
"generateNewToken": "Сгенерировать новый токен, старый немедленно истечет.",
"header": {
"description": "Управление профилем и просмотр конфигураций системы.",
"title": "Настройки"
},
"host": "Хост",
"id": "Id",
"invalidPassword": "Неверный пароль",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "Filtrera Proxy...",
"general": "Allmänt",
"generateNewToken": "Generera ny token, den gamla upphör att gälla omedelbart.",
"header": {
"description": "Hantera din profil och visa systemkonfigurationer.",
"title": "Inställningar"
},
"host": "Värd",
"id": "Id",
"invalidPassword": "Ogiltigt lösenord",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "篩選代理...",
"general": "一般",
"generateNewToken": "產生一個新權杖。舊權杖將立即失效。",
"header": {
"description": "管理您的個人資料並查看系統配置。",
"title": "設置"
},
"host": "主機",
"id": "ID",
"invalidPassword": "密碼無效",
+4
View File
@@ -1202,6 +1202,10 @@
"filterProxy": "筛选代理...",
"general": "常规",
"generateNewToken": "生成新令牌,旧令牌立即失效。",
"header": {
"description": "管理您的个人资料并查看系统配置。",
"title": "设置"
},
"host": "主机",
"id": "ID",
"invalidPassword": "密码无效",