Complete main UI #37
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[data-component=system-tools-base] {
|
[data-component=system-tools-base] {
|
||||||
.root-content {
|
.root-content {
|
||||||
padding: 20px;
|
padding: 30px;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -11,5 +11,31 @@
|
|||||||
content: '*';
|
content: '*';
|
||||||
color: constants.$font-secondary-color;
|
color: constants.$font-secondary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
>*:first-child {
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:nth-child(2) {
|
||||||
|
position: sticky;
|
||||||
|
top: 20px;
|
||||||
|
height: calc(100vh - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-editor-btn {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background-color: constants.$font-secondary-color;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 2px 2px 10px 0 rgba(0,0,0,0.2);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,5 @@
|
|||||||
|
import Icon from '@ant-design/icons'
|
||||||
import '@/assets/css/pages/system/tools/base.scss'
|
import '@/assets/css/pages/system/tools/base.scss'
|
||||||
import FitFullscreen from '@/components/common/FitFullscreen.tsx'
|
|
||||||
import FlexBox from '@/components/common/FlexBox.tsx'
|
|
||||||
import HideScrollbar from '@/components/common/HideScrollbar.tsx'
|
|
||||||
import Card from '@/components/common/Card.tsx'
|
|
||||||
import CodeEditor from '@/components/Playground/CodeEditor'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { IFile, IFiles, ITsconfig } from '@/components/Playground/shared.ts'
|
|
||||||
import {
|
|
||||||
r_sys_tool_base_add,
|
|
||||||
r_sys_tool_base_delete,
|
|
||||||
r_sys_tool_base_get_one,
|
|
||||||
r_sys_tool_base_getList,
|
|
||||||
r_sys_tool_base_update
|
|
||||||
} from '@/services/system.tsx'
|
|
||||||
import {
|
import {
|
||||||
COLOR_PRODUCTION,
|
COLOR_PRODUCTION,
|
||||||
DATABASE_DELETE_SUCCESS,
|
DATABASE_DELETE_SUCCESS,
|
||||||
@@ -20,22 +7,40 @@ import {
|
|||||||
DATABASE_INSERT_SUCCESS,
|
DATABASE_INSERT_SUCCESS,
|
||||||
DATABASE_SELECT_SUCCESS,
|
DATABASE_SELECT_SUCCESS,
|
||||||
DATABASE_UPDATE_SUCCESS
|
DATABASE_UPDATE_SUCCESS
|
||||||
} from '@/constants/common.constants.ts'
|
} from '@/constants/common.constants'
|
||||||
import { utcToLocalTime } from '@/util/datetime.tsx'
|
import { utcToLocalTime } from '@/util/datetime.tsx'
|
||||||
import Permission from '@/components/common/Permission.tsx'
|
import {
|
||||||
|
r_sys_tool_base_add,
|
||||||
|
r_sys_tool_base_delete,
|
||||||
|
r_sys_tool_base_get_one,
|
||||||
|
r_sys_tool_base_getList,
|
||||||
|
r_sys_tool_base_update
|
||||||
|
} from '@/services/system'
|
||||||
|
import { IFile, IFiles, ITsconfig } from '@/components/Playground/shared'
|
||||||
import {
|
import {
|
||||||
base64ToFiles,
|
base64ToFiles,
|
||||||
fileNameToLanguage,
|
fileNameToLanguage,
|
||||||
filesToBase64,
|
filesToBase64,
|
||||||
getFilesSize,
|
getFilesSize,
|
||||||
TS_CONFIG_FILE_NAME
|
TS_CONFIG_FILE_NAME
|
||||||
} from '@/components/Playground/files.ts'
|
} from '@/components/Playground/files'
|
||||||
|
import FitFullscreen from '@/components/common/FitFullscreen'
|
||||||
|
import FlexBox from '@/components/common/FlexBox'
|
||||||
|
import HideScrollbar from '@/components/common/HideScrollbar'
|
||||||
|
import Card from '@/components/common/Card'
|
||||||
|
import CodeEditor from '@/components/Playground/CodeEditor'
|
||||||
|
import Permission from '@/components/common/Permission'
|
||||||
|
|
||||||
const Base = () => {
|
const Base = () => {
|
||||||
|
const blocker = useBlocker(
|
||||||
|
({ currentLocation, nextLocation }) =>
|
||||||
|
currentLocation.pathname !== nextLocation.pathname && Object.keys(hasEdited).length > 0
|
||||||
|
)
|
||||||
const [modal, contextHolder] = AntdModal.useModal()
|
const [modal, contextHolder] = AntdModal.useModal()
|
||||||
const [form] = AntdForm.useForm<ToolBaseAddEditParam>()
|
const [form] = AntdForm.useForm<ToolBaseAddEditParam>()
|
||||||
const formValues = AntdForm.useWatch([], form)
|
const formValues = AntdForm.useWatch([], form)
|
||||||
const [addFileForm] = AntdForm.useForm<{ fileName: string }>()
|
const [addFileForm] = AntdForm.useForm<{ fileName: string }>()
|
||||||
|
const [renameFileForm] = AntdForm.useForm<{ fileName: string }>()
|
||||||
const [newFormValues, setNewFormValues] = useState<ToolBaseAddEditParam>()
|
const [newFormValues, setNewFormValues] = useState<ToolBaseAddEditParam>()
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
|
||||||
@@ -51,6 +56,19 @@ const Base = () => {
|
|||||||
const [baseDetailLoading, setBaseDetailLoading] = useState<Record<string, boolean>>({})
|
const [baseDetailLoading, setBaseDetailLoading] = useState<Record<string, boolean>>({})
|
||||||
const [tsconfig, setTsconfig] = useState<ITsconfig>()
|
const [tsconfig, setTsconfig] = useState<ITsconfig>()
|
||||||
|
|
||||||
|
useBeforeUnload(
|
||||||
|
useCallback(
|
||||||
|
(event) => {
|
||||||
|
if (Object.keys(hasEdited).length) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.returnValue = ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[hasEdited]
|
||||||
|
),
|
||||||
|
{ capture: true }
|
||||||
|
)
|
||||||
|
|
||||||
const handleOnAddBtnClick = () => {
|
const handleOnAddBtnClick = () => {
|
||||||
if (Object.keys(hasEdited).length) {
|
if (Object.keys(hasEdited).length) {
|
||||||
void message.warning('新增前请保存修改')
|
void message.warning('新增前请保存修改')
|
||||||
@@ -275,6 +293,10 @@ const Base = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOnCloseBtnClick = () => {
|
||||||
|
setEditingFileName('')
|
||||||
|
}
|
||||||
|
|
||||||
const handleOnDrawerClose = () => {
|
const handleOnDrawerClose = () => {
|
||||||
setIsDrawerOpen(false)
|
setIsDrawerOpen(false)
|
||||||
}
|
}
|
||||||
@@ -301,7 +323,6 @@ const Base = () => {
|
|||||||
|
|
||||||
const handleOnExpand = (expanded: boolean, record: ToolBaseVo) => {
|
const handleOnExpand = (expanded: boolean, record: ToolBaseVo) => {
|
||||||
if (!expanded) {
|
if (!expanded) {
|
||||||
setEditingFileName('')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
getBaseDetail(record)
|
getBaseDetail(record)
|
||||||
@@ -361,7 +382,11 @@ const Base = () => {
|
|||||||
({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
validator() {
|
validator() {
|
||||||
const newFileName = getFieldValue('fileName') as string
|
const newFileName = getFieldValue('fileName') as string
|
||||||
if (Object.keys(sourceFiles!).includes(newFileName)) {
|
if (
|
||||||
|
Object.keys(sourceFiles!)
|
||||||
|
.map((item) => item.toLowerCase())
|
||||||
|
.includes(newFileName.toLowerCase())
|
||||||
|
) {
|
||||||
return Promise.reject(new Error('文件已存在'))
|
return Promise.reject(new Error('文件已存在'))
|
||||||
}
|
}
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
@@ -451,7 +476,7 @@ const Base = () => {
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
dataIndex: 'enable',
|
dataIndex: 'enable',
|
||||||
width: '10em',
|
width: '12em',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<>
|
<>
|
||||||
@@ -464,6 +489,14 @@ const Base = () => {
|
|||||||
编辑
|
编辑
|
||||||
</a>
|
</a>
|
||||||
</Permission>
|
</Permission>
|
||||||
|
<Permission operationCode={'system:tool:modify:category'}>
|
||||||
|
<a
|
||||||
|
onClick={handleOnRenameFile(record.name)}
|
||||||
|
style={{ color: COLOR_PRODUCTION }}
|
||||||
|
>
|
||||||
|
重命名
|
||||||
|
</a>
|
||||||
|
</Permission>
|
||||||
<Permission operationCode={'system:tool:delete:category'}>
|
<Permission operationCode={'system:tool:delete:category'}>
|
||||||
<a
|
<a
|
||||||
onClick={handleOnDeleteFile(record.name)}
|
onClick={handleOnDeleteFile(record.name)}
|
||||||
@@ -491,9 +524,114 @@ const Base = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleOnRenameFile = (fileName: string) => {
|
||||||
|
return () => {
|
||||||
|
if (Object.keys(hasEdited).length) {
|
||||||
|
void message.warning('重命名文件前请先保存更改')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
renameFileForm.setFieldValue('fileName', fileName)
|
||||||
|
void modal.confirm({
|
||||||
|
title: '重命名文件',
|
||||||
|
content: (
|
||||||
|
<AntdForm form={renameFileForm}>
|
||||||
|
<AntdForm.Item
|
||||||
|
name={'fileName'}
|
||||||
|
label={'新文件名'}
|
||||||
|
style={{ marginTop: 10 }}
|
||||||
|
rules={[
|
||||||
|
{ required: true },
|
||||||
|
{
|
||||||
|
pattern: /\.(jsx|tsx|js|ts|css|json)$/,
|
||||||
|
message:
|
||||||
|
'仅支持 *.jsx, *.tsx, *.js, *.ts, *.css, *.json 文件'
|
||||||
|
},
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator() {
|
||||||
|
const newFileName = getFieldValue('fileName') as string
|
||||||
|
if (
|
||||||
|
Object.keys(sourceFiles!)
|
||||||
|
.map((item) => item.toLowerCase())
|
||||||
|
.includes(newFileName?.toLowerCase()) &&
|
||||||
|
newFileName.toLowerCase() !== fileName.toLowerCase()
|
||||||
|
) {
|
||||||
|
return Promise.reject(new Error('文件已存在'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<AntdInput />
|
||||||
|
</AntdForm.Item>
|
||||||
|
</AntdForm>
|
||||||
|
),
|
||||||
|
onOk: () =>
|
||||||
|
renameFileForm.validateFields().then(
|
||||||
|
() => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const newFileName = renameFileForm.getFieldValue(
|
||||||
|
'fileName'
|
||||||
|
) as string
|
||||||
|
const temp = sourceFiles![fileName].value
|
||||||
|
delete sourceFiles![fileName]
|
||||||
|
|
||||||
|
sourceFiles = {
|
||||||
|
...sourceFiles,
|
||||||
|
[newFileName]: {
|
||||||
|
name: newFileName,
|
||||||
|
language: fileNameToLanguage(newFileName),
|
||||||
|
value: temp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void r_sys_tool_base_update({
|
||||||
|
id: record.id,
|
||||||
|
source: filesToBase64(sourceFiles)
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
const response = res.data
|
||||||
|
switch (response.code) {
|
||||||
|
case DATABASE_UPDATE_SUCCESS:
|
||||||
|
void message.success('重命名成功')
|
||||||
|
if (
|
||||||
|
editingBaseId === record.id &&
|
||||||
|
editingFileName === fileName
|
||||||
|
) {
|
||||||
|
setEditingFileName('')
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
getBaseDetail(record)
|
||||||
|
})
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
void message.error('重命名失败,请稍后重试')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setBaseDetailLoading({
|
||||||
|
...baseDetailLoading,
|
||||||
|
[record.id]: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
return new Promise((_, reject) => {
|
||||||
|
reject('请输入文件名')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleOnDeleteFile = (fileName: string) => {
|
const handleOnDeleteFile = (fileName: string) => {
|
||||||
return () => {
|
return () => {
|
||||||
if (hasEdited) {
|
if (Object.keys(hasEdited).length) {
|
||||||
void message.warning('删除文件前请先保存更改')
|
void message.warning('删除文件前请先保存更改')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -519,6 +657,12 @@ const Base = () => {
|
|||||||
switch (response.code) {
|
switch (response.code) {
|
||||||
case DATABASE_UPDATE_SUCCESS:
|
case DATABASE_UPDATE_SUCCESS:
|
||||||
void message.success('删除成功')
|
void message.success('删除成功')
|
||||||
|
if (
|
||||||
|
editingBaseId === record.id &&
|
||||||
|
editingFileName === fileName
|
||||||
|
) {
|
||||||
|
setEditingFileName('')
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
getBaseDetail(record)
|
getBaseDetail(record)
|
||||||
})
|
})
|
||||||
@@ -547,6 +691,7 @@ const Base = () => {
|
|||||||
dataSource={sourceFileList}
|
dataSource={sourceFileList}
|
||||||
columns={detailColumns}
|
columns={detailColumns}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
|
rowKey={(record) => record.name}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
@@ -625,8 +770,8 @@ const Base = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FitFullscreen data-component={'system-tools-base'}>
|
<FitFullscreen data-component={'system-tools-base'}>
|
||||||
<FlexBox direction={'horizontal'} className={'root-content'}>
|
<HideScrollbar>
|
||||||
<HideScrollbar>
|
<FlexBox direction={'horizontal'} className={'root-content'}>
|
||||||
<Card>
|
<Card>
|
||||||
<AntdTable
|
<AntdTable
|
||||||
dataSource={baseData}
|
dataSource={baseData}
|
||||||
@@ -640,20 +785,23 @@ const Base = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</HideScrollbar>
|
{editingFileName && (
|
||||||
{editingFileName && (
|
<Card>
|
||||||
<Card>
|
<CodeEditor
|
||||||
<CodeEditor
|
files={editingFiles[editingBaseId]}
|
||||||
files={editingFiles[editingBaseId]}
|
selectedFileName={editingFileName}
|
||||||
selectedFileName={editingFileName}
|
onSelectedFileChange={() => {}}
|
||||||
onSelectedFileChange={() => {}}
|
onChangeFileContent={handleOnChangeFileContent}
|
||||||
onChangeFileContent={handleOnChangeFileContent}
|
showFileSelector={false}
|
||||||
showFileSelector={false}
|
tsconfig={tsconfig}
|
||||||
tsconfig={tsconfig}
|
/>
|
||||||
/>
|
<div className={'close-editor-btn'} onClick={handleOnCloseBtnClick}>
|
||||||
</Card>
|
<Icon component={IconOxygenClose} />
|
||||||
)}
|
</div>
|
||||||
</FlexBox>
|
</Card>
|
||||||
|
)}
|
||||||
|
</FlexBox>
|
||||||
|
</HideScrollbar>
|
||||||
<AntdDrawer
|
<AntdDrawer
|
||||||
title={isDrawerEdit ? '编辑基板' : '添加基板'}
|
title={isDrawerEdit ? '编辑基板' : '添加基板'}
|
||||||
onClose={handleOnDrawerClose}
|
onClose={handleOnDrawerClose}
|
||||||
@@ -666,6 +814,14 @@ const Base = () => {
|
|||||||
</AntdDrawer>
|
</AntdDrawer>
|
||||||
</FitFullscreen>
|
</FitFullscreen>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
|
<AntdModal
|
||||||
|
open={blocker.state === 'blocked'}
|
||||||
|
title={'未保存'}
|
||||||
|
onOk={() => blocker.proceed?.()}
|
||||||
|
onCancel={() => blocker.reset?.()}
|
||||||
|
>
|
||||||
|
离开此页面将丢失所有未保存数据,是否继续?
|
||||||
|
</AntdModal>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ export default defineConfig({
|
|||||||
'react-router-dom',
|
'react-router-dom',
|
||||||
{
|
{
|
||||||
react: ['Suspense', 'createContext'],
|
react: ['Suspense', 'createContext'],
|
||||||
'react-router': ['useMatches', 'RouterProvider'],
|
'react-router': ['useMatches', 'RouterProvider', 'useBlocker'],
|
||||||
'react-router-dom': ['createBrowserRouter'],
|
'react-router-dom': ['createBrowserRouter', 'useBeforeUnload'],
|
||||||
antd: ['message', 'notification']
|
antd: ['message', 'notification']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user