Files
oxygen-ui/src/pages/System/Tools/index.tsx

598 lines
26 KiB
TypeScript

import { ChangeEvent, KeyboardEvent } from 'react'
import Icon from '@ant-design/icons'
import { useTheme } from 'antd-style'
import {
DATABASE_DELETE_SUCCESS,
DATABASE_SELECT_SUCCESS,
DATABASE_UPDATE_SUCCESS,
TOOL_NOT_UNDER_REVIEW
} from '@/constants/common.constants'
import { message, modal } from '@/util/common'
import { navigateToCode } from '@/util/navigation'
import {
r_sys_tool_delete,
r_sys_tool_get,
r_sys_tool_get_one,
r_sys_tool_off_shelve,
r_sys_tool_pass,
r_sys_tool_reject
} from '@/services/system'
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 compiler from '@/components/Playground/compiler'
import { IImportMap } from '@/components/Playground/shared'
import { base64ToFiles, IMPORT_MAP_FILE_NAME, strToBase64 } from '@/components/Playground/files'
import Permission from '@/components/common/Permission'
const Tools = () => {
const theme = useTheme()
const navigate = useNavigate()
const [tableParams, setTableParams] = useState<TableParam>({
pagination: {
current: 1,
pageSize: 20,
position: ['bottomCenter'],
showTotal: (total, range) =>
`${
range[0] === range[1] ? `${range[0]}` : `${range[0]}~${range[1]}`
} 项 共 ${total}`
}
})
const [toolData, setToolData] = useState<ToolVo[]>([])
const [isLoading, setIsLoading] = useState(false)
const [searchType, setSearchType] = useState('ALL')
const [searchValue, setSearchValue] = useState('')
const [isUseRegex, setIsUseRegex] = useState(false)
const [isRegexLegal, setIsRegexLegal] = useState(true)
const [form] = AntdForm.useForm<{ pass: boolean }>()
const dataColumns: _ColumnsType<ToolVo> = [
{
dataIndex: 'icon',
title: '图标',
render: (value) => (
<AntdAvatar
src={
<AntdImage
preview={{ mask: <Icon component={IconOxygenEye} /> }}
src={`data:image/svg+xml;base64,${value}`}
alt={'Avatar'}
/>
}
style={{ background: theme.colorBgLayout }}
/>
),
width: '0',
align: 'center'
},
{
title: '名称',
render: (_, record) => (
<AntdTooltip title={record.description}>{record.name}</AntdTooltip>
)
},
{ dataIndex: 'toolId', title: '工具 ID' },
{ dataIndex: 'ver', title: '版本' },
{
title: '平台',
dataIndex: 'platform',
render: (value: string) => `${value.slice(0, 1)}${value.slice(1).toLowerCase()}`,
filters: [
{ text: 'Web', value: 'WEB' },
{ text: 'Desktop', value: 'DESKTOP' },
{ text: 'Android', value: 'ANDROID' }
]
},
{
title: '作者',
render: (_, record) => `${record.author.userInfo.nickname}(${record.author.username})`
},
{
dataIndex: 'keywords',
title: '关键词',
render: (value: string[]) => value.map((item) => <AntdTag>{item}</AntdTag>)
},
{
dataIndex: 'categories',
title: '类别',
render: (value: { name: string }[]) =>
value.map((item) => <AntdTag>{item.name}</AntdTag>)
},
{
dataIndex: 'review',
title: '状态',
width: '4em',
render: (value) => {
switch (value) {
case 'NONE':
return <AntdTag></AntdTag>
case 'PROCESSING':
return <AntdTag color={'purple'}></AntdTag>
case 'REJECT':
return <AntdTag color={'yellow'}></AntdTag>
case 'PASS':
return <AntdTag color={'green'}></AntdTag>
}
},
filters: [
{ text: '编码', value: 'NONE' },
{ text: '审核', value: 'PROCESSING' },
{ text: '驳回', value: 'REJECT' },
{ text: '通过', value: 'PASS' }
]
},
{
title: '操作',
width: '12em',
align: 'center',
render: (_, record) => (
<>
<AntdSpace size={'middle'}>
<a
style={{ color: theme.colorPrimary }}
onClick={handleOnViewBtnClick(record)}
>
</a>
<Permission operationCode={['system:tool:modify:tool']}>
{record.review === 'PROCESSING' && (
<a
style={{ color: theme.colorPrimary }}
onClick={handleOnReviewBtnClick(record)}
>
</a>
)}
{record.review === 'PASS' && (
<a
style={{ color: theme.colorPrimary }}
onClick={handleOnOffShelveBtnClick(record)}
>
</a>
)}
</Permission>
<Permission operationCode={['system:tool:delete:tool']}>
<a
style={{ color: theme.colorPrimary }}
onClick={handleOnDeleteBtnClick(record)}
>
</a>
</Permission>
</AntdSpace>{' '}
</>
)
}
]
const handleOnTableChange = (
pagination: _TablePaginationConfig,
filters: Record<string, _FilterValue | null>,
sorter: _SorterResult<ToolVo> | _SorterResult<ToolVo>[]
) => {
pagination = { ...tableParams.pagination, ...pagination }
if (Array.isArray(sorter)) {
setTableParams({
pagination,
filters,
sortField: sorter.map((value) => value.field).join(',')
})
} else {
setTableParams({
pagination,
filters,
sortField: sorter.field,
sortOrder: sorter.order
})
}
if (pagination.pageSize !== tableParams.pagination?.pageSize) {
setToolData([])
}
}
const handleOnViewBtnClick = (value: ToolVo) => {
return () => {
navigateToCode(navigate, value.id)
}
}
const handleOnReviewBtnClick = (value: ToolVo) => {
return () => {
form.setFieldValue('pass', undefined)
void modal.confirm({
centered: true,
maskClosable: true,
title: '审核',
footer: (_, { OkBtn, CancelBtn }) => (
<>
<OkBtn />
<CancelBtn />
</>
),
content: (
<AntdForm form={form}>
<AntdForm.Item
name={'pass'}
style={{ marginTop: 10 }}
rules={[{ required: true, message: '请选择审核结果' }]}
>
<AntdRadio.Group>
<AntdRadio value={true}></AntdRadio>
<AntdRadio value={false}></AntdRadio>
</AntdRadio.Group>
</AntdForm.Item>
</AntdForm>
),
onOk: () =>
form.validateFields().then(() => {
return new Promise<void>((resolve) => {
switch (form.getFieldValue('pass')) {
case true:
void message.loading({
content: '加载工具中……',
key: 'LOADING_TOOL',
duration: 0
})
void r_sys_tool_get_one(value.id)
.then((res) => {
message.destroy('LOADING_TOOL')
const response = res.data
switch (response.code) {
case DATABASE_SELECT_SUCCESS:
void message.loading({
content: '编译中……',
key: 'COMPILING',
duration: 0
})
try {
const toolVo = response.data!
const files = base64ToFiles(
toolVo.source.data!
)
const importMap = JSON.parse(
files[IMPORT_MAP_FILE_NAME].value
) as IImportMap
void compiler
.compile(
files,
importMap,
toolVo.entryPoint
)
.then((result) => {
message.destroy('COMPILING')
void message.loading({
content: '发布中……',
key: 'UPLOADING',
duration: 0
})
void r_sys_tool_pass(value.id, {
dist: strToBase64(
result.outputFiles[0].text
)
})
.then((res) => {
message.destroy('UPLOADING')
const response = res.data
switch (response.code) {
case DATABASE_UPDATE_SUCCESS:
void message.success(
'发布成功'
)
getTool()
break
case TOOL_NOT_UNDER_REVIEW:
void message.warning(
'工具处于非审核状态'
)
break
default:
void message.error(
'发布失败,请稍后重试'
)
}
})
.catch(() => {
message.destroy('UPLOADING')
})
.finally(() => {
resolve()
})
})
} catch (e) {
resolve()
message.destroy('COMPILING')
void message.error(
'编译失败,请检查代码后重试'
)
}
break
default:
resolve()
void message.error('加载工具失败,稍后重试')
}
})
.catch(() => {
message.destroy('LOADING_TOOL')
resolve()
})
break
default:
void r_sys_tool_reject(value.id)
.then((res) => {
const response = res.data
switch (response.code) {
case DATABASE_UPDATE_SUCCESS:
void message.success('更新成功')
resolve()
break
case TOOL_NOT_UNDER_REVIEW:
void message.warning('工具处于非审核状态')
resolve()
break
default:
void message.error('更新失败,请稍后重试')
resolve()
}
})
.finally(() => {
getTool()
})
}
})
})
})
}
}
const handleOnOffShelveBtnClick = (value: ToolVo) => {
return () => {
modal
.confirm({
centered: true,
maskClosable: true,
title: '确定下架',
content: `确定下架工具 ${value.author.username}:${value.toolId}:${value.ver} 吗?`
})
.then(
(confirmed) => {
if (confirmed) {
setIsLoading(true)
void r_sys_tool_off_shelve(value.id)
.then((res) => {
const response = res.data
if (response.code === DATABASE_UPDATE_SUCCESS) {
void message.success('下架成功')
setTimeout(() => {
getTool()
})
} else {
void message.error('下架失败,请稍后重试')
}
})
.finally(() => {
setIsLoading(false)
})
}
},
() => {}
)
}
}
const handleOnDeleteBtnClick = (value: ToolVo) => {
return () => {
modal
.confirm({
centered: true,
maskClosable: true,
title: '确定删除',
content: `确定删除工具 ${value.author.username}:${value.toolId}:${value.platform.slice(0, 1)}${value.platform.slice(1).toLowerCase()}:${value.ver} 吗?`
})
.then(
(confirmed) => {
if (confirmed) {
setIsLoading(true)
void r_sys_tool_delete(value.id)
.then((res) => {
const response = res.data
if (response.code === DATABASE_DELETE_SUCCESS) {
void message.success('删除成功')
setTimeout(() => {
getTool()
})
} else {
void message.error('删除失败,请稍后重试')
}
})
.finally(() => {
setIsLoading(false)
})
}
},
() => {}
)
}
}
const handleOnSearchValueChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchValue(e.target.value)
if (isUseRegex) {
try {
RegExp(e.target.value)
setIsRegexLegal(!(e.target.value.includes('{}') || e.target.value.includes('[]')))
} catch (e) {
setIsRegexLegal(false)
}
} else {
setIsRegexLegal(true)
}
}
const handleOnSearchValueKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
getTool()
}
}
const handleOnSearchTypeChange = (value: string) => {
setSearchType(value)
}
const handleOnUseRegexChange = (e: _CheckboxChangeEvent) => {
setIsUseRegex(e.target.checked)
if (e.target.checked) {
try {
RegExp(searchValue)
setIsRegexLegal(!(searchValue.includes('{}') || searchValue.includes('[]')))
} catch (e) {
setIsRegexLegal(false)
}
} else {
setIsRegexLegal(true)
}
}
const handleOnQueryBtnClick = () => {
getTool()
}
const getTool = () => {
if (isLoading) {
return
}
if (!isRegexLegal) {
void message.error('非法正则表达式')
return
}
setIsLoading(true)
void r_sys_tool_get({
currentPage: tableParams.pagination?.current,
pageSize: tableParams.pagination?.pageSize,
sortField:
tableParams.sortField && tableParams.sortOrder
? (tableParams.sortField as string)
: undefined,
sortOrder:
tableParams.sortField && tableParams.sortOrder ? tableParams.sortOrder : undefined,
searchType,
searchValue: searchValue.trim().length ? searchValue : undefined,
searchRegex: isUseRegex ? isUseRegex : undefined,
...tableParams.filters
})
.then((res) => {
const response = res.data
switch (response.code) {
case DATABASE_SELECT_SUCCESS:
setToolData(response.data!.records)
setTableParams({
...tableParams,
pagination: {
...tableParams.pagination,
total: response.data!.total
}
})
break
default:
void message.error('获取失败,请稍后重试')
}
})
.finally(() => {
setIsLoading(false)
})
}
useEffect(() => {
getTool()
}, [
JSON.stringify(tableParams.filters),
JSON.stringify(tableParams.sortField),
JSON.stringify(tableParams.sortOrder),
JSON.stringify(tableParams.pagination?.pageSize),
JSON.stringify(tableParams.pagination?.current)
])
const toolbar = (
<FlexBox direction={'horizontal'} gap={10}>
<Card style={{ overflow: 'inherit' }}>
<AntdInput
addonBefore={
<AntdSelect
value={searchType}
onChange={handleOnSearchTypeChange}
style={{ width: '6em' }}
dropdownStyle={{ textAlign: 'center' }}
>
<AntdSelect.Option value={'ALL'}></AntdSelect.Option>
<AntdSelect.Option value={'NAME'}></AntdSelect.Option>
<AntdSelect.Option value={'TOOL_ID'}> ID</AntdSelect.Option>
<AntdSelect.Option value={'NICKNAME'}></AntdSelect.Option>
<AntdSelect.Option value={'USERNAME'}></AntdSelect.Option>
<AntdSelect.Option value={'KEYWORD'}></AntdSelect.Option>
</AntdSelect>
}
suffix={
<>
{!isRegexLegal && (
<span style={{ color: theme.colorErrorText }}></span>
)}
<AntdCheckbox checked={isUseRegex} onChange={handleOnUseRegexChange}>
<AntdTooltip title={'正则表达式'}>.*</AntdTooltip>
</AntdCheckbox>
</>
}
allowClear
value={searchValue}
onChange={handleOnSearchValueChange}
onKeyDown={handleOnSearchValueKeyDown}
status={isRegexLegal ? undefined : 'error'}
placeholder={'请输入搜索内容'}
/>
</Card>
<Card style={{ overflow: 'inherit', flex: '0 0 auto' }}>
<AntdButton onClick={handleOnQueryBtnClick} type={'primary'}>
</AntdButton>
</Card>
</FlexBox>
)
const table = (
<Card>
<AntdTable
dataSource={toolData}
columns={dataColumns}
pagination={tableParams.pagination}
loading={isLoading}
scroll={{ x: true }}
onChange={handleOnTableChange}
/>
</Card>
)
return (
<FitFullscreen>
<HideScrollbar
style={{ padding: 20 }}
isShowVerticalScrollbar
autoHideWaitingTime={1000}
>
<FlexBox gap={20}>
{toolbar}
{table}
</FlexBox>
</HideScrollbar>
</FitFullscreen>
)
}
export default Tools