diff --git a/packages/hydrooj/src/handler/manage.ts b/packages/hydrooj/src/handler/manage.ts index 00a52dbfa..a5c44b490 100644 --- a/packages/hydrooj/src/handler/manage.ts +++ b/packages/hydrooj/src/handler/manage.ts @@ -183,12 +183,12 @@ class SystemConfigHandler extends SystemHandler { @requireSudo async get() { this.response.template = 'manage_config.html'; - let value = this.ctx.config.configSource; + let value = this.ctx.get('config').configSource; try { - value = yaml.dump(Schema.intersect(this.ctx.config.settings)(yaml.load(value))); + value = yaml.dump(Schema.intersect(this.ctx.get('config').settings)(yaml.load(value))); } catch (e) { } this.response.body = { - schema: Schema.intersect(this.ctx.config.settings).toJSON(), + schema: Schema.intersect(this.ctx.get('config').settings).toJSON(), value, }; } @@ -202,7 +202,7 @@ class SystemConfigHandler extends SystemHandler { } catch (e) { throw new ValidationError('value'); } - await this.ctx.config.saveConfig(config); + await this.ctx.get('config').saveConfig(config); } } @@ -321,6 +321,7 @@ class SystemUserPrivHandler extends SystemHandler { } } +export const inject = ['config']; export async function apply(ctx) { ctx.Route('manage', '/manage', SystemMainHandler); ctx.Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler); diff --git a/packages/hydrooj/src/service/storage.ts b/packages/hydrooj/src/service/storage.ts index 0581ca428..9ddd2a785 100644 --- a/packages/hydrooj/src/service/storage.ts +++ b/packages/hydrooj/src/service/storage.ts @@ -65,23 +65,23 @@ const defaultPath = process.env.CI ? '/tmp/file' const FileSetting = Schema.intersect([ Schema.object({ type: Schema.union([ - Schema.const('file').description('local file provider'), - Schema.const('s3').description('s3 provider'), - ] as const).description('provider type'), + Schema.const('file').i18n({ en: 'Local Directory', zh: '本地目录' }), + Schema.const('s3').description('S3'), + ] as const).i18n({ en: 'Storage Provider Type', zh: '存储提供商类型' }), endPointForUser: Schema.string().default('/fs/'), endPointForJudge: Schema.string().default('/fs/'), - }).description('setting_file'), + }).i18n({ en: 'File Storage Setting', zh: '文件存储设置' }), Schema.union([ Schema.object({ type: Schema.const('file').required(), - path: Schema.string().default(defaultPath).description('Storage path'), - secret: Schema.string().description('Download file sign secret').default(nanoid()), + path: Schema.string().default(defaultPath).i18n({ en: 'Storage path', zh: '存储路径' }), + secret: Schema.string().default(nanoid()).i18n({ en: 'Download file sign secret', zh: '下载文件签名密钥' }), }), Schema.object({ type: Schema.const('s3').required(), endPoint: Schema.string(), - accessKey: Schema.string().description('access key'), - secretKey: Schema.string().description('secret key').role('secret'), + accessKey: Schema.string(), + secretKey: Schema.string().role('secret'), bucket: Schema.string().default('hydro'), region: Schema.string().default('us-east-1'), pathStyle: Schema.boolean().default(true), diff --git a/packages/ui-default/components/monaco/index.ts b/packages/ui-default/components/monaco/index.ts index b6a8204ab..5f92b3881 100644 --- a/packages/ui-default/components/monaco/index.ts +++ b/packages/ui-default/components/monaco/index.ts @@ -8,6 +8,8 @@ import list from 'monaco-themes/themes/themelist.json'; import { nanoid } from 'nanoid'; import { i18n, request } from 'vj/utils'; +export { renderMarkdown } from 'monaco-editor/esm/vs/base/browser/markdownRenderer'; + export default monaco; export const customOptions: monaco.editor.IStandaloneDiffEditorConstructionOptions = JSON.parse(localStorage.getItem('editor.config') || '{}'); export function saveCustomOptions() { diff --git a/packages/ui-default/components/monaco/loader.ts b/packages/ui-default/components/monaco/loader.ts index 3a84b4fbe..a7f17c9de 100644 --- a/packages/ui-default/components/monaco/loader.ts +++ b/packages/ui-default/components/monaco/loader.ts @@ -85,7 +85,12 @@ export async function load(features = ['markdown']) { } await res.loadThemePromise; resolve(); - return { monaco: res.default, registerAction: res.registerAction, customOptions: res.customOptions }; + return { + monaco: res.default, + registerAction: res.registerAction, + customOptions: res.customOptions, + renderMarkdown: res.renderMarkdown, + }; } export default load; diff --git a/packages/ui-default/index.ts b/packages/ui-default/index.ts index 94f5dbb74..e1dea3e76 100644 --- a/packages/ui-default/index.ts +++ b/packages/ui-default/index.ts @@ -74,7 +74,7 @@ class MarkdownHandler extends Handler { class SystemConfigSchemaHandler extends Handler { async get() { - const schema = convert(Schema.intersect(this.ctx.config.settings) as any, true); + const schema = convert(Schema.intersect(this.ctx.get('config')!.settings) as any, true); this.response.body = schema; } } diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index c5e3562cc..aea14fdcd 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -43,6 +43,7 @@ "@types/redux-logger": "^3.0.13", "@types/uuid": "^10.0.0", "@types/webpack-env": "^1.18.8", + "@undefined-moe/schemastery-vue": "^7.3.7", "@vscode/codicons": "^0.0.36", "ajv": "^8.17.1", "allotment": "^1.20.3", @@ -59,6 +60,7 @@ "diff": "^7.0.0", "diff-dom": "^5.1.4", "echarts": "^5.6.0", + "element-plus": "^2.9.6", "emojis-keywords": "2.0.0", "emojis-list": "2.1.0", "esbuild-loader": "^4.3.0", @@ -115,6 +117,8 @@ "timeago-react": "^3.0.7", "timeago.js": "^4.0.2", "ts-loader": "^9.5.2", + "veaury": "^2.6.2", + "vue-i18n": "^11.1.2", "web-streams-polyfill": "^4.1.0", "webpack": "^5.98.0", "webpack-bundle-analyzer": "^4.10.2", diff --git a/packages/ui-default/pages/setting.page.tsx b/packages/ui-default/pages/setting.page.tsx index 73a9c285f..7d479f3b7 100644 --- a/packages/ui-default/pages/setting.page.tsx +++ b/packages/ui-default/pages/setting.page.tsx @@ -1,29 +1,159 @@ +import 'element-plus/dist/index.css'; +import '@undefined-moe/schemastery-vue/lib/schemastery-vue.css'; + import yaml from 'js-yaml'; +import React from 'react'; +import ReactDOM from 'react-dom/client'; import Schema from 'schemastery'; +import { applyPureReactInVue, applyVueInReact, createReactMissVue } from 'veaury'; import Notification from 'vj/components/notification'; import { NamedPage } from 'vj/misc/Page'; -import { request } from 'vj/utils'; +import { i18n, request } from 'vj/utils'; + +const locales = { + zh: 'zh-CN', + en: 'en-US', +}; const page = new NamedPage('manage_config', async () => { - const schema = new Schema(UiContext.schema); - setInterval(() => { - try { - const v = yaml.load($('#config').val().toString()); - schema(v); - $('#info').text(''); - } catch (e) { - console.debug(e); - $('#info').text(e.message); - } - }, 1000); - $('#submit').on('click', () => { - const value = $('#config').val(); - request.post('', { value }).then(() => { - Notification.success('保存成功'); - }).catch((e) => { - Notification.error('保存失败:', e.message); - }); + const [{ default: form }, { createI18n }, ElementPlus, { load }] = await Promise.all([ + import('@undefined-moe/schemastery-vue/lib/schemastery-vue.esm') as Promise, + import('vue-i18n'), + import('element-plus'), + import('vj/components/monaco/loader'), + ]); + const { monaco, registerAction, renderMarkdown } = await load(['yaml']); + const KForm = applyVueInReact(form.Form) as any; + + function MarkdownContainer(props) { + const rendered = React.useMemo(() => { + const res = renderMarkdown({ value: props.source }); + const value = res.element.innerHTML; + res.dispose(); + return value; + }, [props.source]); + return
; + } + const [, ReactMissVue] = createReactMissVue({ + useVueInjection() { + return {}; + }, + beforeVueAppMount(app: any) { + app.use(form); + app.use(createI18n({ + legacy: false, + locale: locales[i18n('__id')] || 'en-US', + })); + app.use(ElementPlus); + app.component('k-markdown', applyPureReactInVue(MarkdownContainer)); + }, }); + const initialConfigString = UiContext.config; + const schema = new Schema(UiContext.schema); + const initial = yaml.load(UiContext.config); + + function MonacoContainer({ config, setValue, setError }) { + const editorRef = React.useRef(null); + const [loading, setLoading] = React.useState(false); + const [editor, setEditor] = React.useState(null); + + React.useEffect(() => { + const listener = () => { + if (!editor) return; + editor.layout(); + }; + document.addEventListener('resize', listener); + return () => document.removeEventListener('resize', listener); + }, [editor]); + React.useEffect(() => { + if (!editorRef.current || loading) return; + setLoading(true); + const model = monaco.editor.createModel(config, 'yaml', monaco.Uri.parse('hydro://system/setting.yaml')); + const e = monaco.editor.create(editorRef.current, { + theme: 'vs-light', + lineNumbers: 'off', + glyphMargin: true, + lightbulb: { enabled: monaco.editor.ShowLightbulbIconMode.On }, + model, + minimap: { enabled: false }, + hideCursorInOverviewRuler: true, + overviewRulerLanes: 0, + overviewRulerBorder: false, + fontFamily: UserContext.codeFontFamily, + fontLigatures: '', + unicodeHighlight: { + ambiguousCharacters: true, + }, + }); + registerAction(e, model, editorRef.current); + e.onDidChangeModelContent(() => { + const val = e.getValue({ lineEnding: '\n', preserveBOM: false }); + try { + const loaded = yaml.load(val); + schema(loaded); + setValue(val); + setError(''); + } catch (err) { + setError(err.message); + } + }); + setEditor(e); + }, [editorRef.current]); + React.useEffect(() => { + if (!editor) return; + const current = editor.getValue({ lineEnding: '\n', preserveBOM: false }); + if (current !== config) editor.setValue(config); + }, [editor, config]); + return ( +
+ ); + } + + const initialClone = JSON.parse(JSON.stringify(initial)); + function App() { + const [value, setValue] = React.useState(initialClone); + const [info, setInfo] = React.useState(''); + const [stringConfig, setStringConfig] = React.useState(initialConfigString); + + const updateFromForm = React.useCallback((v) => { + const newDump = yaml.dump(v); + if (newDump === stringConfig) return; + console.log('update.form', newDump); + setStringConfig(newDump); + setValue(v); + }, [stringConfig]); + const updateFromMonaco = React.useCallback((v) => { + if (v === stringConfig) return; + console.log('update.monaco', v); + setStringConfig(v); + setValue(yaml.load(v)); + }, [stringConfig]); + + return +
+ +
+ +
{info}
+
+
+
+
+ +
+
+
; + } + + ReactDOM.createRoot(document.getElementById('app')!).render(); }); export default page; diff --git a/packages/ui-default/templates/manage_config.html b/packages/ui-default/templates/manage_config.html index d25d41eac..16955a37c 100644 --- a/packages/ui-default/templates/manage_config.html +++ b/packages/ui-default/templates/manage_config.html @@ -1,24 +1,10 @@ {% extends "manage_base.html" %} {% block manage_content %} +
+
{{ set(UiContext, 'schema', schema) }} +{{ set(UiContext, 'config', value) }}
-
-
-
- -

-      
-
-
-
- -
-
-
+
{% endblock %}