Files
oxygen-ui/src/pages/Tools/Create.tsx
FatttSnake 51ee15749e Fix(Preview): Fixed code pollution bug
Methods and variables in base and main code are isolated to solve the problem of code pollution. Close #4
2024-09-09 17:22:42 +08:00

394 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Icon from '@ant-design/icons'
import '@/assets/css/pages/tools/create.scss'
import {
DATABASE_DUPLICATE_KEY,
DATABASE_INSERT_SUCCESS,
DATABASE_SELECT_SUCCESS
} from '@/constants/common.constants'
import { navigateToEdit } from '@/util/navigation'
import {
r_tool_category_get,
r_tool_create,
r_tool_template_get,
r_tool_template_get_one
} from '@/services/tool'
import compiler from '@/components/Playground/compiler'
import { IImportMap } from '@/components/Playground/shared'
import { base64ToFiles, base64ToStr, IMPORT_MAP_FILE_NAME } from '@/components/Playground/files'
import FlexBox from '@/components/common/FlexBox'
import Card from '@/components/common/Card'
import FitFullscreen from '@/components/common/FitFullscreen'
import HideScrollbar from '@/components/common/HideScrollbar'
import Playground from '@/components/Playground'
const Create = () => {
const navigate = useNavigate()
const [form] = AntdForm.useForm<ToolCreateParam>()
const formValues = AntdForm.useWatch([], form)
const [templateData, setTemplateData] = useState<ToolTemplateVo[]>()
const [categoryData, setCategoryData] = useState<ToolCategoryVo[]>()
const [templateDetailData, setTemplateDetailData] = useState<Record<string, ToolTemplateVo>>({})
const [previewTemplate, setPreviewTemplate] = useState('')
const [isLoadingTemplate, setIsLoadingTemplate] = useState(false)
const [isLoadingCategory, setIsLoadingCategory] = useState(false)
const [isCreating, setIsCreating] = useState(false)
const [compiledCode, setCompiledCode] = useState('')
const handleOnFinish = (toolAddParam: ToolCreateParam) => {
setIsCreating(true)
void r_tool_create(toolAddParam)
.then((res) => {
const response = res.data
switch (response.code) {
case DATABASE_INSERT_SUCCESS:
void message.success(
`创建工具 ${response.data!.name}<${response.data!.toolId}:${response.data!.platform.slice(0, 1)}${response.data!.platform.slice(1).toLowerCase()}:${response.data!.ver}> 成功`
)
navigateToEdit(navigate, response.data!.toolId, response.data!.platform)
break
case DATABASE_DUPLICATE_KEY:
void message.warning('已存在相同 ID 的应用')
setIsCreating(false)
break
default:
void message.error('创建失败,请稍后重试')
setIsCreating(false)
}
})
.catch(() => {
setIsCreating(false)
})
}
const handleOnIconBeforeUpload = (
file: Parameters<_GetProp<_UploadProps, 'beforeUpload'>>[0]
) => {
if (file.type !== 'image/svg+xml') {
void message.error('仅支持 svg 文件')
return false
}
if (file.size / 1024 / 1024 > 2) {
void message.error('文件大小不能大于2MiB')
}
const reader = new FileReader()
reader.addEventListener('load', () => {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
form.setFieldValue('icon', reader.result!.toString().split(',')[1])
void form.validateFields(['icon'])
})
reader.readAsDataURL(file)
return false
}
const handleOnPlatformChange = (value: string) => {
setIsLoadingTemplate(true)
void r_tool_template_get({
platform: value
})
.then((res) => {
const response = res.data
switch (response.code) {
case DATABASE_SELECT_SUCCESS:
setTemplateData(response.data!)
response.data?.length
? form.setFieldValue('templateId', response.data?.[0].id)
: form.setFieldValue('templateId', null)
response.data?.length && handleOnTemplateChange(response.data?.[0].id)
break
default:
void message.error('获取模板列表失败,请稍后重试')
}
})
.finally(() => {
setIsLoadingTemplate(false)
})
}
const handleOnTemplateChange = (value: string) => {
setPreviewTemplate(value)
if (templateDetailData[value]) {
return
}
setIsLoadingTemplate(true)
void r_tool_template_get_one(value)
.then((res) => {
const response = res.data
switch (response.code) {
case DATABASE_SELECT_SUCCESS:
setTemplateDetailData({ ...templateDetailData, [value]: response.data! })
break
default:
void message.error('获取模板信息失败')
}
})
.finally(() => {
setIsLoadingTemplate(false)
})
}
useEffect(() => {
const template = templateDetailData[previewTemplate]
if (!template) {
return
}
setCompiledCode('')
try {
const baseDist = base64ToStr(template.base.dist.data!)
const files = base64ToFiles(template.source.data!)
const importMap = JSON.parse(files[IMPORT_MAP_FILE_NAME].value) as IImportMap
void compiler
.compile(files, importMap, template.entryPoint)
.then((result) => {
const output = result.outputFiles[0].text
setCompiledCode(`(() => {${output}})();\n(() => {${baseDist}})();`)
})
.catch((reason) => {
void message.error(`编译失败:${reason}`)
setCompiledCode(baseDist)
})
} catch (e) {
void message.error(`载入模板 ${templateDetailData[previewTemplate].name} 失败`)
}
}, [templateDetailData, previewTemplate])
useEffect(() => {
const temp: string[] = []
formValues?.keywords.forEach((item) => {
if (item.length <= 10) {
temp.push(item)
}
})
form.setFieldValue('keyword', temp)
}, [form, formValues?.keywords])
useEffect(() => {
setIsLoadingCategory(true)
void r_tool_category_get()
.then((res) => {
const response = res.data
switch (response.code) {
case DATABASE_SELECT_SUCCESS:
setCategoryData(response.data!)
break
default:
void message.error('获取类别列表失败,请稍后重试')
}
})
.finally(() => {
setIsLoadingCategory(false)
})
}, [])
return (
<FitFullscreen data-component={'tools-create'}>
<FlexBox direction={'horizontal'} className={'root-content'}>
<FlexBox>
<Card className={'title'}>
<FlexBox></FlexBox>
</Card>
<Card className={'config'}>
<HideScrollbar>
<div className={'config-content'}>
<AntdForm
form={form}
layout={'vertical'}
onFinish={handleOnFinish}
disabled={isCreating}
>
<AntdForm.Item
label={'图标'}
name={'icon'}
rules={[
({ getFieldValue }) => ({
validator() {
if (!getFieldValue('icon')) {
return Promise.reject(
new Error('请选择图标')
)
}
return Promise.resolve()
}
})
]}
getValueFromEvent={() => {}}
>
<AntdUpload
listType={'picture-card'}
showUploadList={false}
beforeUpload={handleOnIconBeforeUpload}
accept={'image/svg+xml'}
>
{formValues?.icon ? (
<img
src={`data:image/svg+xml;base64,${formValues.icon}`}
alt={'icon'}
style={{ width: '100%' }}
/>
) : (
<Icon component={IconOxygenPlus} />
)}
</AntdUpload>
</AntdForm.Item>
<AntdForm.Item name={'icon'} hidden>
<AntdInput />
</AntdForm.Item>
<AntdForm.Item
label={'名称'}
name={'name'}
rules={[{ required: true, whitespace: true }]}
>
<AntdInput
maxLength={20}
showCount
placeholder={'请输入名称'}
/>
</AntdForm.Item>
<AntdForm.Item
label={'工具 ID'}
name={'toolId'}
rules={[
{ required: true, whitespace: true },
{
pattern: /^[a-zA-Z-_][0-9a-zA-Z-_]{2,19}$/,
message:
'只能包含字母、数字、连字符和下划线,不能以数字开头'
}
]}
>
<AntdInput
maxLength={20}
showCount
placeholder={'请输入工具 ID'}
/>
</AntdForm.Item>
<AntdForm.Item
label={'平台'}
name={'platform'}
rules={[{ required: true, whitespace: true }]}
>
<AntdSelect
onChange={handleOnPlatformChange}
placeholder={'请选择平台'}
>
<AntdSelect.Option key={'WEB'}>Web</AntdSelect.Option>
<AntdSelect.Option key={'DESKTOP'}>
Desktop
</AntdSelect.Option>
<AntdSelect.Option key={'ANDROID'}>
Android
</AntdSelect.Option>
</AntdSelect>
</AntdForm.Item>
<AntdForm.Item label={'简介'} name={'description'}>
<AntdInput.TextArea
autoSize={{ minRows: 6, maxRows: 6 }}
maxLength={200}
showCount
placeholder={'请输入简介'}
/>
</AntdForm.Item>
<AntdForm.Item
label={'版本'}
name={'ver'}
rules={[
{ required: true, whitespace: true },
{
pattern: /^\d+\.\d+\.\d+$/,
message: `格式必须为 '<数字>.<数字>.<数字>', eg. 1.0.3`
}
]}
>
<AntdInput
maxLength={10}
showCount
placeholder={'请输入版本'}
/>
</AntdForm.Item>
<AntdForm.Item
label={'模板'}
name={'templateId'}
rules={[{ required: true, whitespace: true }]}
>
<AntdSelect
options={templateData?.map((value) => ({
value: value.id,
label: value.name
}))}
loading={isLoadingTemplate}
disabled={isLoadingTemplate}
onChange={handleOnTemplateChange}
placeholder={'请选择模板'}
/>
</AntdForm.Item>
<AntdForm.Item
label={'关键字'}
tooltip={'工具搜索每个不超过10个字符'}
name={'keywords'}
rules={[{ required: true }]}
>
<AntdSelect
mode={'tags'}
maxCount={20}
placeholder={'请输入关键字'}
/>
</AntdForm.Item>
<AntdForm.Item
label={'类别'}
tooltip={'工具分类'}
name={'categories'}
rules={[{ required: true }]}
>
<AntdSelect
mode={'multiple'}
options={categoryData?.map((value) => ({
value: value.id,
label: value.name
}))}
loading={isLoadingCategory}
disabled={isLoadingCategory}
placeholder={'请选择类别'}
/>
</AntdForm.Item>
<AntdForm.Item>
<AntdButton
className={'create-bt'}
type={'primary'}
htmlType={'submit'}
loading={isCreating}
>
</AntdButton>
</AntdForm.Item>
</AntdForm>
</div>
</HideScrollbar>
</Card>
</FlexBox>
<FlexBox>
<Card className={'title'}>
<FlexBox></FlexBox>
</Card>
<Card className={'preview'}>
{compiledCode ? (
<Playground.Output.Preview.Render
iframeKey={previewTemplate}
compiledCode={compiledCode}
mobileMode={formValues.platform === 'ANDROID'}
/>
) : (
<span className={'no-preview'}></span>
)}
</Card>
</FlexBox>
</FlexBox>
</FitFullscreen>
)
}
export default Create