feat(i18n): integrate internationalization into sidebar, title bar, and settings components
- Added useI18n hook to Sidebar and TitleBar components for translation support. - Updated sidebar navigation items to use translation keys instead of hardcoded labels. - Enhanced TitleBar buttons with localized titles for minimize, maximize, and close actions. - Expanded i18n constants to include new namespaces for sidebar and login. - Implemented translation messages for sidebar, settings, and login components in English, Chinese, and Japanese. - Refactored Knowledge and Skills pages to utilize useLocale for locale management. - Updated Login page to display localized text for form labels, placeholders, and error messages. - Enhanced Account and General settings panels with localized titles and descriptions. - Modified SettingMenu to use translation keys for menu items.
This commit is contained in:
@@ -1,2 +1,7 @@
|
|||||||
require('bytenode')
|
"use strict";
|
||||||
require('./main.jsc')
|
require("electron");
|
||||||
|
require("./main-B0AKNiSn.js");
|
||||||
|
require("electron-squirrel-startup");
|
||||||
|
require("electron-log");
|
||||||
|
require("bytenode");
|
||||||
|
require("axios");
|
||||||
|
|||||||
@@ -1 +1,137 @@
|
|||||||
"use strict";const r=require("electron");var i=(e=>(e.EXTERNAL_OPEN="external-open",e.APP_SET_FRAMELESS="app:set-frameless",e.APP_LOAD_PAGE="app:load-page",e.TAB_CREATE="tab:create",e.TAB_LIST="tab:list",e.TAB_NAVIGATE="tab:navigate",e.TAB_RELOAD="tab:reload",e.TAB_BACK="tab:back",e.TAB_FORWARD="tab:forward",e.TAB_SWITCH="tab:switch",e.TAB_CLOSE="tab:close",e.LOG_TO_MAIN="log-to-main",e.READ_FILE="read-file",e.INVOKE="ipc:invoke",e.INVOKE_ASYNC="ipc:invokeAsync",e.APP_MINIMIZE="app:minimize",e.APP_MAXIMIZE="app:maximize",e.APP_QUIT="app:quit",e.FILE_READ="file:read",e.FILE_WRITE="file:write",e.GET_WINDOW_ID="get-window-id",e.CUSTOM_EVENT="custom:event",e.TIME_UPDATE="time:update",e.RENDERER_IS_READY="renderer-ready",e.SHOW_CONTEXT_MENU="show-context-menu",e.START_A_DIALOGUE="start-a-dialogue",e.OPEN_WINDOW="open-window",e.LOG_DEBUG="log-debug",e.LOG_INFO="log-info",e.LOG_WARN="log-warn",e.LOG_ERROR="log-error",e.CONFIG_UPDATED="config-updated",e.SET_CONFIG="set-config",e.GET_CONFIG="get-config",e.UPDATE_CONFIG="update-config",e.SET_THEME_MODE="set-theme-mode",e.GET_THEME_MODE="get-theme-mode",e.IS_DARK_THEME="is-dark-theme",e.THEME_MODE_UPDATED="theme-mode-updated",e.EXECUTE_SCRIPT="execute-script",e.TASK_PROGRESS="task:progress",e.TASK_STARTED="task:started",e.TASK_COMPLETED="task:completed",e.OPEN_CHANNEL="open-channel",e.SCRIPT_LIST="script:list",e.SCRIPT_GET="script:get",e.SCRIPT_SAVE="script:save",e.SCRIPT_DELETE="script:delete",e.SCRIPT_TOGGLE="script:toggle",e.SCRIPT_RUN="script:run",e.SCRIPT_RECORD_START="script:record-start",e.SCRIPT_RECORD_STOP="script:record-stop",e.SCRIPT_CODEGEN="script:codegen",e.GATEWAY_RPC="gateway:rpc",e.GATEWAY_EVENT="gateway:event",e.UPDATE_CHECK="update:check",e.UPDATE_DOWNLOAD="update:download",e.UPDATE_INSTALL="update:install",e.UPDATE_VERSION="update:version",e.UPDATE_STATUS_CHANGED="update:status-changed",e))(i||{});const d={versions:process.versions,external:{open:e=>r.ipcRenderer.invoke("external-open",e)},platform:process.platform,windowMinimize:()=>r.ipcRenderer.invoke("window:minimize"),windowMaximize:()=>r.ipcRenderer.invoke("window:maximize"),windowClose:()=>r.ipcRenderer.invoke("window:close"),windowIsMaximized:()=>r.ipcRenderer.invoke("window:isMaximized"),viewIsReady:()=>r.ipcRenderer.send(i.RENDERER_IS_READY),app:{setFrameless:e=>r.ipcRenderer.invoke(i.APP_SET_FRAMELESS,e),loadPage:e=>r.ipcRenderer.invoke(i.APP_LOAD_PAGE,e)},readFile:e=>r.ipcRenderer.invoke(i.READ_FILE,e),invoke:(e,...n)=>r.ipcRenderer.invoke(e,...n),invokeAsync:(e,...n)=>r.ipcRenderer.invoke(e,...n),on:(e,n)=>{const t=(o,...R)=>n(...R);return r.ipcRenderer.on(e,t),()=>r.ipcRenderer.removeListener(e,t)},send:(e,...n)=>r.ipcRenderer.send(e,...n),getCurrentWindowId:()=>r.ipcRenderer.sendSync(i.GET_WINDOW_ID),logger:{debug:(e,...n)=>r.ipcRenderer.send(i.LOG_DEBUG,e,...n),info:(e,...n)=>r.ipcRenderer.send(i.LOG_INFO,e,...n),warn:(e,...n)=>r.ipcRenderer.send(i.LOG_WARN,e,...n),error:(e,...n)=>r.ipcRenderer.send(i.LOG_ERROR,e,...n)},executeScript:e=>r.ipcRenderer.invoke(i.EXECUTE_SCRIPT,e),onTaskProgress:e=>{const n=(t,o)=>e(o);return r.ipcRenderer.on(i.TASK_PROGRESS,n),()=>r.ipcRenderer.removeListener(i.TASK_PROGRESS,n)},onTaskStarted:e=>{const n=(t,o)=>e(o);return r.ipcRenderer.on(i.TASK_STARTED,n),()=>r.ipcRenderer.removeListener(i.TASK_STARTED,n)},onTaskCompleted:e=>{const n=(t,o)=>e(o);return r.ipcRenderer.on(i.TASK_COMPLETED,n),()=>r.ipcRenderer.removeListener(i.TASK_COMPLETED,n)},openChannel:e=>r.ipcRenderer.invoke(i.OPEN_CHANNEL,e),scriptApi:{list:()=>r.ipcRenderer.invoke(i.SCRIPT_LIST),get:e=>r.ipcRenderer.invoke(i.SCRIPT_GET,e),save:e=>r.ipcRenderer.invoke(i.SCRIPT_SAVE,e),delete:e=>r.ipcRenderer.invoke(i.SCRIPT_DELETE,e),toggle:(e,n)=>r.ipcRenderer.invoke(i.SCRIPT_TOGGLE,e,n),run:e=>r.ipcRenderer.invoke(i.SCRIPT_RUN,e),startRecording:e=>r.ipcRenderer.invoke(i.SCRIPT_RECORD_START,e),stopRecording:()=>r.ipcRenderer.invoke(i.SCRIPT_RECORD_STOP),codegen:(e,n)=>r.ipcRenderer.invoke(i.SCRIPT_CODEGEN,e,n)}};r.contextBridge.exposeInMainWorld("api",d);
|
"use strict";
|
||||||
|
const electron = require("electron");
|
||||||
|
var IPC_EVENTS = /* @__PURE__ */ ((IPC_EVENTS2) => {
|
||||||
|
IPC_EVENTS2["EXTERNAL_OPEN"] = "external-open";
|
||||||
|
IPC_EVENTS2["APP_SET_FRAMELESS"] = "app:set-frameless";
|
||||||
|
IPC_EVENTS2["APP_LOAD_PAGE"] = "app:load-page";
|
||||||
|
IPC_EVENTS2["TAB_CREATE"] = "tab:create";
|
||||||
|
IPC_EVENTS2["TAB_LIST"] = "tab:list";
|
||||||
|
IPC_EVENTS2["TAB_NAVIGATE"] = "tab:navigate";
|
||||||
|
IPC_EVENTS2["TAB_RELOAD"] = "tab:reload";
|
||||||
|
IPC_EVENTS2["TAB_BACK"] = "tab:back";
|
||||||
|
IPC_EVENTS2["TAB_FORWARD"] = "tab:forward";
|
||||||
|
IPC_EVENTS2["TAB_SWITCH"] = "tab:switch";
|
||||||
|
IPC_EVENTS2["TAB_CLOSE"] = "tab:close";
|
||||||
|
IPC_EVENTS2["LOG_TO_MAIN"] = "log-to-main";
|
||||||
|
IPC_EVENTS2["READ_FILE"] = "read-file";
|
||||||
|
IPC_EVENTS2["INVOKE"] = "ipc:invoke";
|
||||||
|
IPC_EVENTS2["INVOKE_ASYNC"] = "ipc:invokeAsync";
|
||||||
|
IPC_EVENTS2["APP_MINIMIZE"] = "app:minimize";
|
||||||
|
IPC_EVENTS2["APP_MAXIMIZE"] = "app:maximize";
|
||||||
|
IPC_EVENTS2["APP_QUIT"] = "app:quit";
|
||||||
|
IPC_EVENTS2["FILE_READ"] = "file:read";
|
||||||
|
IPC_EVENTS2["FILE_WRITE"] = "file:write";
|
||||||
|
IPC_EVENTS2["GET_WINDOW_ID"] = "get-window-id";
|
||||||
|
IPC_EVENTS2["CUSTOM_EVENT"] = "custom:event";
|
||||||
|
IPC_EVENTS2["TIME_UPDATE"] = "time:update";
|
||||||
|
IPC_EVENTS2["RENDERER_IS_READY"] = "renderer-ready";
|
||||||
|
IPC_EVENTS2["SHOW_CONTEXT_MENU"] = "show-context-menu";
|
||||||
|
IPC_EVENTS2["START_A_DIALOGUE"] = "start-a-dialogue";
|
||||||
|
IPC_EVENTS2["OPEN_WINDOW"] = "open-window";
|
||||||
|
IPC_EVENTS2["LOG_DEBUG"] = "log-debug";
|
||||||
|
IPC_EVENTS2["LOG_INFO"] = "log-info";
|
||||||
|
IPC_EVENTS2["LOG_WARN"] = "log-warn";
|
||||||
|
IPC_EVENTS2["LOG_ERROR"] = "log-error";
|
||||||
|
IPC_EVENTS2["CONFIG_UPDATED"] = "config-updated";
|
||||||
|
IPC_EVENTS2["SET_CONFIG"] = "set-config";
|
||||||
|
IPC_EVENTS2["GET_CONFIG"] = "get-config";
|
||||||
|
IPC_EVENTS2["UPDATE_CONFIG"] = "update-config";
|
||||||
|
IPC_EVENTS2["SET_THEME_MODE"] = "set-theme-mode";
|
||||||
|
IPC_EVENTS2["GET_THEME_MODE"] = "get-theme-mode";
|
||||||
|
IPC_EVENTS2["IS_DARK_THEME"] = "is-dark-theme";
|
||||||
|
IPC_EVENTS2["THEME_MODE_UPDATED"] = "theme-mode-updated";
|
||||||
|
IPC_EVENTS2["EXECUTE_SCRIPT"] = "execute-script";
|
||||||
|
IPC_EVENTS2["TASK_PROGRESS"] = "task:progress";
|
||||||
|
IPC_EVENTS2["TASK_STARTED"] = "task:started";
|
||||||
|
IPC_EVENTS2["TASK_COMPLETED"] = "task:completed";
|
||||||
|
IPC_EVENTS2["OPEN_CHANNEL"] = "open-channel";
|
||||||
|
IPC_EVENTS2["SCRIPT_LIST"] = "script:list";
|
||||||
|
IPC_EVENTS2["SCRIPT_GET"] = "script:get";
|
||||||
|
IPC_EVENTS2["SCRIPT_SAVE"] = "script:save";
|
||||||
|
IPC_EVENTS2["SCRIPT_DELETE"] = "script:delete";
|
||||||
|
IPC_EVENTS2["SCRIPT_TOGGLE"] = "script:toggle";
|
||||||
|
IPC_EVENTS2["SCRIPT_RUN"] = "script:run";
|
||||||
|
IPC_EVENTS2["SCRIPT_RECORD_START"] = "script:record-start";
|
||||||
|
IPC_EVENTS2["SCRIPT_RECORD_STOP"] = "script:record-stop";
|
||||||
|
IPC_EVENTS2["SCRIPT_CODEGEN"] = "script:codegen";
|
||||||
|
IPC_EVENTS2["GATEWAY_RPC"] = "gateway:rpc";
|
||||||
|
IPC_EVENTS2["GATEWAY_EVENT"] = "gateway:event";
|
||||||
|
IPC_EVENTS2["UPDATE_CHECK"] = "update:check";
|
||||||
|
IPC_EVENTS2["UPDATE_DOWNLOAD"] = "update:download";
|
||||||
|
IPC_EVENTS2["UPDATE_INSTALL"] = "update:install";
|
||||||
|
IPC_EVENTS2["UPDATE_VERSION"] = "update:version";
|
||||||
|
IPC_EVENTS2["UPDATE_STATUS_CHANGED"] = "update:status-changed";
|
||||||
|
return IPC_EVENTS2;
|
||||||
|
})(IPC_EVENTS || {});
|
||||||
|
const api = {
|
||||||
|
versions: process.versions,
|
||||||
|
external: {
|
||||||
|
open: (url) => electron.ipcRenderer.invoke("external-open", url)
|
||||||
|
},
|
||||||
|
platform: process.platform,
|
||||||
|
windowMinimize: () => electron.ipcRenderer.invoke("window:minimize"),
|
||||||
|
windowMaximize: () => electron.ipcRenderer.invoke("window:maximize"),
|
||||||
|
windowClose: () => electron.ipcRenderer.invoke("window:close"),
|
||||||
|
windowIsMaximized: () => electron.ipcRenderer.invoke("window:isMaximized"),
|
||||||
|
viewIsReady: () => electron.ipcRenderer.send(IPC_EVENTS.RENDERER_IS_READY),
|
||||||
|
app: {
|
||||||
|
setFrameless: (route) => electron.ipcRenderer.invoke(IPC_EVENTS.APP_SET_FRAMELESS, route),
|
||||||
|
loadPage: (page) => electron.ipcRenderer.invoke(IPC_EVENTS.APP_LOAD_PAGE, page)
|
||||||
|
},
|
||||||
|
// 通过 IPC 调用主进程
|
||||||
|
readFile: (filePath) => electron.ipcRenderer.invoke(IPC_EVENTS.READ_FILE, filePath),
|
||||||
|
// 异步调用(映射为 electron 的 invoke)
|
||||||
|
invoke: (channel, ...args) => electron.ipcRenderer.invoke(channel, ...args),
|
||||||
|
// 异步调用(为了兼容老代码)
|
||||||
|
invokeAsync: (channel, ...args) => electron.ipcRenderer.invoke(channel, ...args),
|
||||||
|
// 监听主进程消息
|
||||||
|
on: (event, callback) => {
|
||||||
|
const subscription = (_event, ...args) => callback(...args);
|
||||||
|
electron.ipcRenderer.on(event, subscription);
|
||||||
|
return () => electron.ipcRenderer.removeListener(event, subscription);
|
||||||
|
},
|
||||||
|
// 发送消息到主进程
|
||||||
|
send: (channel, ...args) => electron.ipcRenderer.send(channel, ...args),
|
||||||
|
// 获取窗口ID
|
||||||
|
getCurrentWindowId: () => electron.ipcRenderer.sendSync(IPC_EVENTS.GET_WINDOW_ID),
|
||||||
|
// 发送日志
|
||||||
|
logger: {
|
||||||
|
debug: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_DEBUG, message, ...meta),
|
||||||
|
info: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_INFO, message, ...meta),
|
||||||
|
warn: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_WARN, message, ...meta),
|
||||||
|
error: (message, ...meta) => electron.ipcRenderer.send(IPC_EVENTS.LOG_ERROR, message, ...meta)
|
||||||
|
},
|
||||||
|
// 执行脚本
|
||||||
|
executeScript: (params) => electron.ipcRenderer.invoke(IPC_EVENTS.EXECUTE_SCRIPT, params),
|
||||||
|
// 任务事件
|
||||||
|
onTaskProgress: (cb) => {
|
||||||
|
const subscription = (_event, payload) => cb(payload);
|
||||||
|
electron.ipcRenderer.on(IPC_EVENTS.TASK_PROGRESS, subscription);
|
||||||
|
return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_PROGRESS, subscription);
|
||||||
|
},
|
||||||
|
onTaskStarted: (cb) => {
|
||||||
|
const subscription = (_event, payload) => cb(payload);
|
||||||
|
electron.ipcRenderer.on(IPC_EVENTS.TASK_STARTED, subscription);
|
||||||
|
return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_STARTED, subscription);
|
||||||
|
},
|
||||||
|
onTaskCompleted: (cb) => {
|
||||||
|
const subscription = (_event, payload) => cb(payload);
|
||||||
|
electron.ipcRenderer.on(IPC_EVENTS.TASK_COMPLETED, subscription);
|
||||||
|
return () => electron.ipcRenderer.removeListener(IPC_EVENTS.TASK_COMPLETED, subscription);
|
||||||
|
},
|
||||||
|
// 打开渠道
|
||||||
|
openChannel: (channels) => electron.ipcRenderer.invoke(IPC_EVENTS.OPEN_CHANNEL, channels),
|
||||||
|
// 脚本管理
|
||||||
|
scriptApi: {
|
||||||
|
list: () => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_LIST),
|
||||||
|
get: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_GET, id),
|
||||||
|
save: (input) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_SAVE, input),
|
||||||
|
delete: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_DELETE, id),
|
||||||
|
toggle: (id, enabled) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_TOGGLE, id, enabled),
|
||||||
|
run: (id) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RUN, id),
|
||||||
|
startRecording: (url) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_START, url),
|
||||||
|
stopRecording: () => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_RECORD_STOP),
|
||||||
|
codegen: (id, url) => electron.ipcRenderer.invoke(IPC_EVENTS.SCRIPT_CODEGEN, id, url)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
electron.contextBridge.exposeInMainWorld("api", api);
|
||||||
|
|||||||
2
dist/index.html
vendored
2
dist/index.html
vendored
@@ -8,7 +8,7 @@
|
|||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: http://8.138.234.141 https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com; connect-src 'self' http://8.138.234.141 https://api.iconify.design wss://onefeel.brother7.cn"
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: http://8.138.234.141 https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com; connect-src 'self' http://8.138.234.141 https://api.iconify.design wss://onefeel.brother7.cn"
|
||||||
/>
|
/>
|
||||||
<script type="module" crossorigin src="./assets/index-BCaudNKp.js"></script>
|
<script type="module" crossorigin src="./assets/index-YA1vfBvf.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="./assets/index-zlEBzOiw.css">
|
<link rel="stylesheet" crossorigin href="./assets/index-zlEBzOiw.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { HashRouter } from 'react-router-dom';
|
import { HashRouter } from 'react-router-dom';
|
||||||
|
import { useLocale } from './i18n';
|
||||||
import { AppRouter } from './router';
|
import { AppRouter } from './router';
|
||||||
import { initSettingsStore } from './stores';
|
import { initSettingsStore } from './stores';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
useLocale();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void initSettingsStore();
|
void initSettingsStore();
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Book, Clock, Code, Cpu, House, Puzzle, Settings } from 'lucide-react';
|
import { Book, Clock, Code, Cpu, House, Puzzle, Settings } from 'lucide-react';
|
||||||
|
import { useI18n } from '../../i18n';
|
||||||
import { NAV_ITEMS, normalizeWorkspacePath } from '../../router/routes';
|
import { NAV_ITEMS, normalizeWorkspacePath } from '../../router/routes';
|
||||||
|
|
||||||
const MENU_MARKS: Record<string, typeof House> = {
|
const MENU_MARKS: Record<string, typeof House> = {
|
||||||
@@ -15,6 +16,7 @@ const MENU_MARKS: Record<string, typeof House> = {
|
|||||||
export default function Sidebar() {
|
export default function Sidebar() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { t } = useI18n();
|
||||||
const currentId = normalizeWorkspacePath(location.pathname);
|
const currentId = normalizeWorkspacePath(location.pathname);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -53,7 +55,7 @@ export default function Sidebar() {
|
|||||||
className="mt-[4px] mb-[8px] text-[14px] hover:text-[#2B7FFF]"
|
className="mt-[4px] mb-[8px] text-[14px] hover:text-[#2B7FFF]"
|
||||||
style={{ color: active ? '#2B7FFF' : '#525866' }}
|
style={{ color: active ? '#2B7FFF' : '#525866' }}
|
||||||
>
|
>
|
||||||
{item.label}
|
{t(item.labelKey)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
|
import { useI18n } from '../../i18n';
|
||||||
|
|
||||||
type TitleBarProps = {
|
type TitleBarProps = {
|
||||||
variant?: 'default' | 'light';
|
variant?: 'default' | 'light';
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TitleBar({ variant = 'default' }: TitleBarProps) {
|
export default function TitleBar({ variant = 'default' }: TitleBarProps) {
|
||||||
|
const { t } = useI18n();
|
||||||
const platform = (window as any).api?.platform ?? '';
|
const platform = (window as any).api?.platform ?? '';
|
||||||
|
|
||||||
if (platform === 'linux') return null;
|
if (platform === 'linux') return null;
|
||||||
@@ -34,7 +37,7 @@ export default function TitleBar({ variant = 'default' }: TitleBarProps) {
|
|||||||
className={['flex h-full w-11 items-center justify-center hover:bg-[#999] hover:text-white transition-colors', iconColorClass].join(
|
className={['flex h-full w-11 items-center justify-center hover:bg-[#999] hover:text-white transition-colors', iconColorClass].join(
|
||||||
' ',
|
' ',
|
||||||
)}
|
)}
|
||||||
title="Minimize"
|
title={t('window.minimize')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
(window as any).api?.windowMinimize?.();
|
(window as any).api?.windowMinimize?.();
|
||||||
}}
|
}}
|
||||||
@@ -46,7 +49,7 @@ export default function TitleBar({ variant = 'default' }: TitleBarProps) {
|
|||||||
className={['flex h-full w-11 items-center justify-center hover:bg-[#999] hover:text-white transition-colors', iconColorClass].join(
|
className={['flex h-full w-11 items-center justify-center hover:bg-[#999] hover:text-white transition-colors', iconColorClass].join(
|
||||||
' ',
|
' ',
|
||||||
)}
|
)}
|
||||||
title="Maximize"
|
title={t('window.maximize')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
(window as any).api?.windowMaximize?.();
|
(window as any).api?.windowMaximize?.();
|
||||||
}}
|
}}
|
||||||
@@ -58,7 +61,7 @@ export default function TitleBar({ variant = 'default' }: TitleBarProps) {
|
|||||||
className={['flex h-full w-11 items-center justify-center hover:bg-[#ff0000] hover:text-white transition-colors', iconColorClass].join(
|
className={['flex h-full w-11 items-center justify-center hover:bg-[#ff0000] hover:text-white transition-colors', iconColorClass].join(
|
||||||
' ',
|
' ',
|
||||||
)}
|
)}
|
||||||
title="Close"
|
title={t('window.close')}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
(window as any).api?.windowClose?.();
|
(window as any).api?.windowClose?.();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -10,6 +10,6 @@ export const SUPPORTED_LANGUAGES = [
|
|||||||
{ code: 'ja', label: '日本語' },
|
{ code: 'ja', label: '日本語' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type Namespace = 'common' | 'conversation' | 'setting' | 'window';
|
export type Namespace = 'common' | 'conversation' | 'setting' | 'window' | 'sidebar' | 'login';
|
||||||
|
|
||||||
export const NAMESPACES = ['common', 'conversation', 'setting', 'window'] as const satisfies readonly Namespace[];
|
export const NAMESPACES = ['common', 'conversation', 'setting', 'window', 'sidebar', 'login'] as const satisfies readonly Namespace[];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useSyncExternalStore } from 'react';
|
||||||
import { DEFAULT_LANGUAGE } from '../lib/constants';
|
import { DEFAULT_LANGUAGE } from '../lib/constants';
|
||||||
import type { LanguageCode } from '../types/runtime';
|
import type { LanguageCode } from '../types/runtime';
|
||||||
import type { Namespace } from './constants';
|
import type { Namespace } from './constants';
|
||||||
@@ -103,5 +104,25 @@ export function getMessages(locale?: LanguageCode): MessageTree {
|
|||||||
return i18n.getMessages(locale);
|
return i18n.getMessages(locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function subscribeToLocale(listener: () => void): () => void {
|
||||||
|
return i18n.subscribe(() => {
|
||||||
|
listener();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLocale(): LanguageCode {
|
||||||
|
return useSyncExternalStore(subscribeToLocale, getLocale, getLocale);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useI18n() {
|
||||||
|
const locale = useLocale();
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
t,
|
||||||
|
hasMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export { SUPPORTED_LANGUAGE_CODES, SUPPORTED_LANGUAGES, NAMESPACES } from './constants';
|
export { SUPPORTED_LANGUAGE_CODES, SUPPORTED_LANGUAGES, NAMESPACES } from './constants';
|
||||||
export type { LanguageCode, Namespace };
|
export type { LanguageCode, Namespace };
|
||||||
|
|||||||
@@ -40,6 +40,72 @@ export const messages: I18nMessages = {
|
|||||||
newConversation: 'New conversation',
|
newConversation: 'New conversation',
|
||||||
emptyState: 'No messages yet',
|
emptyState: 'No messages yet',
|
||||||
},
|
},
|
||||||
|
sidebar: {
|
||||||
|
home: 'Home',
|
||||||
|
knowledge: 'Knowledge',
|
||||||
|
models: 'Models',
|
||||||
|
skills: 'Skills',
|
||||||
|
cron: 'Cron',
|
||||||
|
scripts: 'Scripts',
|
||||||
|
settings: 'Settings',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
menu: {
|
||||||
|
systemSettings: 'System Settings',
|
||||||
|
account: 'Account',
|
||||||
|
general: 'General',
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
title: 'Account Settings',
|
||||||
|
description: 'Manage your account details and sign-in security.',
|
||||||
|
accountLabel: 'Account',
|
||||||
|
passwordLabel: 'Login Password',
|
||||||
|
passwordHelp: 'Used for investor login operations, last login time: {time}',
|
||||||
|
configured: 'Configured',
|
||||||
|
changePassword: 'Change Password',
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
title: 'Basic Settings',
|
||||||
|
description: 'Customize the look and feel of the application.',
|
||||||
|
themeSection: 'Theme Settings',
|
||||||
|
languageSection: 'Language',
|
||||||
|
updatesTitle: 'Updates',
|
||||||
|
currentVersion: 'Current Version',
|
||||||
|
checkForUpdates: 'Check for Updates',
|
||||||
|
checkingForUpdates: 'Checking for updates...',
|
||||||
|
latestVersion: 'You have the latest version',
|
||||||
|
newVersionAvailable: 'New version available: v{version}',
|
||||||
|
downloadingVersion: 'Downloading new version... {percent}%',
|
||||||
|
downloadComplete: 'Download complete, ready to install',
|
||||||
|
updateError: 'Update error: {error}',
|
||||||
|
updateHint: 'Check for updates to get the latest features.',
|
||||||
|
downloadUpdate: 'Download Update',
|
||||||
|
restartAndInstall: 'Restart and Install',
|
||||||
|
autoCheckTitle: 'Auto check for updates',
|
||||||
|
autoCheckDescription: 'Check for updates on startup',
|
||||||
|
autoDownloadTitle: 'Auto download updates',
|
||||||
|
autoDownloadDescription: 'Automatically download and install updates',
|
||||||
|
autoUpdateHint: 'When auto-update is enabled, updates will be downloaded and installed automatically.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
title: 'Welcome Back',
|
||||||
|
subtitle: 'A digital teammate on duty 24/7, always ready to help.',
|
||||||
|
username: 'Username',
|
||||||
|
password: 'Password',
|
||||||
|
verificationCode: 'Verification Code',
|
||||||
|
usernamePlaceholder: 'Enter username',
|
||||||
|
passwordPlaceholder: 'Enter password',
|
||||||
|
verificationCodePlaceholder: 'Enter verification code',
|
||||||
|
usernameRequired: 'Please enter a username',
|
||||||
|
passwordRequired: 'Please enter a password',
|
||||||
|
codeRequired: 'Please enter the verification code',
|
||||||
|
submitFailed: 'Login failed, please try again later',
|
||||||
|
submit: 'Sign In',
|
||||||
|
submitting: 'Signing In...',
|
||||||
|
loadingCaptcha: 'Loading...',
|
||||||
|
captchaAlt: 'Verification code',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
app: {
|
app: {
|
||||||
@@ -74,6 +140,72 @@ export const messages: I18nMessages = {
|
|||||||
newConversation: '新建对话',
|
newConversation: '新建对话',
|
||||||
emptyState: '暂无消息',
|
emptyState: '暂无消息',
|
||||||
},
|
},
|
||||||
|
sidebar: {
|
||||||
|
home: '首页',
|
||||||
|
knowledge: '知识库',
|
||||||
|
models: '模型',
|
||||||
|
skills: '技能',
|
||||||
|
cron: '定时任务',
|
||||||
|
scripts: '脚本',
|
||||||
|
settings: '设置',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
menu: {
|
||||||
|
systemSettings: '系统设置',
|
||||||
|
account: '账号',
|
||||||
|
general: '通用',
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
title: '账号设置',
|
||||||
|
description: '管理账号信息与登录安全。',
|
||||||
|
accountLabel: '账号',
|
||||||
|
passwordLabel: '登录密码',
|
||||||
|
passwordHelp: '用于投资人登录操作,最近登录时间:{time}',
|
||||||
|
configured: '已配置',
|
||||||
|
changePassword: '修改密码',
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
title: '基础设置',
|
||||||
|
description: '自定义应用的外观与使用体验。',
|
||||||
|
themeSection: '主题设置',
|
||||||
|
languageSection: '语言',
|
||||||
|
updatesTitle: '版本更新',
|
||||||
|
currentVersion: '当前版本',
|
||||||
|
checkForUpdates: '检查更新',
|
||||||
|
checkingForUpdates: '正在检查更新...',
|
||||||
|
latestVersion: '当前已是最新版本',
|
||||||
|
newVersionAvailable: '发现新版本:v{version}',
|
||||||
|
downloadingVersion: '正在下载新版本... {percent}%',
|
||||||
|
downloadComplete: '下载完成,可立即安装',
|
||||||
|
updateError: '更新失败:{error}',
|
||||||
|
updateHint: '检查更新以获取最新功能。',
|
||||||
|
downloadUpdate: '下载更新',
|
||||||
|
restartAndInstall: '重启并安装',
|
||||||
|
autoCheckTitle: '自动检查更新',
|
||||||
|
autoCheckDescription: '应用启动时自动检查更新',
|
||||||
|
autoDownloadTitle: '自动下载更新',
|
||||||
|
autoDownloadDescription: '自动下载并安装更新',
|
||||||
|
autoUpdateHint: '开启自动更新后,更新包会自动下载并安装。',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
title: '欢迎回来',
|
||||||
|
subtitle: '24 小时在岗,随时待命的数字员工。',
|
||||||
|
username: '账号',
|
||||||
|
password: '密码',
|
||||||
|
verificationCode: '验证码',
|
||||||
|
usernamePlaceholder: '请输入账号',
|
||||||
|
passwordPlaceholder: '请输入密码',
|
||||||
|
verificationCodePlaceholder: '请输入验证码',
|
||||||
|
usernameRequired: '请输入用户名',
|
||||||
|
passwordRequired: '请输入密码',
|
||||||
|
codeRequired: '请输入验证码',
|
||||||
|
submitFailed: '登录失败,请稍后重试',
|
||||||
|
submit: '登录',
|
||||||
|
submitting: '登录中...',
|
||||||
|
loadingCaptcha: '加载中...',
|
||||||
|
captchaAlt: '验证码',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ja: {
|
ja: {
|
||||||
app: {
|
app: {
|
||||||
@@ -82,7 +214,7 @@ export const messages: I18nMessages = {
|
|||||||
window: {
|
window: {
|
||||||
minimize: '最小化',
|
minimize: '最小化',
|
||||||
maximize: '最大化',
|
maximize: '最大化',
|
||||||
restore: '復元',
|
restore: '元に戻す',
|
||||||
close: '閉じる',
|
close: '閉じる',
|
||||||
},
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
@@ -92,7 +224,7 @@ export const messages: I18nMessages = {
|
|||||||
theme: {
|
theme: {
|
||||||
light: 'ライト',
|
light: 'ライト',
|
||||||
dark: 'ダーク',
|
dark: 'ダーク',
|
||||||
system: 'システムに従う',
|
system: 'システム',
|
||||||
},
|
},
|
||||||
language: {
|
language: {
|
||||||
zh: '中国語',
|
zh: '中国語',
|
||||||
@@ -106,7 +238,73 @@ export const messages: I18nMessages = {
|
|||||||
},
|
},
|
||||||
conversation: {
|
conversation: {
|
||||||
newConversation: '新しい会話',
|
newConversation: '新しい会話',
|
||||||
emptyState: 'メッセージはまだありません',
|
emptyState: 'まだメッセージがありません',
|
||||||
|
},
|
||||||
|
sidebar: {
|
||||||
|
home: 'ホーム',
|
||||||
|
knowledge: 'ナレッジ',
|
||||||
|
models: 'モデル',
|
||||||
|
skills: 'スキル',
|
||||||
|
cron: '定時タスク',
|
||||||
|
scripts: 'スクリプト',
|
||||||
|
settings: '設定',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
menu: {
|
||||||
|
systemSettings: 'システム設定',
|
||||||
|
account: 'アカウント',
|
||||||
|
general: '一般',
|
||||||
|
},
|
||||||
|
account: {
|
||||||
|
title: 'アカウント設定',
|
||||||
|
description: 'アカウント情報とサインインの安全性を管理します。',
|
||||||
|
accountLabel: 'アカウント',
|
||||||
|
passwordLabel: 'ログインパスワード',
|
||||||
|
passwordHelp: '投資家ログインに使用します。最終ログイン時刻:{time}',
|
||||||
|
configured: '設定済み',
|
||||||
|
changePassword: 'パスワードを変更',
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
title: '基本設定',
|
||||||
|
description: 'アプリの見た目と操作感をカスタマイズします。',
|
||||||
|
themeSection: 'テーマ設定',
|
||||||
|
languageSection: '言語',
|
||||||
|
updatesTitle: 'アップデート',
|
||||||
|
currentVersion: '現在のバージョン',
|
||||||
|
checkForUpdates: '更新を確認',
|
||||||
|
checkingForUpdates: '更新を確認しています...',
|
||||||
|
latestVersion: '最新バージョンを使用しています',
|
||||||
|
newVersionAvailable: '新しいバージョンがあります:v{version}',
|
||||||
|
downloadingVersion: '新しいバージョンをダウンロード中... {percent}%',
|
||||||
|
downloadComplete: 'ダウンロードが完了しました。インストールできます',
|
||||||
|
updateError: 'アップデートエラー:{error}',
|
||||||
|
updateHint: '最新機能を利用するには更新を確認してください。',
|
||||||
|
downloadUpdate: '更新をダウンロード',
|
||||||
|
restartAndInstall: '再起動してインストール',
|
||||||
|
autoCheckTitle: '更新を自動確認',
|
||||||
|
autoCheckDescription: '起動時に更新を確認します',
|
||||||
|
autoDownloadTitle: '更新を自動ダウンロード',
|
||||||
|
autoDownloadDescription: '更新を自動でダウンロードしてインストールします',
|
||||||
|
autoUpdateHint: '自動更新を有効にすると、更新が自動でダウンロードされてインストールされます。',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
title: 'おかえりなさい',
|
||||||
|
subtitle: '24時間待機し、いつでも支援できるデジタルチームメイトです。',
|
||||||
|
username: 'アカウント',
|
||||||
|
password: 'パスワード',
|
||||||
|
verificationCode: '認証コード',
|
||||||
|
usernamePlaceholder: 'アカウントを入力してください',
|
||||||
|
passwordPlaceholder: 'パスワードを入力してください',
|
||||||
|
verificationCodePlaceholder: '認証コードを入力してください',
|
||||||
|
usernameRequired: 'ユーザー名を入力してください',
|
||||||
|
passwordRequired: 'パスワードを入力してください',
|
||||||
|
codeRequired: '認証コードを入力してください',
|
||||||
|
submitFailed: 'ログインに失敗しました。しばらくしてからもう一度お試しください',
|
||||||
|
submit: 'ログイン',
|
||||||
|
submitting: 'ログイン中...',
|
||||||
|
loadingCaptcha: '読み込み中...',
|
||||||
|
captchaAlt: '認証コード',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useLocale } from '../../i18n';
|
||||||
import { getLocale, i18n } from '../../i18n';
|
|
||||||
import type { LanguageCode } from '../../types/runtime';
|
import type { LanguageCode } from '../../types/runtime';
|
||||||
|
|
||||||
type Primitive = string | number;
|
type Primitive = string | number;
|
||||||
@@ -216,9 +215,7 @@ function createKnowledgeTranslate(locale: LanguageCode): KnowledgeTranslate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useKnowledgeCopy(): KnowledgeTranslate {
|
export function useKnowledgeCopy(): KnowledgeTranslate {
|
||||||
const [locale, setLocale] = useState<LanguageCode>(getLocale());
|
const locale = useLocale();
|
||||||
|
|
||||||
useEffect(() => i18n.subscribe(setLocale), []);
|
|
||||||
|
|
||||||
return createKnowledgeTranslate(locale);
|
return createKnowledgeTranslate(locale);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { type ChangeEvent, type FormEvent, useEffect, useMemo, useState } from 'react';
|
import { type ChangeEvent, type FormEvent, useEffect, useMemo, useState } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
import { Lock, User } from 'lucide-react';
|
import { Lock, User } from 'lucide-react';
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import blueLogo from '../../assets/images/login/blue_logo.png';
|
import blueLogo from '../../assets/images/login/blue_logo.png';
|
||||||
import loginBackground from '../../assets/images/login/login_bg.png';
|
import loginBackground from '../../assets/images/login/login_bg.png';
|
||||||
import loginIllustration from '../../assets/images/login/logo.png';
|
import loginIllustration from '../../assets/images/login/logo.png';
|
||||||
import userIcon from '../../assets/images/login/user_icon.png';
|
import userIcon from '../../assets/images/login/user_icon.png';
|
||||||
|
|
||||||
|
import { useI18n } from '../../i18n';
|
||||||
import TitleBar from '../../components/layout/TitleBar';
|
import TitleBar from '../../components/layout/TitleBar';
|
||||||
import { resolvePostLoginPath } from '../../router/auth';
|
import { resolvePostLoginPath } from '../../router/auth';
|
||||||
import {
|
import {
|
||||||
@@ -35,6 +36,7 @@ const credentialFieldInputClass =
|
|||||||
'h-full min-w-0 flex-1 border-0 bg-transparent pr-4 text-[14px] text-gray-800 outline-none placeholder:text-[#99A0AE] disabled:cursor-not-allowed dark:text-gray-100 dark:placeholder:text-gray-500';
|
'h-full min-w-0 flex-1 border-0 bg-transparent pr-4 text-[14px] text-gray-800 outline-none placeholder:text-[#99A0AE] disabled:cursor-not-allowed dark:text-gray-100 dark:placeholder:text-gray-500';
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
|
const { t } = useI18n();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const platform = (window as any).api?.platform ?? '';
|
const platform = (window as any).api?.platform ?? '';
|
||||||
@@ -72,9 +74,9 @@ export default function LoginPage() {
|
|||||||
function validate(values: LoginFormValues): FormErrors {
|
function validate(values: LoginFormValues): FormErrors {
|
||||||
const nextErrors: FormErrors = {};
|
const nextErrors: FormErrors = {};
|
||||||
|
|
||||||
if (!values.username.trim()) nextErrors.username = '请输入用户名';
|
if (!values.username.trim()) nextErrors.username = t('login.usernameRequired');
|
||||||
if (!values.password.trim()) nextErrors.password = '请输入密码';
|
if (!values.password.trim()) nextErrors.password = t('login.passwordRequired');
|
||||||
if (!values.code.trim()) nextErrors.code = '请输入验证码';
|
if (!values.code.trim()) nextErrors.code = t('login.codeRequired');
|
||||||
|
|
||||||
return nextErrors;
|
return nextErrors;
|
||||||
}
|
}
|
||||||
@@ -104,7 +106,7 @@ export default function LoginPage() {
|
|||||||
navigate(resolvePostLoginPath(location.state as { from?: string } | null), { replace: true });
|
navigate(resolvePostLoginPath(location.state as { from?: string } | null), { replace: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setErrors({
|
setErrors({
|
||||||
submit: error instanceof Error ? error.message : '登录失败,请稍后重试',
|
submit: error instanceof Error ? error.message : t('login.submitFailed'),
|
||||||
});
|
});
|
||||||
refreshCaptcha(false);
|
refreshCaptcha(false);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -121,7 +123,7 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-screen flex flex-col"
|
className="flex h-screen flex-col"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${loginBackground})`,
|
backgroundImage: `url(${loginBackground})`,
|
||||||
backgroundSize: '100% 100%',
|
backgroundSize: '100% 100%',
|
||||||
@@ -132,27 +134,27 @@ export default function LoginPage() {
|
|||||||
<TitleBar variant="light" />
|
<TitleBar variant="light" />
|
||||||
|
|
||||||
<main
|
<main
|
||||||
className={['box-border pl-2 pr-2 pb-2 flex-auto flex', platform !== 'linux' ? 'pt-2' : 'pt-11'].join(' ')}
|
className={['box-border flex flex-auto pl-2 pr-2 pb-2', platform !== 'linux' ? 'pt-2' : 'pt-11'].join(' ')}
|
||||||
>
|
>
|
||||||
<div className="w-[836px] box-border rounded-2xl border border-black/5 bg-white p-8 shadow-[0_18px_50px_rgba(15,23,42,0.12)] dark:border-[#2a2a2d] dark:bg-[#1b1b1d]">
|
<div className="box-border w-[836px] rounded-2xl border border-black/5 bg-white p-8 shadow-[0_18px_50px_rgba(15,23,42,0.12)] dark:border-[#2a2a2d] dark:bg-[#1b1b1d]">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img className="w-12 h-12" src={blueLogo} alt="zn-ai" />
|
<img className="h-12 w-12" src={blueLogo} alt="zn-ai" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-6 box-border flex flex-col items-center justify-center pt-10">
|
<div className="mb-6 box-border flex flex-col items-center justify-center pt-10">
|
||||||
<img className="mb-3 h-20 w-20" src={userIcon} alt="" />
|
<img className="mb-3 h-20 w-20" src={userIcon} alt="" />
|
||||||
<div className="mb-1 text-[24px] leading-[32px] font-medium text-gray-800 dark:text-gray-100">
|
<div className="mb-1 text-[24px] leading-[32px] font-medium text-gray-800 dark:text-gray-100">
|
||||||
欢迎回到登录
|
{t('login.title')}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[16px] leading-[24px] text-gray-500 dark:text-gray-400">
|
<div className="text-[16px] leading-[24px] text-gray-500 dark:text-gray-400">
|
||||||
24小时在岗,从不打烊的数字员工
|
{t('login.subtitle')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form className="mx-auto flex w-[392px] flex-col gap-4" onSubmit={handleSubmit}>
|
<form className="mx-auto flex w-[392px] flex-col gap-4" onSubmit={handleSubmit}>
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-[14px] text-gray-600 dark:text-gray-300" htmlFor="login-username">
|
<label className="mb-2 block text-[14px] text-gray-600 dark:text-gray-300" htmlFor="login-username">
|
||||||
账号
|
{t('login.username')}
|
||||||
</label>
|
</label>
|
||||||
<div className={credentialFieldShellClass}>
|
<div className={credentialFieldShellClass}>
|
||||||
<User aria-hidden="true" className={credentialFieldIconClass} />
|
<User aria-hidden="true" className={credentialFieldIconClass} />
|
||||||
@@ -161,37 +163,37 @@ export default function LoginPage() {
|
|||||||
className={credentialFieldInputClass}
|
className={credentialFieldInputClass}
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
placeholder="请输入账号"
|
placeholder={t('login.usernamePlaceholder')}
|
||||||
value={form.username}
|
value={form.username}
|
||||||
onChange={(event) => handleInputChange('username', event)}
|
onChange={(event) => handleInputChange('username', event)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.username ? <div className="pt-2 text-[12px] text-[#dc2626]">{errors.username}</div> : null}
|
{errors.username ? <div className="pt-2 text-[12px] text-[#dc2626]">{errors.username}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-[14px] text-gray-600 dark:text-gray-300" htmlFor="login-password">
|
<label className="mb-2 block text-[14px] text-gray-600 dark:text-gray-300" htmlFor="login-password">
|
||||||
密码
|
{t('login.password')}
|
||||||
</label>
|
</label>
|
||||||
<div className={credentialFieldShellClass}>
|
<div className={credentialFieldShellClass}>
|
||||||
<Lock aria-hidden="true" className={credentialFieldIconClass} />
|
<Lock aria-hidden="true" className={credentialFieldIconClass} />
|
||||||
<input
|
<input
|
||||||
id="login-password"
|
id="login-password"
|
||||||
className={credentialFieldInputClass}
|
className={credentialFieldInputClass}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
placeholder="请输入密码"
|
placeholder={t('login.passwordPlaceholder')}
|
||||||
type="password"
|
type="password"
|
||||||
value={form.password}
|
value={form.password}
|
||||||
onChange={(event) => handleInputChange('password', event)}
|
onChange={(event) => handleInputChange('password', event)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errors.password ? <div className="pt-2 text-[12px] text-[#dc2626]">{errors.password}</div> : null}
|
{errors.password ? <div className="pt-2 text-[12px] text-[#dc2626]">{errors.password}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-2 block text-[14px] text-gray-600 dark:text-gray-300" htmlFor="login-code">
|
<label className="mb-2 block text-[14px] text-gray-600 dark:text-gray-300" htmlFor="login-code">
|
||||||
验证码
|
{t('login.verificationCode')}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<input
|
<input
|
||||||
@@ -199,7 +201,7 @@ export default function LoginPage() {
|
|||||||
className="h-10 min-w-0 flex-1 rounded-[10px] border border-black/10 bg-gray-50 px-4 text-[14px] text-gray-800 outline-none transition focus:border-[#2B7FFF] dark:border-[#2a2a2d] dark:bg-[#222225] dark:text-gray-100"
|
className="h-10 min-w-0 flex-1 rounded-[10px] border border-black/10 bg-gray-50 px-4 text-[14px] text-gray-800 outline-none transition focus:border-[#2B7FFF] dark:border-[#2a2a2d] dark:bg-[#222225] dark:text-gray-100"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
placeholder="请输入验证码"
|
placeholder={t('login.verificationCodePlaceholder')}
|
||||||
value={form.code}
|
value={form.code}
|
||||||
onChange={(event) => handleInputChange('code', event)}
|
onChange={(event) => handleInputChange('code', event)}
|
||||||
/>
|
/>
|
||||||
@@ -210,9 +212,13 @@ export default function LoginPage() {
|
|||||||
onClick={refreshCaptcha}
|
onClick={refreshCaptcha}
|
||||||
>
|
>
|
||||||
{captchaUrl ? (
|
{captchaUrl ? (
|
||||||
<img className="h-full w-full object-cover" src={captchaUrl} alt="验证码" />
|
<img
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
src={captchaUrl}
|
||||||
|
alt={t('login.captchaAlt')}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-[12px] text-[#99A0AE]">加载中</span>
|
<span className="text-[12px] text-[#99A0AE]">{t('login.loadingCaptcha')}</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,7 +236,7 @@ export default function LoginPage() {
|
|||||||
className="mt-4 w-full rounded-lg bg-blue-600 py-2 text-white transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-blue-300"
|
className="mt-4 w-full rounded-lg bg-blue-600 py-2 text-white transition hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-blue-300"
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
{submitting ? '登录中...' : '登录'}
|
{submitting ? t('login.submitting') : t('login.submit')}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useI18n } from '../../../i18n';
|
||||||
import SectionHeader from './SectionHeader';
|
import SectionHeader from './SectionHeader';
|
||||||
import { CheckCircleIcon } from './SettingIcons';
|
import { CheckCircleIcon } from './SettingIcons';
|
||||||
|
|
||||||
@@ -5,40 +6,42 @@ const ACCOUNT_ID = '1234567890';
|
|||||||
const LAST_LOGIN_TIME = '2022-11-09 16:24:30';
|
const LAST_LOGIN_TIME = '2022-11-09 16:24:30';
|
||||||
|
|
||||||
export default function AccountSettingsPanel() {
|
export default function AccountSettingsPanel() {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex-1 h-full p-[20px] select-none">
|
<section className="flex-1 h-full p-[20px] select-none">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="Account Settings"
|
title={t('settings.account.title')}
|
||||||
description="Please associate PMS and channel room names, smart mapping is available"
|
description={t('settings.account.description')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="w-full flex items-center mt-[20px] py-[20px] box-border border-b border-dashed border-[#E5E8EE] dark:border-gray-700">
|
<div className="mt-[20px] box-border flex w-full items-center border-b border-dashed border-[#E5E8EE] py-[20px] dark:border-gray-700">
|
||||||
<div className="text-[16px] font-medium text-[#171717] dark:text-gray-100 mr-[24px] whitespace-nowrap">
|
<div className="mr-[24px] whitespace-nowrap text-[16px] font-medium text-[#171717] dark:text-gray-100">
|
||||||
Account
|
{t('settings.account.accountLabel')}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[14px] font-medium text-[#171717] dark:text-gray-100">{ACCOUNT_ID}</div>
|
<div className="text-[14px] font-medium text-[#171717] dark:text-gray-100">{ACCOUNT_ID}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full flex items-center py-[20px] box-border border-b border-dashed border-[#E5E8EE] dark:border-gray-700 gap-3">
|
<div className="box-border flex w-full items-center gap-3 border-b border-dashed border-[#E5E8EE] py-[20px] dark:border-gray-700">
|
||||||
<div className="text-[16px] font-medium text-[#171717] dark:text-gray-100 mr-[24px] whitespace-nowrap">
|
<div className="mr-[24px] whitespace-nowrap text-[16px] font-medium text-[#171717] dark:text-gray-100">
|
||||||
Login Password
|
{t('settings.account.passwordLabel')}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[14px] text-[#99A0AE] dark:text-gray-500 min-w-0">
|
<div className="min-w-0 text-[14px] text-[#99A0AE] dark:text-gray-500">
|
||||||
Used for investor login operations, last login time: {LAST_LOGIN_TIME}
|
{t('settings.account.passwordHelp', { time: LAST_LOGIN_TIME })}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border border-[#E5E8EE] dark:border-gray-700 rounded-[6px] px-[6px] py-[4px] flex items-center shrink-0">
|
<div className="flex shrink-0 items-center rounded-[6px] border border-[#E5E8EE] px-[6px] py-[4px] dark:border-gray-700">
|
||||||
<CheckCircleIcon className="w-[16px] h-[16px] text-[#1FC16B]" />
|
<CheckCircleIcon className="h-[16px] w-[16px] text-[#1FC16B]" />
|
||||||
<span className="text-[12px] text-[#525866] dark:text-gray-400 ml-[2px] whitespace-nowrap">
|
<span className="ml-[2px] whitespace-nowrap text-[12px] text-[#525866] dark:text-gray-400">
|
||||||
Configured
|
{t('settings.account.configured')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="ml-auto text-[14px] text-[#2B7FFF] hover:text-[#1F6AE5] transition-colors whitespace-nowrap"
|
className="ml-auto whitespace-nowrap text-[14px] text-[#2B7FFF] transition-colors hover:text-[#1F6AE5]"
|
||||||
>
|
>
|
||||||
Change Password
|
{t('settings.account.changePassword')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { t } from '../../../i18n';
|
import { useI18n } from '../../../i18n';
|
||||||
import { SUPPORTED_LANGUAGE_CODES } from '../../../i18n/constants';
|
import { SUPPORTED_LANGUAGE_CODES } from '../../../i18n/constants';
|
||||||
import type { LanguageCode, ThemeMode } from '../../../types/runtime';
|
import type { LanguageCode, ThemeMode } from '../../../types/runtime';
|
||||||
import type { SettingUpdateState } from '../useSettingUpdateState';
|
import type { SettingUpdateState } from '../useSettingUpdateState';
|
||||||
@@ -24,22 +24,28 @@ const THEME_OPTIONS: Array<{
|
|||||||
{ value: 'system', icon: ComputerIcon, labelPath: 'theme.system' },
|
{ value: 'system', icon: ComputerIcon, labelPath: 'theme.system' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function getUpdateStatusText(updateState: SettingUpdateState) {
|
function getUpdateStatusText(t: ReturnType<typeof useI18n>['t'], updateState: SettingUpdateState) {
|
||||||
switch (updateState.status) {
|
switch (updateState.status) {
|
||||||
case 'checking':
|
case 'checking':
|
||||||
return 'Checking for updates...';
|
return t('settings.general.checkingForUpdates');
|
||||||
case 'not-available':
|
case 'not-available':
|
||||||
return 'You have the latest version';
|
return t('settings.general.latestVersion');
|
||||||
case 'available':
|
case 'available':
|
||||||
return `New version available: v${updateState.updateInfo?.version ?? ''}`;
|
return t('settings.general.newVersionAvailable', {
|
||||||
|
version: updateState.updateInfo?.version ?? '',
|
||||||
|
});
|
||||||
case 'downloading':
|
case 'downloading':
|
||||||
return `Downloading new version... ${Math.round(updateState.progress?.percent ?? 0)}%`;
|
return t('settings.general.downloadingVersion', {
|
||||||
|
percent: Math.round(updateState.progress?.percent ?? 0),
|
||||||
|
});
|
||||||
case 'downloaded':
|
case 'downloaded':
|
||||||
return 'Download complete, ready to install';
|
return t('settings.general.downloadComplete');
|
||||||
case 'error':
|
case 'error':
|
||||||
return `Update error: ${updateState.error ?? 'Unknown error'}`;
|
return t('settings.general.updateError', {
|
||||||
|
error: updateState.error ?? t('common.unknownError'),
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
return 'Check for updates to get latest features';
|
return t('settings.general.updateHint');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,16 +56,18 @@ export default function GeneralSettingsPanel({
|
|||||||
onLanguageChange,
|
onLanguageChange,
|
||||||
updateState,
|
updateState,
|
||||||
}: GeneralSettingsPanelProps) {
|
}: GeneralSettingsPanelProps) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex-1 h-full p-[20px] select-none">
|
<section className="flex-1 h-full p-[20px] select-none">
|
||||||
<SectionHeader
|
<SectionHeader
|
||||||
title="Basic Settings"
|
title={t('settings.general.title')}
|
||||||
description="Customize the look and feel of the application"
|
description={t('settings.general.description')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="w-full flex items-center mt-[20px] py-[20px] box-border border-b border-dashed border-[#E5E8EE] dark:border-gray-700">
|
<div className="mt-[20px] box-border flex w-full items-center border-b border-dashed border-[#E5E8EE] py-[20px] dark:border-gray-700">
|
||||||
<div className="text-[16px] font-medium text-[#171717] dark:text-gray-100 mr-[24px] whitespace-nowrap">
|
<div className="mr-[24px] whitespace-nowrap text-[16px] font-medium text-[#171717] dark:text-gray-100">
|
||||||
Theme Settings
|
{t('settings.general.themeSection')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
@@ -74,13 +82,13 @@ export default function GeneralSettingsPanel({
|
|||||||
void onThemeChange(value);
|
void onThemeChange(value);
|
||||||
}}
|
}}
|
||||||
className={[
|
className={[
|
||||||
'px-5 py-1.5 rounded-full border text-[14px] font-medium transition-all duration-200 flex items-center gap-2',
|
'flex items-center gap-2 rounded-full border px-5 py-1.5 text-[14px] font-medium transition-all duration-200',
|
||||||
active
|
active
|
||||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-500 dark:border-blue-400 text-blue-700 dark:text-blue-300'
|
? 'border-blue-500 bg-blue-50 text-blue-700 dark:border-blue-400 dark:bg-blue-900/20 dark:text-blue-300'
|
||||||
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700',
|
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<Icon className="w-4 h-4" />
|
<Icon className="h-4 w-4" />
|
||||||
{t(labelPath)}
|
{t(labelPath)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
@@ -89,11 +97,11 @@ export default function GeneralSettingsPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full flex items-center mt-[20px] py-[20px] box-border border-b border-dashed border-[#E5E8EE] dark:border-gray-700">
|
<div className="mt-[20px] box-border flex w-full items-center border-b border-dashed border-[#E5E8EE] py-[20px] dark:border-gray-700">
|
||||||
<div className="text-[16px] font-medium text-[#171717] dark:text-gray-100 mr-[24px] whitespace-nowrap">
|
<div className="mr-[24px] whitespace-nowrap text-[16px] font-medium text-[#171717] dark:text-gray-100">
|
||||||
Language
|
{t('settings.general.languageSection')}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex flex-wrap gap-2">
|
||||||
{SUPPORTED_LANGUAGE_CODES.map((code) => {
|
{SUPPORTED_LANGUAGE_CODES.map((code) => {
|
||||||
const active = language === code;
|
const active = language === code;
|
||||||
|
|
||||||
@@ -105,10 +113,10 @@ export default function GeneralSettingsPanel({
|
|||||||
void onLanguageChange(code);
|
void onLanguageChange(code);
|
||||||
}}
|
}}
|
||||||
className={[
|
className={[
|
||||||
'px-5 py-1.5 rounded-full border text-[14px] font-medium transition-all duration-200 flex items-center gap-2',
|
'flex items-center gap-2 rounded-full border px-5 py-1.5 text-[14px] font-medium transition-all duration-200',
|
||||||
active
|
active
|
||||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-500 dark:border-blue-400 text-blue-700 dark:text-blue-300'
|
? 'border-blue-500 bg-blue-50 text-blue-700 dark:border-blue-400 dark:bg-blue-900/20 dark:text-blue-300'
|
||||||
: 'bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700',
|
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
{t(`language.${code}`)}
|
{t(`language.${code}`)}
|
||||||
@@ -119,11 +127,15 @@ export default function GeneralSettingsPanel({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-[40px]">
|
<div className="mt-[40px]">
|
||||||
<div className="text-[24px] font-medium text-[#171717] dark:text-gray-100 mb-[24px]">Updates</div>
|
<div className="mb-[24px] text-[24px] font-medium text-[#171717] dark:text-gray-100">
|
||||||
|
{t('settings.general.updatesTitle')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between mb-[16px] gap-4">
|
<div className="mb-[16px] flex items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[14px] text-[#525866] dark:text-gray-400 mb-[4px]">Current Version</div>
|
<div className="mb-[4px] text-[14px] text-[#525866] dark:text-gray-400">
|
||||||
|
{t('settings.general.currentVersion')}
|
||||||
|
</div>
|
||||||
<div className="text-[28px] font-bold text-[#171717] dark:text-gray-100">
|
<div className="text-[28px] font-bold text-[#171717] dark:text-gray-100">
|
||||||
v{updateState.currentVersion}
|
v{updateState.currentVersion}
|
||||||
</div>
|
</div>
|
||||||
@@ -131,31 +143,29 @@ export default function GeneralSettingsPanel({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-[8px] rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
className="rounded-full p-[8px] transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void updateState.checkUpdate();
|
void updateState.checkUpdate();
|
||||||
}}
|
}}
|
||||||
aria-label="Check for updates"
|
aria-label={t('settings.general.checkForUpdates')}
|
||||||
>
|
>
|
||||||
<RefreshIcon
|
<RefreshIcon
|
||||||
className={[
|
className={[
|
||||||
'w-5 h-5 text-[#525866] dark:text-gray-400',
|
'h-5 w-5 text-[#525866] dark:text-gray-400',
|
||||||
updateState.status === 'checking' ? 'animate-spin' : '',
|
updateState.status === 'checking' ? 'animate-spin' : '',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-[#F5F7FA] dark:bg-gray-800 rounded-[8px] p-[16px] flex items-center justify-between gap-4 mb-[16px]">
|
<div className="mb-[16px] flex items-center justify-between gap-4 rounded-[8px] bg-[#F5F7FA] p-[16px] dark:bg-gray-800">
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
'text-[14px]',
|
'text-[14px]',
|
||||||
updateState.status === 'error'
|
updateState.status === 'error' ? 'text-red-500' : 'text-[#525866] dark:text-gray-300',
|
||||||
? 'text-red-500'
|
|
||||||
: 'text-[#525866] dark:text-gray-300',
|
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
{getUpdateStatusText(updateState)}
|
{getUpdateStatusText(t, updateState)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
@@ -165,9 +175,9 @@ export default function GeneralSettingsPanel({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
void updateState.downloadUpdate();
|
void updateState.downloadUpdate();
|
||||||
}}
|
}}
|
||||||
className="px-[14px] py-[8px] rounded-[8px] bg-[#2B7FFF] text-white text-[14px] font-medium hover:bg-[#1F6AE5] transition-colors"
|
className="rounded-[8px] bg-[#2B7FFF] px-[14px] py-[8px] text-[14px] font-medium text-white transition-colors hover:bg-[#1F6AE5]"
|
||||||
>
|
>
|
||||||
Download Update
|
{t('settings.general.downloadUpdate')}
|
||||||
</button>
|
</button>
|
||||||
) : updateState.status === 'downloaded' ? (
|
) : updateState.status === 'downloaded' ? (
|
||||||
<button
|
<button
|
||||||
@@ -175,9 +185,9 @@ export default function GeneralSettingsPanel({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
void updateState.installUpdate();
|
void updateState.installUpdate();
|
||||||
}}
|
}}
|
||||||
className="px-[14px] py-[8px] rounded-[8px] bg-[#1FC16B] text-white text-[14px] font-medium hover:bg-[#17A95C] transition-colors"
|
className="rounded-[8px] bg-[#1FC16B] px-[14px] py-[8px] text-[14px] font-medium text-white transition-colors hover:bg-[#17A95C]"
|
||||||
>
|
>
|
||||||
Restart and Install
|
{t('settings.general.restartAndInstall')}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
@@ -185,31 +195,31 @@ export default function GeneralSettingsPanel({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
void updateState.checkUpdate();
|
void updateState.checkUpdate();
|
||||||
}}
|
}}
|
||||||
className="px-[14px] py-[8px] rounded-[8px] border border-[#E5E8EE] dark:border-gray-600 bg-white dark:bg-gray-700 text-[#171717] dark:text-gray-100 text-[14px] font-medium hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors inline-flex items-center gap-2"
|
className="inline-flex items-center gap-2 rounded-[8px] border border-[#E5E8EE] bg-white px-[14px] py-[8px] text-[14px] font-medium text-[#171717] transition-colors hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600"
|
||||||
>
|
>
|
||||||
<RefreshIcon
|
<RefreshIcon
|
||||||
className={[
|
className={[
|
||||||
'w-4 h-4',
|
'h-4 w-4',
|
||||||
updateState.status === 'checking' ? 'animate-spin' : '',
|
updateState.status === 'checking' ? 'animate-spin' : '',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
/>
|
/>
|
||||||
Check for Updates
|
{t('settings.general.checkForUpdates')}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-[12px] text-[#99A0AE] dark:text-gray-500 mb-[32px]">
|
<div className="mb-[32px] text-[12px] text-[#99A0AE] dark:text-gray-500">
|
||||||
When auto-update is enabled, updates will be downloaded and installed automatically.
|
{t('settings.general.autoUpdateHint')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between py-[16px] border-b border-[#E5E8EE] dark:border-gray-800 gap-4">
|
<div className="flex items-center justify-between gap-4 border-b border-[#E5E8EE] py-[16px] dark:border-gray-800">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[16px] text-[#171717] dark:text-gray-100 mb-[4px]">
|
<div className="mb-[4px] text-[16px] text-[#171717] dark:text-gray-100">
|
||||||
Auto check for updates
|
{t('settings.general.autoCheckTitle')}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[14px] text-[#99A0AE] dark:text-gray-500">
|
<div className="text-[14px] text-[#99A0AE] dark:text-gray-500">
|
||||||
Check for updates on startup
|
{t('settings.general.autoCheckDescription')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -219,13 +229,13 @@ export default function GeneralSettingsPanel({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between py-[16px] gap-4">
|
<div className="flex items-center justify-between gap-4 py-[16px]">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[16px] text-[#171717] dark:text-gray-100 mb-[4px]">
|
<div className="mb-[4px] text-[16px] text-[#171717] dark:text-gray-100">
|
||||||
Auto download updates
|
{t('settings.general.autoDownloadTitle')}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[14px] text-[#99A0AE] dark:text-gray-500">
|
<div className="text-[14px] text-[#99A0AE] dark:text-gray-500">
|
||||||
Automatically download and install updates
|
{t('settings.general.autoDownloadDescription')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useI18n } from '../../../i18n';
|
||||||
import { SettingsIcon, UserIcon } from './SettingIcons';
|
import { SettingsIcon, UserIcon } from './SettingIcons';
|
||||||
|
|
||||||
export type SettingView = 'account' | 'general';
|
export type SettingView = 'account' | 'general';
|
||||||
@@ -9,27 +10,31 @@ type SettingMenuProps = {
|
|||||||
|
|
||||||
const MENU_ITEMS: Array<{
|
const MENU_ITEMS: Array<{
|
||||||
id: SettingView;
|
id: SettingView;
|
||||||
label: string;
|
labelKey: 'settings.menu.account' | 'settings.menu.general';
|
||||||
Icon: typeof UserIcon;
|
Icon: typeof UserIcon;
|
||||||
}> = [
|
}> = [
|
||||||
{
|
{
|
||||||
id: 'account',
|
id: 'account',
|
||||||
label: 'Account',
|
labelKey: 'settings.menu.account',
|
||||||
Icon: UserIcon,
|
Icon: UserIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'general',
|
id: 'general',
|
||||||
label: 'General',
|
labelKey: 'settings.menu.general',
|
||||||
Icon: SettingsIcon,
|
Icon: SettingsIcon,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function SettingMenu({ currentView, onChange }: SettingMenuProps) {
|
export default function SettingMenu({ currentView, onChange }: SettingMenuProps) {
|
||||||
return (
|
const { t } = useI18n();
|
||||||
<aside className="w-[160px] h-full box-border border-r border-[#E5E8EE] dark:border-gray-700 py-[12px] px-[8px] flex flex-col gap-[4px] select-none shrink-0">
|
|
||||||
<div className="text-[12px] text-[#99A0AE] dark:text-gray-500 p-[4px]">System Settings</div>
|
|
||||||
|
|
||||||
{MENU_ITEMS.map(({ id, label, Icon }) => {
|
return (
|
||||||
|
<aside className="box-border flex h-full w-[160px] shrink-0 flex-col gap-[4px] border-r border-[#E5E8EE] px-[8px] py-[12px] select-none dark:border-gray-700">
|
||||||
|
<div className="p-[4px] text-[12px] text-[#99A0AE] dark:text-gray-500">
|
||||||
|
{t('settings.menu.systemSettings')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{MENU_ITEMS.map(({ id, labelKey, Icon }) => {
|
||||||
const active = currentView === id;
|
const active = currentView === id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -38,19 +43,19 @@ export default function SettingMenu({ currentView, onChange }: SettingMenuProps)
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onChange(id)}
|
onClick={() => onChange(id)}
|
||||||
className={[
|
className={[
|
||||||
'box-border flex items-center py-[10px] px-[12px] rounded-[6px] cursor-pointer transition-colors',
|
'box-border flex cursor-pointer items-center rounded-[6px] px-[12px] py-[10px] transition-colors',
|
||||||
active ? 'bg-[#EFF6FF] dark:bg-[#222225]' : 'hover:bg-[#EFF6FF] dark:hover:bg-[#222225]',
|
active ? 'bg-[#EFF6FF] dark:bg-[#222225]' : 'hover:bg-[#EFF6FF] dark:hover:bg-[#222225]',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className="w-[20px] h-[20px]"
|
className="h-[20px] w-[20px]"
|
||||||
style={{ color: active ? '#2B7FFF' : '#525866' }}
|
style={{ color: active ? '#2B7FFF' : '#525866' }}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="box-border px-[8px] text-[14px] font-medium dark:text-gray-300"
|
className="box-border px-[8px] text-[14px] font-medium dark:text-gray-300"
|
||||||
style={{ color: active ? '#2B7FFF' : '#525866' }}
|
style={{ color: active ? '#2B7FFF' : '#525866' }}
|
||||||
>
|
>
|
||||||
{label}
|
{t(labelKey)}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useLocale } from '../../i18n';
|
||||||
import { getLocale, i18n } from '../../i18n';
|
|
||||||
import type { LanguageCode } from '../../types/runtime';
|
import type { LanguageCode } from '../../types/runtime';
|
||||||
import { SKILLS_MESSAGES } from './messages';
|
import { SKILLS_MESSAGES } from './messages';
|
||||||
|
|
||||||
@@ -46,9 +45,7 @@ function createSkillsTranslate(locale: LanguageCode): SkillsTranslate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSkillsCopy(): SkillsTranslate {
|
export function useSkillsCopy(): SkillsTranslate {
|
||||||
const [locale, setLocale] = useState<LanguageCode>(getLocale());
|
const locale = useLocale();
|
||||||
|
|
||||||
useEffect(() => i18n.subscribe(setLocale), []);
|
|
||||||
|
|
||||||
return createSkillsTranslate(locale);
|
return createSkillsTranslate(locale);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,20 +12,26 @@ export type WorkspacePath = Exclude<AppPath, '/login'>;
|
|||||||
|
|
||||||
export type NavItem = {
|
export type NavItem = {
|
||||||
path: WorkspacePath;
|
path: WorkspacePath;
|
||||||
label: string;
|
labelKey:
|
||||||
description: string;
|
| 'sidebar.home'
|
||||||
|
| 'sidebar.knowledge'
|
||||||
|
| 'sidebar.models'
|
||||||
|
| 'sidebar.skills'
|
||||||
|
| 'sidebar.cron'
|
||||||
|
| 'sidebar.scripts'
|
||||||
|
| 'sidebar.settings';
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_PATH: WorkspacePath = '/home';
|
export const DEFAULT_PATH: WorkspacePath = '/home';
|
||||||
|
|
||||||
export const NAV_ITEMS: NavItem[] = [
|
export const NAV_ITEMS: NavItem[] = [
|
||||||
{ path: '/home', label: '首页', description: '对话与首页入口' },
|
{ path: '/home', labelKey: 'sidebar.home' },
|
||||||
{ path: '/knowledge', label: '知识库', description: '知识库与内容管理' },
|
{ path: '/knowledge', labelKey: 'sidebar.knowledge' },
|
||||||
{ path: '/agents', label: '模型', description: '智能体与提供方' },
|
{ path: '/agents', labelKey: 'sidebar.models' },
|
||||||
{ path: '/skills', label: '技能', description: '技能与能力集' },
|
{ path: '/skills', labelKey: 'sidebar.skills' },
|
||||||
{ path: '/cron', label: '定时任务', description: '计划与调度' },
|
{ path: '/cron', labelKey: 'sidebar.cron' },
|
||||||
{ path: '/scripts', label: '脚本', description: '脚本与自动化' },
|
{ path: '/scripts', labelKey: 'sidebar.scripts' },
|
||||||
{ path: '/setting', label: '设置', description: '应用配置' },
|
{ path: '/setting', labelKey: 'sidebar.settings' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function normalizeWorkspacePath(pathname: string): WorkspacePath {
|
export function normalizeWorkspacePath(pathname: string): WorkspacePath {
|
||||||
|
|||||||
Reference in New Issue
Block a user