From c4bb484c6e15e3ec2543a3601002d1f6a73d329c Mon Sep 17 00:00:00 2001 From: Jacky Date: Sun, 1 Jun 2025 23:08:17 +0800 Subject: [PATCH] refactor: refresh editor ui --- .cursor/mcp.json | 9 + app/components.d.ts | 2 + app/src/components/BaseEditor/BaseEditor.vue | 87 ++++ app/src/components/BaseEditor/index.ts | 4 + app/src/components/CertInfo/CertInfo.vue | 4 +- app/src/components/ChatGPT/ChatGPT.vue | 20 +- .../components/ChatGPT/ChatMessageInput.vue | 3 +- .../components/ChatGPT/ChatMessageList.vue | 56 +-- app/src/components/ChatGPT/chatgpt.ts | 14 +- .../NgxConfigEditor/NgxConfigEditor.vue | 29 +- .../components/NgxConfigEditor/NgxServer.vue | 43 +- .../NgxConfigEditor/NgxUpstream.vue | 27 +- app/src/version.json | 2 +- app/src/views/config/ConfigEditor.vue | 395 ++---------------- app/src/views/config/InspectConfig.vue | 5 + .../config/components/ConfigLeftPanel.vue | 306 ++++++++++++++ .../config/components/ConfigRightPanel.vue | 24 ++ .../components/ConfigRightPanel/Basic.vue | 66 +++ .../components/ConfigRightPanel/Chat.vue | 15 + .../ConfigRightPanel/ConfigRightPanel.vue | 73 ++++ .../components/ConfigRightPanel/Deploy.vue | 46 ++ .../components/ConfigRightPanel/index.ts | 3 + app/src/views/config/components/index.ts | 4 + app/src/views/config/configColumns.tsx | 5 +- app/src/views/site/site_edit/SiteEdit.vue | 36 +- .../components/EnableTLS/EnableTLS.vue | 4 +- .../site_edit/components/RightPanel/Chat.vue | 2 +- .../components/RightPanel/RightPanel.vue | 23 +- .../components/SiteEditor/SiteEditor.vue | 11 +- .../site_edit/components/SiteEditor/store.ts | 5 + app/src/views/stream/StreamEdit.vue | 51 +-- .../stream/components/RightPanel/Basic.vue | 44 +- .../stream/components/RightPanel/Chat.vue | 2 +- .../components/RightPanel/RightPanel.vue | 12 +- .../views/stream/components/StreamEditor.vue | 203 ++++----- .../stream/components/StreamStatusSelect.vue | 4 +- app/src/views/stream/store.ts | 3 + 37 files changed, 945 insertions(+), 697 deletions(-) create mode 100644 .cursor/mcp.json create mode 100644 app/src/components/BaseEditor/BaseEditor.vue create mode 100644 app/src/components/BaseEditor/index.ts create mode 100644 app/src/views/config/components/ConfigLeftPanel.vue create mode 100644 app/src/views/config/components/ConfigRightPanel.vue create mode 100644 app/src/views/config/components/ConfigRightPanel/Basic.vue create mode 100644 app/src/views/config/components/ConfigRightPanel/Chat.vue create mode 100644 app/src/views/config/components/ConfigRightPanel/ConfigRightPanel.vue create mode 100644 app/src/views/config/components/ConfigRightPanel/Deploy.vue create mode 100644 app/src/views/config/components/ConfigRightPanel/index.ts create mode 100644 app/src/views/config/components/index.ts diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 00000000..09cca01f --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,9 @@ +{ + "mcpServers": { + "eslint": { + "command": "npx", + "args": ["@eslint/mcp@latest"], + "env": {} + } + } +} \ No newline at end of file diff --git a/app/components.d.ts b/app/components.d.ts index 35db57ef..52c4be5d 100644 --- a/app/components.d.ts +++ b/app/components.d.ts @@ -68,6 +68,7 @@ declare module 'vue' { AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger'] AutoCertFormAutoCertForm: typeof import('./src/components/AutoCertForm/AutoCertForm.vue')['default'] AutoCertFormDNSChallenge: typeof import('./src/components/AutoCertForm/DNSChallenge.vue')['default'] + BaseEditorBaseEditor: typeof import('./src/components/BaseEditor/BaseEditor.vue')['default'] BreadcrumbBreadcrumb: typeof import('./src/components/Breadcrumb/Breadcrumb.vue')['default'] CertInfoCertInfo: typeof import('./src/components/CertInfo/CertInfo.vue')['default'] ChartAreaChart: typeof import('./src/components/Chart/AreaChart.vue')['default'] @@ -78,6 +79,7 @@ declare module 'vue' { ChatGPTChatMessageInput: typeof import('./src/components/ChatGPT/ChatMessageInput.vue')['default'] ChatGPTChatMessageList: typeof import('./src/components/ChatGPT/ChatMessageList.vue')['default'] CodeEditorCodeEditor: typeof import('./src/components/CodeEditor/CodeEditor.vue')['default'] + CommonEditorCommonEditor: typeof import('./src/components/CommonEditor/CommonEditor.vue')['default'] ConfigHistoryConfigHistory: typeof import('./src/components/ConfigHistory/ConfigHistory.vue')['default'] ConfigHistoryDiffViewer: typeof import('./src/components/ConfigHistory/DiffViewer.vue')['default'] EnvGroupTabsEnvGroupTabs: typeof import('./src/components/EnvGroupTabs/EnvGroupTabs.vue')['default'] diff --git a/app/src/components/BaseEditor/BaseEditor.vue b/app/src/components/BaseEditor/BaseEditor.vue new file mode 100644 index 00000000..a7292d9d --- /dev/null +++ b/app/src/components/BaseEditor/BaseEditor.vue @@ -0,0 +1,87 @@ + + + + + + + diff --git a/app/src/components/BaseEditor/index.ts b/app/src/components/BaseEditor/index.ts new file mode 100644 index 00000000..8b3edbdf --- /dev/null +++ b/app/src/components/BaseEditor/index.ts @@ -0,0 +1,4 @@ +import BaseEditor from './BaseEditor.vue' + +export { BaseEditor } +export default BaseEditor diff --git a/app/src/components/CertInfo/CertInfo.vue b/app/src/components/CertInfo/CertInfo.vue index b5cd3cd0..ad63d299 100644 --- a/app/src/components/CertInfo/CertInfo.vue +++ b/app/src/components/CertInfo/CertInfo.vue @@ -44,5 +44,7 @@ const isValid = computed(() => dayjs().isAfter(props.cert?.not_before) && dayjs( diff --git a/app/src/components/ChatGPT/ChatGPT.vue b/app/src/components/ChatGPT/ChatGPT.vue index 8fe7707b..984a0f23 100644 --- a/app/src/components/ChatGPT/ChatGPT.vue +++ b/app/src/components/ChatGPT/ChatGPT.vue @@ -1,5 +1,6 @@ diff --git a/app/src/components/ChatGPT/ChatMessageInput.vue b/app/src/components/ChatGPT/ChatMessageInput.vue index 6406925f..8880c9df 100644 --- a/app/src/components/ChatGPT/ChatMessageInput.vue +++ b/app/src/components/ChatGPT/ChatMessageInput.vue @@ -59,7 +59,7 @@ const messagesLength = computed(() => messages.value?.length ?? 0) backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); padding: 16px; - border-top: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 0 0 8px 8px; .control-btn { display: flex; @@ -76,7 +76,6 @@ const messagesLength = computed(() => messages.value?.length ?? 0) .dark { .input-msg { background: rgba(30, 30, 30, 0.8); - border-top: 1px solid rgba(255, 255, 255, 0.1); } } diff --git a/app/src/components/ChatGPT/ChatMessageList.vue b/app/src/components/ChatGPT/ChatMessageList.vue index 5fc01265..dba81ead 100644 --- a/app/src/components/ChatGPT/ChatMessageList.vue +++ b/app/src/components/ChatGPT/ChatMessageList.vue @@ -6,57 +6,6 @@ import ChatMessage from './ChatMessage.vue' const chatGPTStore = useChatGPTStore() const { messages, editingIdx, editValue, loading } = storeToRefs(chatGPTStore) -const messageListRef = useTemplateRef('messageList') -let scrollTimeoutId: number | null = null - -function scrollToBottom() { - // Debounce scroll operations for better performance - if (scrollTimeoutId) { - clearTimeout(scrollTimeoutId) - } - - scrollTimeoutId = window.setTimeout(() => { - requestAnimationFrame(() => { - if (messageListRef.value) { - let element = messageListRef.value.parentElement - while (element) { - const style = window.getComputedStyle(element) - if (style.overflowY === 'auto' || style.overflowY === 'scroll') { - element.scrollTo({ - top: element.scrollHeight, - behavior: 'smooth', - }) - return - } - element = element.parentElement - } - } - }) - }, 50) // 50ms debounce -} - -// Watch for messages changes and auto scroll - with debouncing -watch(() => messages.value, () => { - scrollToBottom() -}, { deep: true, flush: 'post' }) - -// Auto scroll when messages are loaded -onMounted(() => { - scrollToBottom() -}) - -// Clean up on unmount -onUnmounted(() => { - if (scrollTimeoutId) { - clearTimeout(scrollTimeoutId) - } -}) - -// Expose scroll function for parent component -defineExpose({ - scrollToBottom, -}) - function handleEdit(index: number) { chatGPTStore.startEdit(index) } @@ -77,7 +26,7 @@ async function handleRegenerate(index: number) { diff --git a/app/src/components/NgxConfigEditor/NgxServer.vue b/app/src/components/NgxConfigEditor/NgxServer.vue index d0ecbeee..134d3b22 100644 --- a/app/src/components/NgxConfigEditor/NgxServer.vue +++ b/app/src/components/NgxConfigEditor/NgxServer.vue @@ -22,6 +22,10 @@ const serversLength = computed(() => { return ngxConfig.value.servers?.length ?? 0 }) +const hasServers = computed(() => { + return serversLength.value > 0 +}) + watch(serversLength, () => { if (curServerIdx.value >= serversLength.value) curServerIdx.value = serversLength.value - 1 @@ -38,6 +42,9 @@ watch(curServerIdx, () => { }) function addServer() { + if (!ngxConfig.value.servers) + ngxConfig.value.servers = [] + ngxConfig.value.servers.push({ comments: '', locations: [], @@ -63,7 +70,32 @@ function removeServer(index: number) { -
- -
+
+ + + +
- {{ $gettext('Create') }} + + {{ $gettext('Add Upstream') }}
@@ -195,5 +205,12 @@ function getAvailabilityResult(directive: NgxDirective) { diff --git a/app/src/version.json b/app/src/version.json index 93838231..816e0d00 100644 --- a/app/src/version.json +++ b/app/src/version.json @@ -1 +1 @@ -{"version":"2.1.0","build_id":1,"total_build":427} \ No newline at end of file +{"version":"2.1.0","build_id":2,"total_build":428} \ No newline at end of file diff --git a/app/src/views/config/ConfigEditor.vue b/app/src/views/config/ConfigEditor.vue index e8944bf6..277bef26 100644 --- a/app/src/views/config/ConfigEditor.vue +++ b/app/src/views/config/ConfigEditor.vue @@ -1,380 +1,29 @@ diff --git a/app/src/views/config/InspectConfig.vue b/app/src/views/config/InspectConfig.vue index c8f7b971..2239674b 100644 --- a/app/src/views/config/InspectConfig.vue +++ b/app/src/views/config/InspectConfig.vue @@ -2,6 +2,10 @@ import ngx from '@/api/ngx' import { logLevel } from '@/views/config/constants' +defineProps<{ + banner?: boolean +}>() + const data = ref({ level: 0, message: '', @@ -24,6 +28,7 @@ defineExpose({
+import type { Config } from '@/api/config' +import { HistoryOutlined } from '@ant-design/icons-vue' +import { message } from 'ant-design-vue' +import { trim, trimEnd } from 'lodash' +import config from '@/api/config' +import ngx from '@/api/ngx' +import CodeEditor from '@/components/CodeEditor' +import { ConfigHistory } from '@/components/ConfigHistory' +import FooterToolbar from '@/components/FooterToolbar' +import { useBreadcrumbs } from '@/composables/useBreadcrumbs' +import InspectConfig from '@/views/config/InspectConfig.vue' + +const route = useRoute() +const router = useRouter() + +// eslint-disable-next-line vue/require-typed-ref +const refForm = ref() +const origName = ref('') +const addMode = computed(() => !route.params.name) +const showHistory = ref(false) + +const basePath = computed(() => { + if (route.query.basePath) + return trim(route?.query?.basePath?.toString(), '/') + else if (typeof route.params.name === 'object') + return (route.params.name as string[]).slice(0, -1).join('/') + else + return '' +}) + +const data = ref({ + name: '', + content: '', + filepath: '', + sync_node_ids: [] as number[], + sync_overwrite: false, +} as Config) + +const modifiedAt = ref('') +const nginxConfigBase = ref('') +const loading = ref(true) + +const newPath = computed(() => { + // Decode and display after combining paths + const path = [nginxConfigBase.value, basePath.value, data.value.name] + .filter(v => v) + .join('/') + return path +}) + +const relativePath = computed(() => (basePath.value ? `${basePath.value}/${route.params.name}` : route.params.name) as string) +const breadcrumbs = useBreadcrumbs() + +// Use Vue 3.4+ useTemplateRef for InspectConfig component +const inspectConfigRef = useTemplateRef>('inspectConfig') + +// Expose data for right panel +defineExpose({ + data, + refForm, + addMode, + newPath, + modifiedAt, + origName, + loading, +}) + +async function init() { + const { name } = route.params + + data.value.name = name?.[name?.length - 1] ?? '' + origName.value = data.value.name + + if (!addMode.value) { + config.getItem(relativePath.value).then(r => { + data.value = r + modifiedAt.value = r.modified_at + + const filteredPath = trimEnd(data.value.filepath + .replaceAll(`${nginxConfigBase.value}/`, ''), data.value.name) + .split('/') + .filter(v => v) + + // Build accumulated path to maintain original encoding state + let accumulatedPath = '' + const path = filteredPath.map((segment, index) => { + // Decode for display + const decodedSegment = decodeURIComponent(segment) + + // Accumulated path keeps original encoding state + if (index === 0) { + accumulatedPath = segment + } + else { + accumulatedPath = `${accumulatedPath}/${segment}` + } + + return { + name: 'Manage Configs', + translatedName: () => decodedSegment, + path: '/config', + query: { + dir: accumulatedPath, + }, + hasChildren: false, + } + }) + + breadcrumbs.value = [{ + name: 'Dashboard', + translatedName: () => $gettext('Dashboard'), + path: '/dashboard', + hasChildren: false, + }, { + name: 'Manage Configs', + translatedName: () => $gettext('Manage Configs'), + path: '/config', + hasChildren: false, + }, ...path, { + name: 'Edit Config', + translatedName: () => origName.value, + hasChildren: false, + }] + }) + } + else { + data.value.content = '' + data.value.filepath = '' + + const pathSegments = basePath.value + .split('/') + .filter(v => v) + + // Build accumulated path + let accumulatedPath = '' + const path = pathSegments.map((segment, index) => { + // Decode for display + const decodedSegment = decodeURIComponent(segment) + + // Accumulated path keeps original encoding state + if (index === 0) { + accumulatedPath = segment + } + else { + accumulatedPath = `${accumulatedPath}/${segment}` + } + + return { + name: 'Manage Configs', + translatedName: () => decodedSegment, + path: '/config', + query: { + dir: accumulatedPath, + }, + hasChildren: false, + } + }) + + breadcrumbs.value = [{ + name: 'Dashboard', + translatedName: () => $gettext('Dashboard'), + path: '/dashboard', + hasChildren: false, + }, { + name: 'Manage Configs', + translatedName: () => $gettext('Manage Configs'), + path: '/config', + hasChildren: false, + }, ...path, { + name: 'Add Config', + translatedName: () => $gettext('Add Configuration'), + hasChildren: false, + }] + } + loading.value = false +} + +onMounted(async () => { + await config.get_base_path().then(r => { + nginxConfigBase.value = r.base_path + }) + await init() +}) + +function save() { + refForm.value?.validate().then(() => { + const payload = { + name: addMode.value ? data.value.name : undefined, + base_dir: addMode.value ? basePath.value : undefined, + content: data.value.content, + sync_node_ids: data.value.sync_node_ids, + sync_overwrite: data.value.sync_overwrite, + } + + const api = addMode.value + ? config.createItem(payload) + : config.updateItem(relativePath.value, payload) + + api.then(r => { + data.value.content = r.content + message.success($gettext('Saved successfully')) + + if (addMode.value) { + router.push({ + path: `/config/${data.value.name}/edit`, + query: { + basePath: basePath.value, + }, + }) + } + else { + data.value = r + // Run test after saving to verify configuration + inspectConfigRef.value?.test() + } + }) + }) +} + +function formatCode() { + ngx.format_code(data.value.content).then(r => { + data.value.content = r.content + message.success($gettext('Format successfully')) + }) +} + +function goBack() { + // Keep orignal path with encoding state + const encodedPath = basePath.value || '' + + router.push({ + path: '/config', + query: { + dir: encodedPath || undefined, + }, + }) +} + +function openHistory() { + showHistory.value = true +} + + + + + diff --git a/app/src/views/config/components/ConfigRightPanel.vue b/app/src/views/config/components/ConfigRightPanel.vue new file mode 100644 index 00000000..13c44bc1 --- /dev/null +++ b/app/src/views/config/components/ConfigRightPanel.vue @@ -0,0 +1,24 @@ + + + diff --git a/app/src/views/config/components/ConfigRightPanel/Basic.vue b/app/src/views/config/components/ConfigRightPanel/Basic.vue new file mode 100644 index 00000000..33250c20 --- /dev/null +++ b/app/src/views/config/components/ConfigRightPanel/Basic.vue @@ -0,0 +1,66 @@ + + + diff --git a/app/src/views/config/components/ConfigRightPanel/Chat.vue b/app/src/views/config/components/ConfigRightPanel/Chat.vue new file mode 100644 index 00000000..64c8a570 --- /dev/null +++ b/app/src/views/config/components/ConfigRightPanel/Chat.vue @@ -0,0 +1,15 @@ + + + diff --git a/app/src/views/config/components/ConfigRightPanel/ConfigRightPanel.vue b/app/src/views/config/components/ConfigRightPanel/ConfigRightPanel.vue new file mode 100644 index 00000000..11ab1005 --- /dev/null +++ b/app/src/views/config/components/ConfigRightPanel/ConfigRightPanel.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/app/src/views/config/components/ConfigRightPanel/Deploy.vue b/app/src/views/config/components/ConfigRightPanel/Deploy.vue new file mode 100644 index 00000000..31770518 --- /dev/null +++ b/app/src/views/config/components/ConfigRightPanel/Deploy.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/app/src/views/config/components/ConfigRightPanel/index.ts b/app/src/views/config/components/ConfigRightPanel/index.ts new file mode 100644 index 00000000..4e4b03bd --- /dev/null +++ b/app/src/views/config/components/ConfigRightPanel/index.ts @@ -0,0 +1,3 @@ +import ConfigRightPanel from './ConfigRightPanel.vue' + +export default ConfigRightPanel diff --git a/app/src/views/config/components/index.ts b/app/src/views/config/components/index.ts new file mode 100644 index 00000000..65900e8d --- /dev/null +++ b/app/src/views/config/components/index.ts @@ -0,0 +1,4 @@ +import ConfigLeftPanel from './ConfigLeftPanel.vue' +import ConfigRightPanel from './ConfigRightPanel.vue' + +export { ConfigLeftPanel, ConfigRightPanel } diff --git a/app/src/views/config/configColumns.tsx b/app/src/views/config/configColumns.tsx index 48fdaae8..030d2899 100644 --- a/app/src/views/config/configColumns.tsx +++ b/app/src/views/config/configColumns.tsx @@ -1,5 +1,4 @@ import type { CustomRenderArgs, StdTableColumn } from '@uozi-admin/curd' -import { FileFilled, FolderFilled } from '@ant-design/icons-vue' import { datetimeRender } from '@uozi-admin/curd' const configColumns: StdTableColumn[] = [{ @@ -15,8 +14,8 @@ const configColumns: StdTableColumn[] = [{ return (
{isDir - ? - : } + ?
+ :
}
) } diff --git a/app/src/views/site/site_edit/SiteEdit.vue b/app/src/views/site/site_edit/SiteEdit.vue index 9880539e..dc611e83 100644 --- a/app/src/views/site/site_edit/SiteEdit.vue +++ b/app/src/views/site/site_edit/SiteEdit.vue @@ -1,34 +1,24 @@ @@ -36,7 +26,7 @@ import SiteEditor from '@/views/site/site_edit/components/SiteEditor' :deep(.ant-card) { box-shadow: unset; - .card-body, .ant-tabs-content { + .ant-tabs-content { max-height: calc(100vh - 260px); } } diff --git a/app/src/views/site/site_edit/components/EnableTLS/EnableTLS.vue b/app/src/views/site/site_edit/components/EnableTLS/EnableTLS.vue index e9411429..fa76904c 100644 --- a/app/src/views/site/site_edit/components/EnableTLS/EnableTLS.vue +++ b/app/src/views/site/site_edit/components/EnableTLS/EnableTLS.vue @@ -7,7 +7,7 @@ import { useSiteEditorStore } from '@/views/site/site_edit/components/SiteEditor const [modal, ContextHolder] = Modal.useModal() const editorStore = useSiteEditorStore() -const { ngxConfig, curServerIdx, curDirectivesMap } = storeToRefs(editorStore) +const { ngxConfig, curServerIdx, curDirectivesMap, hasServers } = storeToRefs(editorStore) function confirmChangeTLS(status: CheckedType) { modal.confirm({ @@ -107,7 +107,7 @@ const supportSSL = computed(() => {