From 027519aa60cacedb1fbe33bf54b1b2a49a3a34dd Mon Sep 17 00:00:00 2001 From: FatttSnake Date: Mon, 22 Jan 2024 18:19:31 +0800 Subject: [PATCH] Add ToolTemplate management page --- src/constants/urls.constants.ts | 1 - src/global.d.ts | 12 +- src/pages/System/Tools/Base.tsx | 4 +- src/pages/System/Tools/Template.tsx | 891 +++++++++++++++++++++++++++- src/services/system.tsx | 18 +- 5 files changed, 911 insertions(+), 15 deletions(-) diff --git a/src/constants/urls.constants.ts b/src/constants/urls.constants.ts index 4b04f01..84eb783 100644 --- a/src/constants/urls.constants.ts +++ b/src/constants/urls.constants.ts @@ -28,7 +28,6 @@ export const URL_SYS_STATISTICS_ACTIVE = `${URL_SYS_STATISTICS}/active` export const URL_SYS_TOOL = '/system/tool' export const URL_SYS_TOOL_CATEGORY = `${URL_SYS_TOOL}/category` export const URL_SYS_TOOL_BASE = `${URL_SYS_TOOL}/base` -export const URL_SYS_TOOL_BASE_LIST = `${URL_SYS_TOOL_BASE}/list` export const URL_SYS_TOOL_TEMPLATE = `${URL_SYS_TOOL}/template` export const URL_API_V1 = '/api/v1' diff --git a/src/global.d.ts b/src/global.d.ts index d771a46..818b33a 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -507,23 +507,19 @@ interface ToolBaseAddEditParam { interface ToolTemplateVo { id: string name: string - ver: string baseId: string source: ToolDataVo - dist: ToolDataVo enable: boolean createTime: string updateTime: string } interface ToolTemplateAddEditParam { - id: string - name: string - ver: string - baseId: string + id?: string + name?: string + baseId?: string source?: string - dist?: string - enable: boolean + enable?: boolean } interface ToolVo { diff --git a/src/pages/System/Tools/Base.tsx b/src/pages/System/Tools/Base.tsx index 7734e0e..0990a33 100644 --- a/src/pages/System/Tools/Base.tsx +++ b/src/pages/System/Tools/Base.tsx @@ -13,7 +13,7 @@ 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_get, r_sys_tool_base_update } from '@/services/system' import { IFile, IFiles, ITsconfig } from '@/components/Playground/shared' @@ -307,7 +307,7 @@ const Base = () => { } setIsLoading(true) - void r_sys_tool_base_getList() + void r_sys_tool_base_get() .then((res) => { const response = res.data if (response.code === DATABASE_SELECT_SUCCESS) { diff --git a/src/pages/System/Tools/Template.tsx b/src/pages/System/Tools/Template.tsx index edf5155..f80bdb9 100644 --- a/src/pages/System/Tools/Template.tsx +++ b/src/pages/System/Tools/Template.tsx @@ -1,5 +1,894 @@ +import Icon from '@ant-design/icons' +import '@/assets/css/pages/system/tools/base.scss' +import { + COLOR_PRODUCTION, + DATABASE_DELETE_SUCCESS, + DATABASE_DUPLICATE_KEY, + DATABASE_INSERT_SUCCESS, + DATABASE_SELECT_SUCCESS, + DATABASE_UPDATE_SUCCESS +} from '@/constants/common.constants' +import { utcToLocalTime } from '@/util/datetime.tsx' +import { + r_sys_tool_template_update, + r_sys_tool_template_delete, + r_sys_tool_template_add, + r_sys_tool_template_get, + r_sys_tool_template_get_one, + r_sys_tool_base_get +} from '@/services/system' +import { IFile, IFiles, ITsconfig } from '@/components/Playground/shared' +import { + base64ToFiles, + fileNameToLanguage, + filesToBase64, + getFilesSize, + TS_CONFIG_FILE_NAME +} 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' +import { useState } from 'react' + const Template = () => { - return <>2 + const blocker = useBlocker( + ({ currentLocation, nextLocation }) => + currentLocation.pathname !== nextLocation.pathname && Object.keys(hasEdited).length > 0 + ) + const [modal, contextHolder] = AntdModal.useModal() + const [form] = AntdForm.useForm() + const formValues = AntdForm.useWatch([], form) + const [addFileForm] = AntdForm.useForm<{ fileName: string }>() + const [renameFileForm] = AntdForm.useForm<{ fileName: string }>() + const [newFormValues, setNewFormValues] = useState() + const [isLoading, setIsLoading] = useState(false) + const [isDrawerOpen, setIsDrawerOpen] = useState(false) + const [isDrawerEdit, setIsDrawerEdit] = useState(false) + const [baseData, setBaseData] = useState([]) + const [isLoadingBaseData, setIsLoadingBaseData] = useState(false) + const [submittable, setSubmittable] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + const [editingTemplateId, setEditingTemplateId] = useState('') + const [editingFiles, setEditingFiles] = useState>({}) + const [editingFileName, setEditingFileName] = useState('') + const [hasEdited, setHasEdited] = useState>({}) + const [templateData, setTemplateData] = useState([]) + const [templateDetailData, setTemplateDetailData] = useState>({}) + const [templateDetailLoading, setTemplateDetailLoading] = useState>({}) + const [tsconfig, setTsconfig] = useState() + + useBeforeUnload( + useCallback( + (event) => { + if (Object.keys(hasEdited).length) { + event.preventDefault() + event.returnValue = '' + } + }, + [hasEdited] + ), + { capture: true } + ) + + const handleOnAddBtnClick = () => { + if (Object.keys(hasEdited).length) { + void message.warning('新增前请保存修改') + return + } + setIsDrawerEdit(false) + setIsDrawerOpen(true) + form.setFieldValue('id', undefined) + form.setFieldValue('name', newFormValues?.name) + form.setFieldValue('baseId', newFormValues?.baseId) + form.setFieldValue('enable', newFormValues?.enable ?? true) + if (!baseData || !baseData.length) { + getBaseData() + } + } + + const templateColumns: _ColumnsType = [ + { + title: '名称', + render: (_, record) => ( + + {record.name} + + ) + }, + { + title: '基板', + dataIndex: ['base', 'name'] + }, + { + title: '创建时间', + dataIndex: 'createTime', + width: '20%', + align: 'center', + render: (value: string) => utcToLocalTime(value) + }, + { + title: '修改时间', + dataIndex: 'updateTime', + width: '20%', + align: 'center', + render: (value: string) => utcToLocalTime(value) + }, + { + title: '状态', + dataIndex: 'enable', + width: '5%', + align: 'center', + render: (value) => + value ? 启用 : 禁用 + }, + { + title: ( + <> + 操作 ( + + 新增 + + ) + + ), + dataIndex: 'enable', + width: '15em', + align: 'center', + render: (_, record) => ( + <> + + {hasEdited[record.id] && ( + + + 保存 + + + )} + + + 编辑 + + + + + 删除 + + + + + ) + } + ] + + const handleOnSaveBtnClick = (value: ToolTemplateVo) => { + return () => { + if (isLoading) { + return + } + setIsLoading(true) + + const source = filesToBase64(editingFiles[value.id]) + + void r_sys_tool_template_update({ id: value.id, source }) + .then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_UPDATE_SUCCESS: + void message.success('保存成功') + delete hasEdited[value.id] + setHasEdited({ ...hasEdited }) + getTemplateDetail(value) + break + default: + void message.error('出错了,请稍后重试') + } + }) + .finally(() => { + setIsLoading(false) + }) + } + } + + const handleOnEditBtnClick = (value: ToolTemplateVo) => { + return () => { + if (Object.keys(hasEdited).length) { + void message.warning('编辑前请保存修改') + return + } + + setIsDrawerEdit(true) + setIsDrawerOpen(true) + form.setFieldValue('id', value.id) + form.setFieldValue('name', value.name) + form.setFieldValue('baseId', value.baseId) + form.setFieldValue('enable', value.enable) + if (!baseData || !baseData.length) { + getBaseData() + } + void form.validateFields() + } + } + + const handleOnDeleteBtnClick = (value: ToolTemplateVo) => { + return () => { + modal + .confirm({ + title: '确定删除', + content: `确定删除模板 ${value.name} 吗?` + }) + .then( + (confirmed) => { + if (confirmed) { + setIsLoading(true) + + void r_sys_tool_template_delete(value.id) + .then((res) => { + const response = res.data + if (response.code === DATABASE_DELETE_SUCCESS) { + void message.success('删除成功') + setHasEdited({}) + setEditingFileName('') + setEditingFiles({}) + setEditingTemplateId('') + setTimeout(() => { + getTemplate() + }) + } else { + void message.error('删除失败,请稍后重试') + } + }) + .finally(() => { + setIsLoading(false) + }) + } + }, + () => {} + ) + } + } + + const filterOption = (input: string, option?: { label: string; value: string }) => + (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) + + const handleOnSubmit = () => { + if (isSubmitting) { + return + } + setIsSubmitting(true) + + if (isDrawerEdit) { + void r_sys_tool_template_update(formValues) + .then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_UPDATE_SUCCESS: + setIsDrawerOpen(false) + void message.success('更新成功') + getTemplate() + break + case DATABASE_DUPLICATE_KEY: + void message.error('已存在相同名称的模板') + break + default: + void message.error('更新失败,请稍后重试') + } + }) + .finally(() => { + setIsSubmitting(false) + }) + } else { + void r_sys_tool_template_add(formValues) + .then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_INSERT_SUCCESS: + setIsDrawerOpen(false) + void message.success('添加成功') + setNewFormValues(undefined) + getTemplate() + break + case DATABASE_DUPLICATE_KEY: + void message.error('已存在相同名称的模板') + break + default: + void message.error('添加失败,请稍后重试') + } + }) + .finally(() => { + setIsSubmitting(false) + }) + } + } + + const handleOnCloseBtnClick = () => { + setEditingFileName('') + } + + const handleOnDrawerClose = () => { + setIsDrawerOpen(false) + } + + const getTemplate = () => { + if (isLoading) { + return + } + setIsLoading(true) + + void r_sys_tool_template_get() + .then((res) => { + const response = res.data + if (response.code === DATABASE_SELECT_SUCCESS) { + setTemplateData(response.data!) + } else { + void message.error('获取失败,请稍后重试') + } + }) + .finally(() => { + setIsLoading(false) + }) + } + + const handleOnExpand = (expanded: boolean, record: ToolTemplateVo) => { + if (!expanded) { + return + } + getTemplateDetail(record) + } + + const getTemplateDetail = (record: ToolTemplateVo) => { + if (templateDetailLoading[record.id] || hasEdited[record.id]) { + return + } + setTemplateDetailLoading({ ...templateDetailLoading, [record.id]: true }) + + void r_sys_tool_template_get_one(record.id) + .then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_SELECT_SUCCESS: + setTemplateDetailData({ + ...templateDetailData, + [record.id]: response.data! + }) + break + default: + void message.error(`获取模板 ${record.name} 文件内容失败,请稍后重试`) + } + }) + .finally(() => { + setTemplateDetailLoading({ ...templateDetailLoading, [record.id]: false }) + }) + } + + const expandedRowRender = (record: ToolTemplateVo) => { + const templateDetailVo = templateDetailData[record.id] + let sourceFiles: IFiles | undefined = undefined + let sourceFileList: IFile[] = [] + if (templateDetailVo) { + sourceFiles = base64ToFiles(templateDetailVo.source.data) + sourceFileList = Object.values(sourceFiles) + } + + const handleOnAddFile = () => { + if (Object.keys(hasEdited).length) { + void message.warning('新建文件前请先保存更改') + return + } + + void modal.confirm({ + title: '新建文件', + content: ( + + ({ + validator() { + const newFileName = getFieldValue('fileName') as string + if ( + Object.keys(sourceFiles!) + .map((item) => item.toLowerCase()) + .includes(newFileName.toLowerCase()) + ) { + return Promise.reject(new Error('文件已存在')) + } + return Promise.resolve() + } + }) + ]} + > + + + + ), + onOk: () => + addFileForm.validateFields().then( + () => { + return new Promise((resolve) => { + const newFileName = addFileForm.getFieldValue('fileName') as string + + setTemplateDetailLoading({ + ...templateDetailLoading, + [record.id]: true + }) + + sourceFiles = { + ...sourceFiles, + [newFileName]: { + name: newFileName, + language: fileNameToLanguage(newFileName), + value: '' + } + } + + void r_sys_tool_template_update({ + id: record.id, + source: filesToBase64(sourceFiles) + }) + .then((res) => { + addFileForm.setFieldValue('fileName', '') + const response = res.data + switch (response.code) { + case DATABASE_UPDATE_SUCCESS: + void message.success('添加成功') + setTimeout(() => { + getTemplateDetail(record) + }) + resolve(true) + break + default: + void message.error('添加失败,请稍后重试') + resolve(true) + } + }) + .finally(() => { + setTemplateDetailLoading({ + ...templateDetailLoading, + [record.id]: false + }) + }) + }) + }, + () => { + return new Promise((_, reject) => { + reject('请输入文件名') + }) + } + ) + }) + } + + const detailColumns: _ColumnsType = [ + { title: '文件名', dataIndex: 'name' }, + { + title: ( + <> + 文件总大小 +
+ {sourceFiles ? getFilesSize(sourceFiles) : 'Unknown'} + + ), + width: '10em', + align: 'center' + }, + { + title: ( + <> + 操作 ( + + 新增 + + ) + + ), + dataIndex: 'enable', + width: '12em', + align: 'center', + render: (_, record) => ( + <> + + + + 编辑 + + + + + 重命名 + + + + + 删除 + + + + + ) + } + ] + + const handleOnEditFile = (fileName: string) => { + return () => { + if (editingTemplateId !== record.id) { + setTsconfig(undefined) + } + if (!hasEdited[record.id]) { + setEditingFiles({ ...editingFiles, [record.id]: sourceFiles! }) + } + setEditingTemplateId(record.id) + setEditingFileName(fileName) + } + } + + const handleOnRenameFile = (fileName: string) => { + return () => { + if (Object.keys(hasEdited).length) { + void message.warning('重命名文件前请先保存更改') + return + } + renameFileForm.setFieldValue('fileName', fileName) + void modal.confirm({ + title: '重命名文件', + content: ( + + ({ + 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() + } + }) + ]} + > + + + + ), + 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_template_update({ + id: record.id, + source: filesToBase64(sourceFiles) + }) + .then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_UPDATE_SUCCESS: + void message.success('重命名成功') + if ( + editingTemplateId === record.id && + editingFileName === fileName + ) { + setEditingFileName('') + } + setTimeout(() => { + getTemplateDetail(record) + }) + break + default: + void message.error('重命名失败,请稍后重试') + } + }) + .finally(() => { + setTemplateDetailLoading({ + ...templateDetailLoading, + [record.id]: false + }) + }) + + resolve(true) + }) + }, + () => { + return new Promise((_, reject) => { + reject('请输入文件名') + }) + } + ) + }) + } + } + + const handleOnDeleteFile = (fileName: string) => { + return () => { + if (Object.keys(hasEdited).length) { + void message.warning('删除文件前请先保存更改') + return + } + + modal + .confirm({ + title: '确定删除', + content: `确定删除文件 ${fileName} 吗?` + }) + .then( + (confirmed) => { + if (confirmed) { + setTemplateDetailLoading({ + ...templateDetailLoading, + [record.id]: true + }) + + delete sourceFiles![fileName] + + void r_sys_tool_template_update({ + id: record.id, + source: filesToBase64(sourceFiles!) + }) + .then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_UPDATE_SUCCESS: + void message.success('删除成功') + if ( + editingTemplateId === record.id && + editingFileName === fileName + ) { + setEditingFileName('') + } + setTimeout(() => { + getTemplateDetail(record) + }) + break + default: + void message.error('删除失败,请稍后重试') + } + }) + .finally(() => { + setTemplateDetailLoading({ + ...templateDetailLoading, + [record.id]: false + }) + }) + } + }, + () => {} + ) + } + } + + return ( + + record.name} + /> + + ) + } + + const handleOnChangeFileContent = (_content: string, _fileName: string, files: IFiles) => { + setEditingFiles({ ...editingFiles, [editingTemplateId]: files }) + setHasEdited({ ...hasEdited, [editingTemplateId]: true }) + } + + const getBaseData = () => { + if (isLoadingBaseData) { + return + } + setIsLoadingBaseData(true) + + void r_sys_tool_base_get() + .then((res) => { + const response = res.data + switch (response.code) { + case DATABASE_SELECT_SUCCESS: + setBaseData(response.data!) + break + default: + void message.error('获取基板列表失败,请稍后重试') + } + }) + .finally(() => { + setIsLoadingBaseData(false) + }) + } + + useEffect(() => { + try { + const tsconfigStr = editingFiles[editingTemplateId][TS_CONFIG_FILE_NAME].value + setTsconfig(JSON.parse(tsconfigStr) as ITsconfig) + } catch (e) { + /* empty */ + } + }, [editingFiles, editingTemplateId]) + + useEffect(() => { + form.validateFields({ validateOnly: true }).then( + () => { + setSubmittable(true) + }, + () => { + setSubmittable(false) + } + ) + + if (!isDrawerEdit && formValues) { + setNewFormValues({ + name: formValues.name, + enable: formValues.enable + }) + } + }, [formValues]) + + useEffect(() => { + getTemplate() + }, []) + + const drawerToolbar = ( + + + + + + + + 取消 + + + 提交 + + + ) + + const addAndEditForm = ( + + + + + + + ({ + value: value.id, + label: `${value.name}${!value.enable ? '(已禁用)' : ''}` + }))} + /> + + + + + + ) + + return ( + <> + + + + + record.id} + loading={isLoading} + pagination={false} + expandable={{ + expandedRowRender, + onExpand: handleOnExpand + }} + /> + + {editingFileName && ( + + {}} + onChangeFileContent={handleOnChangeFileContent} + showFileSelector={false} + tsconfig={tsconfig} + /> +
+ +
+
+ )} +
+
+ + {addAndEditForm} + +
+ {contextHolder} + blocker.proceed?.()} + onCancel={() => blocker.reset?.()} + > + 离开此页面将丢失所有未保存数据,是否继续? + + + ) } export default Template diff --git a/src/services/system.tsx b/src/services/system.tsx index 9271605..f26efad 100644 --- a/src/services/system.tsx +++ b/src/services/system.tsx @@ -19,7 +19,7 @@ import { URL_SYS_SETTINGS_SENSITIVE, URL_SYS_TOOL_CATEGORY, URL_SYS_TOOL_BASE, - URL_SYS_TOOL_BASE_LIST + URL_SYS_TOOL_TEMPLATE } from '@/constants/urls.constants' import request from '@/services/index' @@ -128,8 +128,6 @@ export const r_sys_tool_category_update = (param: ToolCategoryAddEditParam) => export const r_sys_tool_category_delete = (id: string) => request.delete(`${URL_SYS_TOOL_CATEGORY}/${id}`) -export const r_sys_tool_base_getList = () => request.get(URL_SYS_TOOL_BASE_LIST) - export const r_sys_tool_base_get = () => request.get(URL_SYS_TOOL_BASE) export const r_sys_tool_base_get_one = (id: string) => @@ -142,3 +140,17 @@ export const r_sys_tool_base_update = (param: ToolBaseAddEditParam) => request.put(URL_SYS_TOOL_BASE, param) export const r_sys_tool_base_delete = (id: string) => request.delete(`${URL_SYS_TOOL_BASE}/${id}`) + +export const r_sys_tool_template_get = () => request.get(URL_SYS_TOOL_TEMPLATE) + +export const r_sys_tool_template_get_one = (id: string) => + request.get(`${URL_SYS_TOOL_TEMPLATE}/${id}`) + +export const r_sys_tool_template_add = (param: ToolTemplateAddEditParam) => + request.post(URL_SYS_TOOL_TEMPLATE, param) + +export const r_sys_tool_template_update = (param: ToolTemplateAddEditParam) => + request.put(URL_SYS_TOOL_TEMPLATE, param) + +export const r_sys_tool_template_delete = (id: string) => + request.delete(`${URL_SYS_TOOL_TEMPLATE}/${id}`)