mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
改
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: false
|
||||||
|
description: 任务完成前的检查
|
||||||
|
---
|
||||||
|
|
||||||
|
1.每次完成任务之前必须跑一遍测试(比如说npm系的typecheck/lint)
|
||||||
|
|
||||||
|
2.每次完成任务之前必须跑一遍格式化(比如说npm系的prettier)
|
||||||
|
|
||||||
|
3.任务完成时详细输出你所做的事情和需要用户确认的问题(如有)
|
||||||
|
|
||||||
|
4.每次规划任务必须按照步骤来,每个步骤结束后遵循第三条规定。
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { HolderOutlined, DeleteOutlined } from '@ant-design/icons'
|
import { HolderOutlined } from '@ant-design/icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import RuleComponent from './autoScore/ruleComponent'
|
import RuleComponent from './autoScore/ruleComponent'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
InputNumber,
|
|
||||||
Button,
|
Button,
|
||||||
message,
|
message,
|
||||||
Table,
|
Table,
|
||||||
@@ -297,50 +296,6 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
setActionList([])
|
setActionList([])
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddTrigger = () => {
|
|
||||||
const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1
|
|
||||||
const defaultTrigger = TRIGGER_DEFINITIONS[0]
|
|
||||||
if (!defaultTrigger) {
|
|
||||||
messageApi.error(t('autoScore.noTriggerAvailable'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setTriggerList((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
id: nextId,
|
|
||||||
eventName: defaultTrigger.eventName,
|
|
||||||
value: '',
|
|
||||||
relation: 'AND'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAddAction = () => {
|
|
||||||
const nextId = actionList.length ? Math.max(...actionList.map((a) => a.id)) + 1 : 1
|
|
||||||
const defaultAction = ACTION_DEFINITIONS[0]
|
|
||||||
if (!defaultAction) {
|
|
||||||
messageApi.error(t('autoScore.noActionAvailable'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setActionList((prev) => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
id: nextId,
|
|
||||||
eventName: defaultAction.eventName,
|
|
||||||
value: '',
|
|
||||||
reason: ''
|
|
||||||
}
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemoveAction = (id: number) => {
|
|
||||||
setActionList((prev) => prev.filter((a) => a.id !== id))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleActionChange = (id: number, field: keyof ActionItem, value: string | number) => {
|
|
||||||
setActionList((prev) => prev.map((a) => (a.id === id ? { ...a, [field]: value } : a)))
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns: ColumnsType<AutoScoreRule> = [
|
const columns: ColumnsType<AutoScoreRule> = [
|
||||||
{
|
{
|
||||||
key: 'drag',
|
key: 'drag',
|
||||||
@@ -443,61 +398,6 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const renderActionItem = (action: ActionItem) => (
|
|
||||||
<div
|
|
||||||
key={action.id}
|
|
||||||
style={{
|
|
||||||
padding: '12px',
|
|
||||||
border: '1px solid #d9d9d9',
|
|
||||||
borderRadius: '6px',
|
|
||||||
backgroundColor: 'var(--ss-card-bg)',
|
|
||||||
marginBottom: '8px'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Space wrap>
|
|
||||||
<Select
|
|
||||||
value={action.eventName}
|
|
||||||
onChange={(value) => handleActionChange(action.id, 'eventName', value)}
|
|
||||||
style={{ width: 150 }}
|
|
||||||
options={ACTION_DEFINITIONS.map((d) => ({
|
|
||||||
label: t(d.labelKey),
|
|
||||||
value: d.eventName
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
{action.eventName === 'add_score' && (
|
|
||||||
<InputNumber
|
|
||||||
value={action.value ? parseInt(action.value) : undefined}
|
|
||||||
onChange={(value) => handleActionChange(action.id, 'value', String(value || 0))}
|
|
||||||
placeholder={t('autoScore.scorePlaceholder')}
|
|
||||||
min={-100}
|
|
||||||
max={100}
|
|
||||||
style={{ width: 120 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{action.eventName === 'add_tag' && (
|
|
||||||
<Input
|
|
||||||
value={action.value}
|
|
||||||
onChange={(e) => handleActionChange(action.id, 'value', e.target.value)}
|
|
||||||
placeholder={t('autoScore.tagNamePlaceholder')}
|
|
||||||
style={{ width: 200 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Input
|
|
||||||
value={action.reason}
|
|
||||||
onChange={(e) => handleActionChange(action.id, 'reason', e.target.value)}
|
|
||||||
placeholder={t('autoScore.reasonPlaceholder')}
|
|
||||||
style={{ width: 200 }}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
onClick={() => handleRemoveAction(action.id)}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '24px' }}>
|
<div style={{ padding: '24px' }}>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
@@ -527,8 +427,8 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<RuleComponent />
|
<RuleComponent />
|
||||||
|
|
||||||
{/* <Card
|
{/* <Card
|
||||||
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
||||||
title={t('autoScore.whenTriggered')}
|
title={t('autoScore.whenTriggered')}
|
||||||
>
|
>
|
||||||
@@ -547,7 +447,7 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Card> */}
|
</Card> */}
|
||||||
|
|
||||||
{/* <Card
|
{/* <Card
|
||||||
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
||||||
title={t('autoScore.triggeredActions')}
|
title={t('autoScore.triggeredActions')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useRef, useCallback, useEffect } from 'react'
|
import React, { useState, useRef, useCallback, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Button, Segmented, Input, Tag, message, Typography, InputNumber } from 'antd'
|
import { Button, Segmented, Input, Tag, message, Typography, InputNumber, Space } from 'antd'
|
||||||
import { PlusOutlined, UploadOutlined, FileExcelOutlined } from '@ant-design/icons'
|
import { PlusOutlined, UploadOutlined, FileExcelOutlined } from '@ant-design/icons'
|
||||||
import { OOBEBackground } from './OOBEBackground'
|
import { OOBEBackground } from './OOBEBackground'
|
||||||
import { useTheme } from '../../contexts/ThemeContext'
|
import { useTheme } from '../../contexts/ThemeContext'
|
||||||
@@ -13,7 +13,7 @@ interface oobeProps {
|
|||||||
onComplete: () => void
|
onComplete: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
type oobeStep = 'language' | 'theme' | 'students' | 'reasons' | 'start'
|
type oobeStep = 'language' | 'theme' | 'password' | 'students' | 'reasons' | 'start'
|
||||||
|
|
||||||
interface studentItem {
|
interface studentItem {
|
||||||
name: string
|
name: string
|
||||||
@@ -86,10 +86,12 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
|
|||||||
const [reasons, setReasons] = useState<reasonItem[]>([])
|
const [reasons, setReasons] = useState<reasonItem[]>([])
|
||||||
const [newReasonContent, setNewReasonContent] = useState('')
|
const [newReasonContent, setNewReasonContent] = useState('')
|
||||||
const [newReasonDelta, setNewReasonDelta] = useState(1)
|
const [newReasonDelta, setNewReasonDelta] = useState(1)
|
||||||
|
const [adminPassword, setAdminPassword] = useState('')
|
||||||
|
const [pointsPassword, setPointsPassword] = useState('')
|
||||||
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const steps: oobeStep[] = ['language', 'theme', 'students', 'reasons', 'start']
|
const steps: oobeStep[] = ['language', 'theme', 'password', 'students', 'reasons', 'start']
|
||||||
const stepIndex = steps.indexOf(currentStep) + 1
|
const stepIndex = steps.indexOf(currentStep) + 1
|
||||||
const totalSteps = steps.length
|
const totalSteps = steps.length
|
||||||
|
|
||||||
@@ -315,6 +317,13 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (adminPassword || pointsPassword) {
|
||||||
|
await (window as any).api.authSetPasswords({
|
||||||
|
adminPassword: adminPassword || null,
|
||||||
|
pointsPassword: pointsPassword || null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const res = await (window as any).api.setSetting('is_wizard_completed', true)
|
const res = await (window as any).api.setSetting('is_wizard_completed', true)
|
||||||
if (!res?.success) throw new Error(res?.message || 'failed')
|
if (!res?.success) throw new Error(res?.message || 'failed')
|
||||||
|
|
||||||
@@ -436,6 +445,52 @@ export const OOBE: React.FC<oobeProps> = ({ visible, onComplete }) => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
case 'password':
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
|
<Typography.Text type="secondary">
|
||||||
|
{t('oobe.steps.password.description')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
<div>
|
||||||
|
<Typography.Text strong>{t('oobe.steps.password.adminPassword')}</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
style={{ display: 'block', fontSize: 12, marginTop: 4 }}
|
||||||
|
>
|
||||||
|
{t('oobe.steps.password.adminPasswordHint')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Input.Password
|
||||||
|
value={adminPassword}
|
||||||
|
onChange={(e) => setAdminPassword(e.target.value)}
|
||||||
|
placeholder={t('oobe.steps.password.passwordPlaceholder')}
|
||||||
|
maxLength={6}
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Typography.Text strong>{t('oobe.steps.password.pointsPassword')}</Typography.Text>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
style={{ display: 'block', fontSize: 12, marginTop: 4 }}
|
||||||
|
>
|
||||||
|
{t('oobe.steps.password.pointsPasswordHint')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Input.Password
|
||||||
|
value={pointsPassword}
|
||||||
|
onChange={(e) => setPointsPassword(e.target.value)}
|
||||||
|
placeholder={t('oobe.steps.password.passwordPlaceholder')}
|
||||||
|
maxLength={6}
|
||||||
|
style={{ marginTop: 8 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{t('oobe.steps.password.hint')}
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
case 'students':
|
case 'students':
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { Space, Input, InputNumber, Select, Button } from 'antd'
|
import { Space, Input, InputNumber, Select, Button } from 'antd'
|
||||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'
|
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import type { ActionItem } from './types'
|
|
||||||
import { ACTION_DEFINITIONS, SCORE_RANGE } from './constants'
|
const SCORE_RANGE = { MIN: -999, MAX: 999 }
|
||||||
|
|
||||||
|
const ACTION_DEFINITIONS = [
|
||||||
|
{ eventName: 'add_score', labelKey: 'autoScore.actionAddScore' },
|
||||||
|
{ eventName: 'add_tag', labelKey: 'autoScore.actionAddTag' }
|
||||||
|
]
|
||||||
|
|
||||||
|
interface ActionItem {
|
||||||
|
id: number
|
||||||
|
eventName: string
|
||||||
|
value?: string
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
interface ActionComponentProps {
|
interface ActionComponentProps {
|
||||||
actions: ActionItem[]
|
actions: ActionItem[]
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
.queryBuilder {
|
.queryBuilder {
|
||||||
--querybuilder-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
--querybuilder-font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
|
||||||
|
sans-serif;
|
||||||
--querybuilder-background: var(--ss-card-bg, #ffffff);
|
--querybuilder-background: var(--ss-card-bg, #ffffff);
|
||||||
--querybuilder-color: var(--ss-text-main, rgba(0, 0, 0, 0.85));
|
--querybuilder-color: var(--ss-text-main, rgba(0, 0, 0, 0.85));
|
||||||
--querybuilder-border-radius: 6px;
|
--querybuilder-border-radius: 6px;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { RuleGroupType, Field, RuleType } from 'react-querybuilder'
|
import type { RuleGroupType, Field, RuleType } from 'react-querybuilder'
|
||||||
import {defaultOperators} from 'react-querybuilder'
|
import { defaultOperators } from 'react-querybuilder'
|
||||||
import { fetchAllTags } from '../TagEditorDialog'
|
import { fetchAllTags } from '../TagEditorDialog'
|
||||||
|
|
||||||
const tags = await fetchAllTags()
|
const tags = await fetchAllTags()
|
||||||
@@ -21,7 +21,7 @@ export interface AutoScoreRuleData {
|
|||||||
actions: AutoScoreAction[]
|
actions: AutoScoreAction[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validator = (r: RuleType) => !!r.value;
|
export const validator = (r: RuleType) => !!r.value
|
||||||
|
|
||||||
export const getFields = (t: (key: string) => string): Field[] => [
|
export const getFields = (t: (key: string) => string): Field[] => [
|
||||||
{
|
{
|
||||||
@@ -31,18 +31,30 @@ export const getFields = (t: (key: string) => string): Field[] => [
|
|||||||
valueEditorType: 'multiselect',
|
valueEditorType: 'multiselect',
|
||||||
values: tags.map((tag) => tag.name),
|
values: tags.map((tag) => tag.name),
|
||||||
defaultValue: tags.length > 0 ? [tags[0].name] : [],
|
defaultValue: tags.length > 0 ? [tags[0].name] : [],
|
||||||
operators: defaultOperators.filter((op) => op.name === 'in'),
|
operators: defaultOperators.filter((op) => op.name === 'in')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'interval_time_passed',
|
name: 'interval_time_passed',
|
||||||
label: t('triggers.intervalTime.label'),
|
label: t('triggers.intervalTime.label'),
|
||||||
matchModes: ['all'],
|
matchModes: ['all'],
|
||||||
subproperties: [
|
subproperties: [
|
||||||
{ name: 'month', label: t('triggers.intervalTime.monthLabel'), inputType: 'number', datatype: 'month', operators: ['='] },
|
{
|
||||||
/* { name: 'week', label: t('triggers.intervalTime.weekLabel'), inputType: 'week', datatype: 'week', operators: ['='] },
|
name: 'month',
|
||||||
*/ { name: 'time', label: t('triggers.intervalTime.timeLabel'), inputType: 'time', datatype: 'time', operators: ['='] },
|
label: t('triggers.intervalTime.monthLabel'),
|
||||||
],
|
inputType: 'number',
|
||||||
},
|
datatype: 'month',
|
||||||
|
operators: ['=']
|
||||||
|
},
|
||||||
|
/* { name: 'week', label: t('triggers.intervalTime.weekLabel'), inputType: 'week', datatype: 'week', operators: ['='] },
|
||||||
|
*/ {
|
||||||
|
name: 'time',
|
||||||
|
label: t('triggers.intervalTime.timeLabel'),
|
||||||
|
inputType: 'time',
|
||||||
|
datatype: 'time',
|
||||||
|
operators: ['=']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export const defaultQuery: RuleGroupType = {
|
export const defaultQuery: RuleGroupType = {
|
||||||
@@ -50,10 +62,10 @@ export const defaultQuery: RuleGroupType = {
|
|||||||
rules: [{ field: 'interval_time_passed', operator: '=', value: '1440' }]
|
rules: [{ field: 'interval_time_passed', operator: '=', value: '1440' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function queryToAutoScoreRule(query: RuleGroupType): AutoScoreRuleData {
|
export function queryToAutoScoreRule(_query: RuleGroupType): AutoScoreRuleData {
|
||||||
const triggers: AutoScoreTrigger[] = []
|
const triggers: AutoScoreTrigger[] = []
|
||||||
|
|
||||||
/* const processRuleGroup = (group: RuleGroupType, relation: 'AND' | 'OR' = 'AND') => {
|
/* const processRuleGroup = (group: RuleGroupType, relation: 'AND' | 'OR' = 'AND') => {
|
||||||
group.rules.forEach((rule, index) => {
|
group.rules.forEach((rule, index) => {
|
||||||
if ('rules' in rule) {
|
if ('rules' in rule) {
|
||||||
processRuleGroup(rule, group.combinator === 'and' ? 'AND' : 'OR')
|
processRuleGroup(rule, group.combinator === 'and' ? 'AND' : 'OR')
|
||||||
|
|||||||
@@ -50,6 +50,16 @@
|
|||||||
"title": "Set Theme",
|
"title": "Set Theme",
|
||||||
"description": "Choose your preferred interface appearance",
|
"description": "Choose your preferred interface appearance",
|
||||||
"mode": "Mode",
|
"mode": "Mode",
|
||||||
|
"password": {
|
||||||
|
"title": "Set Password",
|
||||||
|
"description": "Set login password to protect your data (optional)",
|
||||||
|
"adminPassword": "Admin Password",
|
||||||
|
"adminPasswordHint": "Full access to all features, 6 digits",
|
||||||
|
"pointsPassword": "Points Password",
|
||||||
|
"pointsPasswordHint": "Points operations only, 6 digits",
|
||||||
|
"passwordPlaceholder": "Enter 6 digits",
|
||||||
|
"hint": "Leave empty to skip, you can configure in settings later"
|
||||||
|
},
|
||||||
"lightMode": "Light",
|
"lightMode": "Light",
|
||||||
"darkMode": "Dark",
|
"darkMode": "Dark",
|
||||||
"primaryColor": "Primary Color",
|
"primaryColor": "Primary Color",
|
||||||
@@ -111,6 +121,7 @@
|
|||||||
"language": "Language",
|
"language": "Language",
|
||||||
"languageHint": "Select interface display language",
|
"languageHint": "Select interface display language",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
|
"password": "Set Password",
|
||||||
"themeMode": "Mode",
|
"themeMode": "Mode",
|
||||||
"lightMode": "Light",
|
"lightMode": "Light",
|
||||||
"darkMode": "Dark",
|
"darkMode": "Dark",
|
||||||
|
|||||||
@@ -50,6 +50,16 @@
|
|||||||
"title": "设置主题",
|
"title": "设置主题",
|
||||||
"description": "选择您喜欢的界面外观",
|
"description": "选择您喜欢的界面外观",
|
||||||
"mode": "模式",
|
"mode": "模式",
|
||||||
|
"password": {
|
||||||
|
"title": "设置密码",
|
||||||
|
"description": "设置登录密码以保护您的数据(可选)",
|
||||||
|
"adminPassword": "管理密码",
|
||||||
|
"adminPasswordHint": "可管理所有功能,6位数字",
|
||||||
|
"pointsPassword": "积分密码",
|
||||||
|
"pointsPasswordHint": "仅可操作积分,6位数字",
|
||||||
|
"passwordPlaceholder": "请输入6位数字",
|
||||||
|
"hint": "留空则不设置密码,之后可在设置中配置"
|
||||||
|
},
|
||||||
"lightMode": "浅色",
|
"lightMode": "浅色",
|
||||||
"darkMode": "深色",
|
"darkMode": "深色",
|
||||||
"primaryColor": "主色",
|
"primaryColor": "主色",
|
||||||
@@ -111,6 +121,7 @@
|
|||||||
"language": "语言",
|
"language": "语言",
|
||||||
"languageHint": "选择界面显示语言",
|
"languageHint": "选择界面显示语言",
|
||||||
"theme": "主题",
|
"theme": "主题",
|
||||||
|
"password": "设置密码",
|
||||||
"themeMode": "模式",
|
"themeMode": "模式",
|
||||||
"lightMode": "浅色",
|
"lightMode": "浅色",
|
||||||
"darkMode": "深色",
|
"darkMode": "深色",
|
||||||
|
|||||||
Reference in New Issue
Block a user