Browse Source

添加XForm模块

main
dark 3 months ago
parent
commit
f0967abc95
  1. 2
      .env.proxy.local
  2. 5
      src/global.d.ts
  3. 4
      src/layout/ListPageLayout.tsx
  4. 8
      src/layout/style.ts
  5. 0
      src/pages/db/vod/components/Edit.tsx
  6. 0
      src/pages/db/vod/components/context.ts
  7. 0
      src/pages/db/vod/components/form/PrimaryFacts.tsx
  8. 0
      src/pages/db/vod/components/style.ts
  9. 288
      src/pages/db/vod/index.tsx
  10. 2
      src/pages/db/vod/style.ts
  11. 86
      src/pages/x-form/hooks/useApi.tsx
  12. 301
      src/pages/x-form/index.tsx
  13. 14
      src/pages/x-form/style.ts
  14. 32
      src/request-base-url-interceptors.ts
  15. 254
      src/request.ts
  16. 4
      src/service/base.ts
  17. 13
      src/service/db/vod.ts
  18. 10
      src/service/x-form/model.ts
  19. 90
      src/store/db/vod.ts
  20. 124
      src/store/x-form/model.ts
  21. 42
      src/types/db/vod.d.ts
  22. 61
      src/types/x-form/model.d.ts
  23. 14
      src/utils/index.ts
  24. 82
      vite.config.ts
  25. 777
      yarn.lock

2
.env.proxy.local

@ -1 +1 @@
API_URL=http://47.113.117.106:8000
API_URL=http://47.113.117.106:10000

5
src/global.d.ts

@ -39,6 +39,11 @@ export type IApiResult<T = any> = {
message: string; message: string;
} }
export type IError = {
code: number;
message: string;
}
export type TreeItem<T> = { export type TreeItem<T> = {
children?: TreeItem<T>[]; children?: TreeItem<T>[];
[key: keyof T]: T[keyof T]; [key: keyof T]: T[keyof T];

4
src/layout/ListPageLayout.tsx

@ -47,7 +47,7 @@ const ListPageLayout: React.FC<IListPageLayoutProps> = (
ghost={false} ghost={false}
// breadcrumbRender={false} // breadcrumbRender={false}
title={ title={
<Space>
<Space align={'center'} className={'page-title'} >
{ {
currentMenu?.parent && <span className={'black'} currentMenu?.parent && <span className={'black'}
onClick={() => { onClick={() => {
@ -58,7 +58,7 @@ const ListPageLayout: React.FC<IListPageLayoutProps> = (
}) })
}}> <ArrowLeftOutlined/></span> }}> <ArrowLeftOutlined/></span>
} }
<span>{title || currentMenu?.title}</span>
{title || currentMenu?.title}
</Space> </Space>
} }
className={cx(styles.container, styles.pageCard, styles.layoutTable, className)} className={cx(styles.container, styles.pageCard, styles.layoutTable, className)}

8
src/layout/style.ts

@ -367,6 +367,14 @@ export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any)
min-height: 100%; min-height: 100%;
} }
} }
.page-title{
justify-content: center;
.ant-space-item{
display: inline-flex;
justify-content: center;
}
}
` `
return { return {

0
src/pages/db/movie/components/Edit.tsx → src/pages/db/vod/components/Edit.tsx

0
src/pages/db/movie/components/context.ts → src/pages/db/vod/components/context.ts

0
src/pages/db/movie/components/form/PrimaryFacts.tsx → src/pages/db/vod/components/form/PrimaryFacts.tsx

0
src/pages/db/movie/components/style.ts → src/pages/db/vod/components/style.ts

288
src/pages/db/movie/index.tsx → src/pages/db/vod/index.tsx

@ -2,10 +2,10 @@ import { t, useTranslation } from '@/i18n.ts'
import { Button, Form, Popconfirm, Divider, Space, Tooltip, Badge, Layout, Menu, Spin, Select } from 'antd' import { Button, Form, Popconfirm, Divider, Space, Tooltip, Badge, Layout, Menu, Spin, Select } from 'antd'
import { useAtom, useAtomValue } from 'jotai' import { useAtom, useAtomValue } from 'jotai'
import { import {
deleteMovieAtom,
saveOrUpdateMovieAtom, movieAtom, moviesAtom, movieSearchAtom,
} from '@/store/db/movie'
import React, { useEffect, useMemo, useState } from 'react'
deleteVodAtom,
saveOrUpdateVodAtom, vodAtom, vodsAtom, vodSearchAtom,
} from '@/store/db/vod'
import { useEffect, useMemo, useState } from 'react'
import Action from '@/components/action/Action.tsx' import Action from '@/components/action/Action.tsx'
import { import {
BetaSchemaForm, BetaSchemaForm,
@ -15,12 +15,14 @@ import {
import ListPageLayout from '@/layout/ListPageLayout.tsx' import ListPageLayout from '@/layout/ListPageLayout.tsx'
import { useStyle } from './style' import { useStyle } from './style'
import { ExportOutlined, FilterOutlined } from '@ant-design/icons' import { ExportOutlined, FilterOutlined } from '@ant-design/icons'
import { getValueCount } from '@/utils'
import { genProTableColumnWidthProps, getValueCount } from '@/utils'
import { Table as ProTable } from '@/components/table' import { Table as ProTable } from '@/components/table'
import InteractPopup from '@/components/interact-popup' import InteractPopup from '@/components/interact-popup'
import Edit from './components/Edit.tsx' import Edit from './components/Edit.tsx'
import dayjs from 'dayjs'
import Switch from '@/components/switch'
const i18nPrefix = 'movies.list'
const i18nPrefix = 'vod.list'
const SwitchLanguage = () => { const SwitchLanguage = () => {
@ -52,17 +54,17 @@ const SwitchLanguage = () => {
) )
} }
const Movie = () => {
const Vod = () => {
const { styles, cx } = useStyle() const { styles, cx } = useStyle()
const { t } = useTranslation() const { t } = useTranslation()
const [ form ] = Form.useForm() const [ form ] = Form.useForm()
const [ filterForm ] = Form.useForm() const [ filterForm ] = Form.useForm()
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateMovieAtom)
const [ search, setSearch ] = useAtom(movieSearchAtom)
const [ currentMovie, setMovie ] = useAtom(movieAtom)
const { data, isFetching, isLoading, refetch } = useAtomValue(moviesAtom)
const { mutate: deleteMovie, isPending: isDeleting } = useAtomValue(deleteMovieAtom)
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateVodAtom)
const [ search, setSearch ] = useAtom(vodSearchAtom)
const [ currentVod, setVod ] = useAtom(vodAtom)
const { data, isFetching, isLoading, refetch } = useAtomValue(vodsAtom)
const { mutate: deleteVod, isPending: isDeleting } = useAtomValue(deleteVodAtom)
const [ open, setOpen ] = useState(false) const [ open, setOpen ] = useState(false)
const [ openFilter, setFilterOpen ] = useState(false) const [ openFilter, setFilterOpen ] = useState(false)
@ -78,21 +80,258 @@ const Movie = () => {
formItemProps: { hidden: true } formItemProps: { hidden: true }
}, },
{ {
title: t(`${i18nPrefix}.columns.name`, 'name'),
dataIndex: 'name',
title: t(`${i18nPrefix}.columns.title`, '标题'),
dataIndex: 'title',
fixed: 'left',
...genProTableColumnWidthProps(250),
}, },
{ {
title: t(`${i18nPrefix}.columns.description`, 'description'),
dataIndex: 'description',
title: t(`${i18nPrefix}.columns.title_sub`, '副标'),
dataIndex: 'title_sub',
ellipsis: true,
...genProTableColumnWidthProps(300),
}, },
{ {
title: t(`${i18nPrefix}.columns.updated_at`, 'updated_at'),
dataIndex: 'updated_at',
title: t(`${i18nPrefix}.columns.letter`, '首字母'),
dataIndex: 'letter',
...genProTableColumnWidthProps(80),
}, },
{ {
title: t(`${i18nPrefix}.columns.tag`, 'TAG'),
dataIndex: 'tag',
...genProTableColumnWidthProps(120),
},
{
title: t(`${i18nPrefix}.columns.status`, '状态'),
dataIndex: 'status',
valueType: 'switch',
align: 'center',
...genProTableColumnWidthProps(80),
render: (_, record) => {
return <Switch value={record.status} size={'small'}/>
}
},
{
title: t(`${i18nPrefix}.columns.category`, '分类'),
dataIndex: 'category',
...genProTableColumnWidthProps(120),
},
{
title: t(`${i18nPrefix}.columns.pic`, '图片'),
dataIndex: 'pic',
render: (_, record) => {
return <img alt='' src={record.pic} style={{ height: 40}} />
}
},
{
title: t(`${i18nPrefix}.columns.role`, '人员角色'),
dataIndex: 'role',
hideInTable: true,
},
{
title: t(`${i18nPrefix}.columns.remarks`, '备注'),
dataIndex: 'remarks',
ellipsis: true,
...genProTableColumnWidthProps(200),
},
{
title: t(`${i18nPrefix}.columns.pubdate`, '发布时间'),
dataIndex: 'pubdate',
valueType: 'dateTime',
...genProTableColumnWidthProps(120),
render: (_, record) => {
if (!record.pubdate){
return null
}
return dayjs(record.pubdate).format('YYYY-MM-DD HH:mm:ss')
}
},
{
title: t(`${i18nPrefix}.columns.total`, '总集数'),
dataIndex: 'total',
...genProTableColumnWidthProps(80),
},
{
title: t(`${i18nPrefix}.columns.serial`, '连载数'),
dataIndex: 'serial',
...genProTableColumnWidthProps(80),
},
{
title: t(`${i18nPrefix}.columns.duration`, '视频时长'),
dataIndex: 'duration',
...genProTableColumnWidthProps(120),
},
{
title: t(`${i18nPrefix}.columns.area`, '地区'),
dataIndex: 'area',
...genProTableColumnWidthProps(120),
},
{
title: t(`${i18nPrefix}.columns.lang`, '语言'),
dataIndex: 'lang',
...genProTableColumnWidthProps(120),
},
{
title: t(`${i18nPrefix}.columns.source_url`, '源站点地址'),
dataIndex: 'source_url',
hideInTable: true,
hideInSearch: true,
},
{
title: t(`${i18nPrefix}.columns.year`, '年份'),
dataIndex: 'year',
...genProTableColumnWidthProps(120),
},
{
title: t(`${i18nPrefix}.columns.douban_score`, '豆瓣评分'),
dataIndex: 'douban_score',
...genProTableColumnWidthProps(120),
},
{
title: t(`${i18nPrefix}.columns.douban_id`, '豆瓣ID'),
dataIndex: 'douban_id',
hideInTable: true,
},
{
title: t(`${i18nPrefix}.columns.imdb_score`, 'imdb评分'),
dataIndex: 'imdb_score',
...genProTableColumnWidthProps(120),
},
{
title: t(`${i18nPrefix}.columns.imdb_id`, 'imdbID'),
dataIndex: 'imdb_id',
hideInTable: true,
},
{
title: t(`${i18nPrefix}.columns.content`, '内容'),
dataIndex: 'content',
ellipsis: {
},
...genProTableColumnWidthProps(200),
},
{
title: t(`${i18nPrefix}.columns.videostatus`, '视频'),
dataIndex: 'videostatus',
...genProTableColumnWidthProps(80),
align: 'center',
render: (_, record) => {
return <Switch value={record.videostatus} size={'small'}/>
}
},
{
title: t(`${i18nPrefix}.columns.adultstatus`, '成人电影'),
dataIndex: 'adultstatus',
align: 'center',
...genProTableColumnWidthProps(80),
render: (_, record) => {
return <Switch value={record.adultstatus} size={'small'}/>
}
},
{
title: t(`${i18nPrefix}.columns.typestatus`, '系列'),
dataIndex: 'typestatus',
align: 'center',
...genProTableColumnWidthProps(80),
render: (_, record) => {
return <Switch value={record.typestatus} size={'small'}/>
}
},
{
title: t(`${i18nPrefix}.columns.homepage`, '主页'),
dataIndex: 'homepage',
ellipsis: true,
...genProTableColumnWidthProps(120),
render: (_, record) => {
return <a href={record.homepage} target={'_blank'}>{record.homepage}</a>
}
},
{
title: t(`${i18nPrefix}.columns.budgetstatus`, '预算'),
dataIndex: 'budgetstatus',
...genProTableColumnWidthProps(120),
},
{
title: t(`${i18nPrefix}.columns.revenuestatus`, '票房'),
dataIndex: 'revenuestatus',
...genProTableColumnWidthProps(120),
},
{
title: t(`${i18nPrefix}.columns.facebook_id`, 'facebook_id'),
dataIndex: 'facebook_id',
hideInTable: true,
hideInSearch: true,
},
{
title: t(`${i18nPrefix}.columns.instagram_id`, 'instagram_id'),
dataIndex: 'instagram_id',
hideInTable: true,
hideInSearch: true,
},
{
title: t(`${i18nPrefix}.columns.twitter_id`, 'twitter_id'),
dataIndex: 'twitter_id',
hideInTable: true,
hideInSearch: true,
},
{
title: t(`${i18nPrefix}.columns.wikidata_id`, 'wikidata_id'),
dataIndex: 'wikidata_id',
hideInTable: true,
hideInSearch: true,
},
{
title: t(`${i18nPrefix}.columns.freebase_id`, 'freebase_id'),
dataIndex: 'freebase_id',
hideInTable: true,
hideInSearch: true,
},
{
title: t(`${i18nPrefix}.columns.tv_rage_id`, 'tv_rage_id'),
dataIndex: 'tv_rage_id',
hideInTable: true,
hideInSearch: true,
},
{
title: t(`${i18nPrefix}.columns.ver`, '版本'),
dataIndex: 'ver',
...genProTableColumnWidthProps(120),
},
{
title: t(`${i18nPrefix}.columns.option`, '操作'), title: t(`${i18nPrefix}.columns.option`, '操作'),
key: 'option', key: 'option',
valueType: 'option', valueType: 'option',
@ -102,14 +341,13 @@ const Movie = () => {
as={'a'} as={'a'}
onClick={() => { onClick={() => {
form.setFieldsValue(record) form.setFieldsValue(record)
setMovie(record)
setOpen(true) setOpen(true)
}}>{t('actions.edit')}</Action>, }}>{t('actions.edit')}</Action>,
<Popconfirm <Popconfirm
key={'del_confirm'} key={'del_confirm'}
disabled={isDeleting} disabled={isDeleting}
onConfirm={() => { onConfirm={() => {
deleteMovie([ record.id ])
deleteVod([ record.id ])
}} }}
title={t('message.deleteConfirm')}> title={t('message.deleteConfirm')}>
<a key="del"> <a key="del">
@ -119,7 +357,7 @@ const Movie = () => {
] ]
} }
] as ProColumns[] ] as ProColumns[]
}, [ isDeleting, currentMovie, search ])
}, [ isDeleting, currentVod, search ])
useEffect(() => { useEffect(() => {
@ -178,7 +416,7 @@ const Movie = () => {
] ]
}} }}
scroll={{ scroll={{
x: 2500, y: 'calc(100vh - 290px)'
x: 3500, y: 'calc(100vh - 290px)'
}} }}
search={false} search={false}
onRow={(record) => { onRow={(record) => {
@ -187,7 +425,7 @@ const Movie = () => {
// 'ant-table-row-selected': currentMovie?.id === record.id // 'ant-table-row-selected': currentMovie?.id === record.id
}), }),
onClick: () => { onClick: () => {
setMovie(record)
setVod(record)
} }
} }
}} }}
@ -239,7 +477,7 @@ const Movie = () => {
extra: <SwitchLanguage />, extra: <SwitchLanguage />,
}} }}
> >
<Edit record={currentMovie} form={form}/>
<Edit record={currentVod} form={form}/>
</InteractPopup> </InteractPopup>
@ -307,4 +545,4 @@ const Movie = () => {
) )
} }
export default Movie
export default Vod

2
src/pages/db/movie/style.ts → src/pages/db/vod/style.ts

@ -1,7 +1,7 @@
import { createStyles } from '@/theme' import { createStyles } from '@/theme'
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => {
const prefix = `${prefixCls}-${token?.proPrefix}-movie-list-page`
const prefix = `${prefixCls}-${token?.proPrefix}-vod-list-page`
const container = css` const container = css`
.ant-table-cell { .ant-table-cell {

86
src/pages/x-form/hooks/useApi.tsx

@ -0,0 +1,86 @@
import { useNavigate } from '@tanstack/react-router'
import { useAtom } from 'jotai/index'
import { apiAtom } from '@/store/x-form/model.ts'
import { useEffect, useRef, useState } from 'react'
import { Input, message, Modal } from 'antd'
import { Route } from '@/pages/x-form'
export const useApi = () => {
const nav = useNavigate()
const [ api, setApi ] = useAtom(apiAtom)
const { api: apiParam } = Route.useSearch()
const [ innerApi, setInnerApi ] = useState('')
const [ open, setOpen ] = useState(false)
const apiRef = useRef<string>()
useEffect(() => {
if (!apiParam && api) {
apiRef.current = api
nav({
to: '/x-form',
search: {
api
}
})
return
}
if (apiParam && !api) {
apiRef.current = apiParam
setApi(apiParam)
return
}
//延时弹出
setTimeout(() => {
if (!apiRef.current) {
setOpen(true)
}
}, 2000)
}, [ api, apiParam ])
const holderElement = (
<>
<Modal
title={'请指定 api 参数'}
closable={false}
open={open}
maskClosable={false}
afterOpenChange={setOpen}
onCancel={() => {
setOpen(false)
}}
onOk={() => {
if (!innerApi) {
message.destroy()
message.error('请填写 api 参数')
return
}
setOpen(false)
setApi(innerApi)
nav({
to: '/x-form',
search: {
api: innerApi
}
})
}}
>
<Input value={innerApi} onChange={e => {
setInnerApi(e.target.value)
}}/>
</Modal>
</>
)
return {
holderElement,
updateApi: setOpen,
setApi,
api,
} as const
}

301
src/pages/x-form/index.tsx

@ -0,0 +1,301 @@
import { createFileRoute } from '@tanstack/react-router'
import ListPageLayout from '@/layout/ListPageLayout.tsx'
import { useApi } from './hooks/useApi.tsx'
import { Badge, Button, Divider, Form, Popconfirm, Space, Tabs, Tag, Tooltip } from 'antd'
import { EditOutlined, FilterOutlined } from '@ant-design/icons'
import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components'
import { useEffect, useMemo, useState } from 'react'
import { useAtomValue } from 'jotai'
import {
deleteModelAtom, modelAtom,
modelCURDAtom,
modelsAtom,
modelSearchAtom,
saveOrUpdateModelAtom
} from '@/store/x-form/model.ts'
import { useAtom } from 'jotai/index'
import { getValueCount, unSetColumnRules } from '@/utils'
import { useStyle } from './style.ts'
import Action from '@/components/action/Action.tsx'
const XForm = () => {
const { styles, cx } = useStyle()
const { holderElement, updateApi, api } = useApi()
const [ form ] = Form.useForm()
const [ filterForm ] = Form.useForm()
const [ model, setModel ] = useAtom(modelAtom)
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateModelAtom)
const [ search, setSearch ] = useAtom(modelSearchAtom)
const { data, isFetching, isLoading, refetch } = useAtomValue(modelsAtom)
const { mutate: deleteModel, isPending: isDeleting } = useAtomValue(deleteModelAtom)
const { data: curdModal, isLoading: curdLoading } = useAtomValue(modelCURDAtom)
const [ open, setOpen ] = useState(false)
const [ openFilter, setFilterOpen ] = useState(false)
const [ searchKey, setSearchKey ] = useState(search?.key)
const columns = useMemo(() => {
return (curdModal?.column?.map((item) => {
return {
title: item.label,
dataIndex: item.prop,
key: item.prop,
valueType: item.type,
hideInSearch: !item.search,
hideInTable: item.hide,
} as ProColumns<any>
}) || []).concat([
{
title: 'ID',
dataIndex: 'id',
hideInTable: true,
hideInSearch: true,
formItemProps: { hidden: true }
},
{
title: '操作',
key: 'option',
valueType: 'option',
fixed: 'right',
render: (_, record) => [
<Action key="edit"
as={'a'}
onClick={() => {
form.setFieldsValue(record)
setOpen(true)
}}>{'编辑'}</Action>,
<Popconfirm
key={'del_confirm'}
disabled={isDeleting}
onConfirm={() => {
deleteModel([ record.id ])
}}
title={'确定要删除吗?'}>
<a key="del">
</a>
</Popconfirm>
]
}
])
}, [ curdModal?.column, deleteModel, form, isDeleting, setOpen ])
useEffect(() => {
setSearchKey(search?.key)
filterForm.setFieldsValue(search)
}, [ search ])
useEffect(() => {
if (isSuccess) {
setOpen(false)
}
}, [ isSuccess ])
return (
<>
{holderElement}
<ListPageLayout
className={styles.container}
title={<>
<Tag color={'green'} style={{ marginBlockEnd: 0 }}>API</Tag>
<span>{api} <EditOutlined
style={{ color: '#666', cursor: 'pointer', fontSize: 16 }}
onClick={() => {
updateApi(true)
}}/></span>
</>}>
<ProTable
rowKey="id"
headerTitle={api}
toolbar={{
search: {
loading: isFetching && !!search?.key,
onSearch: (value: string) => {
setSearch(prev => ({
...prev,
title: value
}))
},
allowClear: true,
onChange: (e) => {
setSearchKey(e.target?.value)
},
value: searchKey,
placeholder: '输入关键字搜索',
},
actions: [
<Tooltip key={'filter'} title={'高级查询'}>
<Badge count={getValueCount(search)}>
<Button
onClick={() => {
setFilterOpen(true)
}}
icon={<FilterOutlined/>} shape={'circle'} size={'small'}/>
</Badge>
</Tooltip>,
<Divider type={'vertical'} key={'divider'}/>,
<Button key={'add'}
onClick={() => {
form.resetFields()
form.setFieldsValue({
id: 0,
})
setOpen(true)
}}
type={'primary'}>{'添加'}</Button>
]
}}
scroll={{
// x: 3500,
y: 'calc(100vh - 290px)'
}}
search={false}
onRow={(record) => {
return {
className: cx({
// 'ant-table-row-selected': currentMovie?.id === record.id
}),
onClick: () => {
setModel(record)
}
}
}}
dateFormatter="string"
loading={isLoading || isFetching || curdLoading}
dataSource={data?.rows ?? []}
columns={columns}
options={{
reload: () => {
refetch()
},
}}
pagination={{
total: data?.total,
pageSize: search.pageSize,
current: search.page,
onShowSizeChange: (current: number, size: number) => {
setSearch({
...search,
pageSize: size,
page: current
})
},
onChange: (current, pageSize) => {
setSearch(prev => {
return {
...prev,
page: current,
pageSize: pageSize,
}
})
},
}}
/>
<BetaSchemaForm
grid={true}
shouldUpdate={false}
width={1000}
form={form}
layout={'vertical'}
scrollToFirstError={true}
title={form.getFieldValue('id') !== 0 ? '编辑' : '添加'}
layoutType={'DrawerForm'}
open={open}
drawerProps={{
maskClosable: false,
}}
onOpenChange={(open) => {
setOpen(open)
}}
loading={isSubmitting}
onFinish={async (values) => {
saveOrUpdate(values)
}}
columns={columns as ProFormColumnsType[]}/>
<BetaSchemaForm
title={'高级查询'}
grid={true}
shouldUpdate={false}
width={500}
form={filterForm}
open={openFilter}
onOpenChange={open => {
setFilterOpen(open)
}}
layout={'vertical'}
scrollToFirstError={true}
layoutType={'DrawerForm'}
drawerProps={{
maskClosable: false,
mask: false,
}}
submitter={{
searchConfig: {
resetText: '清空',
submitText: '查询',
},
onReset: () => {
filterForm.resetFields()
},
render: (props,) => {
return (
<div style={{ textAlign: 'right' }}>
<Space>
<Button onClick={() => {
props.reset()
}}>{props.searchConfig?.resetText}</Button>
<Button type="primary"
onClick={() => {
props.submit()
}}
>{props.searchConfig?.submitText}</Button>
</Space>
</div>
)
},
}}
onFinish={async (values) => {
//处理,变成数组
Object.keys(values).forEach(key => {
if (typeof values[key] === 'string' && values[key].includes(',')) {
values[key] = values[key].split(',')
}
})
setSearch(values)
}}
columns={unSetColumnRules(columns.filter(item => !item.hideInSearch) as ProFormColumnsType[])}/>
</ListPageLayout>
</>
)
}
type XFormRouteSearch = {
api: string
}
// @ts-ignore fix route id
export const Route = createFileRoute('x-form')({
validateSearch: (search: Record<string, unknown>): XFormRouteSearch => {
// validate and parse the search params into a typed state
// console.log(search.id)
return {
api: (search.api ?? '') as string
} as XFormRouteSearch
},
})
export default XForm

14
src/pages/x-form/style.ts

@ -0,0 +1,14 @@
import { createStyles } from '@/theme'
export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => {
const prefix = `${prefixCls}-${token?.proPrefix}-x-form-page`
const container = css`
`
return {
container: cx(prefix, props?.className, container),
}
})

32
src/request-base-url-interceptors.ts

@ -0,0 +1,32 @@
import { AxiosInstance } from 'axios'
const baseURLMap = {
package: 'http://154.88.7.8:45321/api/v1',
movie: 'http://47.113.117.106:10000/api/v1',
default: 'http://127.0.0.1:8686/api/v1',
}
/**
* urlbaseURL
* @param axiosInstance
*/
export const requestBaseUrlInterceptors = (axiosInstance: AxiosInstance) => {
//拦截url,适应不同的baseURL
axiosInstance.interceptors.request.use((config) => {
const { url } = config
//取url的第1个/后的字符串
const key = url?.split('/')[1]
const baseURL = baseURLMap[key!]
if (baseURL) {
config.baseURL = baseURL
} else {
config.baseURL = baseURLMap['default']
}
return config
}, (error) => {
// console.log('error', error)
return Promise.reject(error)
})
}

254
src/request.ts

@ -3,170 +3,176 @@ import { IApiResult } from '@/global'
import { Record } from '@icon-park/react' import { Record } from '@icon-park/react'
import { message } from 'antd' import { message } from 'antd'
import axios, { import axios, {
AxiosRequestConfig,
AxiosInstance, AxiosResponse,
AxiosRequestConfig,
AxiosInstance, AxiosResponse,
} from 'axios' } from 'axios'
export type { AxiosRequestConfig } export type { AxiosRequestConfig }
type FetchMethod = <T = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>) => Promise<IApiResult<T>> type FetchMethod = <T = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>) => Promise<IApiResult<T>>
interface RequestMethods extends Pick<AxiosInstance, 'get' | 'post' | 'put' | 'delete' | 'request' | 'postForm' | 'patch' | 'patchForm' | 'putForm' | 'options' > {
download: (url: string, data?: any) => Promise<BlobPart>
interface RequestMethods extends Pick<AxiosInstance, 'get' | 'post' | 'put' | 'delete' | 'request' | 'postForm' | 'patch' | 'patchForm' | 'putForm' | 'options'> {
download: (url: string, data?: any) => Promise<BlobPart>
} }
const axiosInstance = axios.create({ const axiosInstance = axios.create({
baseURL: '/api/v1',
// timeout: 1000,
headers: {
'Content-Type': 'application/json',
},
baseURL: '/api/v1',
// timeout: 1000,
headers: {
'Content-Type': 'application/json',
},
validateStatus: status => {
return status >= 200 && status < 300
}
}) })
//拦截request,添加token //拦截request,添加token
axiosInstance.interceptors.request.use((config) => { axiosInstance.interceptors.request.use((config) => {
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
const token = getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
return config
}, (error) => { }, (error) => {
console.log('error', error)
return Promise.reject(error)
console.log('error', error)
return Promise.reject(error)
}) })
//拦截response,返回data //拦截response,返回data
axiosInstance.interceptors.response.use( axiosInstance.interceptors.response.use(
(response) => { (response) => {
// console.log('response', response.data)
message.destroy()
const result = response.data as IApiResult
switch (result.code) {
case 200:
//login
if (response.config.url?.includes('/sys/login')) {
setToken(result.data.token)
const search = new URLSearchParams(window.location.search)
// eslint-disable-next-line no-case-declarations
const redirect = search.get('redirect')
if (redirect) {
window.location.href = redirect
}
}
return response
case 401:
setToken('')
if (window.location.pathname === '/login') {
return Promise.reject(new Error('to login'))
}
// 401: 未登录
message.error('登录失败,跳转重新登录')
// eslint-disable-next-line no-case-declarations
const search = new URLSearchParams(window.location.search)
// eslint-disable-next-line no-case-declarations
let redirect = window.location.pathname
if (search.toString() !== '') {
redirect = window.location.pathname + '?=' + search.toString()
}
window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`
return Promise.reject(new Error('to login'))
default:
message.error(result.message ?? '请求失败')
return Promise.reject(response)
}
// console.log('response', response.data)
message.destroy()
const result = response.data as IApiResult
switch (result.code) {
case 0:
return response
case 200:
//login
if (response.config.url?.includes('/sys/login')) {
setToken(result.data.token)
const search = new URLSearchParams(window.location.search)
// eslint-disable-next-line no-case-declarations
const redirect = search.get('redirect')
if (redirect) {
window.location.href = redirect
}
}
return response
case 401:
setToken('')
if (window.location.pathname === '/login') {
return Promise.reject(new Error('to login'))
}
// 401: 未登录
message.error('登录失败,跳转重新登录')
// eslint-disable-next-line no-case-declarations
const search = new URLSearchParams(window.location.search)
// eslint-disable-next-line no-case-declarations
let redirect = window.location.pathname
if (search.toString() !== '') {
redirect = window.location.pathname + '?=' + search.toString()
}
window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`
return Promise.reject(new Error('to login'))
default:
message.error(result.message ?? '请求失败')
return Promise.reject(response)
}
}, (error) => { }, (error) => {
// console.log('error', error)
message.destroy()
const { response } = error
if (response) {
switch (response.status) {
case 401:
if (window.location.pathname === '/login') {
return
}
setToken('')
// 401: 未登录
message.error('登录失败,跳转重新登录')
// eslint-disable-next-line no-case-declarations
const search = new URLSearchParams(window.location.search)
// eslint-disable-next-line no-case-declarations
let redirect = window.location.pathname
if (search.toString() !== '') {
redirect = window.location.pathname + '?=' + search.toString()
}
window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`
return
case 403:
message.error('没有权限')
break
case 404:
message.error('请求的资源不存在')
break
default:
message.error(response.data.message ?? response.data ?? error.message ?? '请求失败')
return Promise.reject(response)
// console.log('error', error)
message.destroy()
const { response } = error
if (response) {
switch (response.status) {
case 401:
if (window.location.pathname === '/login') {
return
} }
setToken('')
// 401: 未登录
message.error('登录失败,跳转重新登录')
// eslint-disable-next-line no-case-declarations
const search = new URLSearchParams(window.location.search)
// eslint-disable-next-line no-case-declarations
let redirect = window.location.pathname
if (search.toString() !== '') {
redirect = window.location.pathname + '?=' + search.toString()
}
window.location.href = `/login?redirect=${encodeURIComponent(redirect)}`
return
case 403:
message.error('没有权限')
break
case 404:
message.error('请求的资源不存在')
break
default:
message.error(response.data.message ?? response.data ?? error.message ?? '请求失败')
return Promise.reject(response)
} }
}
return Promise.reject(error)
return Promise.reject(error)
}) })
//扩展download方法 //扩展download方法
// @ts-ignore fix download // @ts-ignore fix download
axiosInstance.download = (url: string, data?: any) => { axiosInstance.download = (url: string, data?: any) => {
const formData = new FormData()
for (const key in data) {
formData.append(key, data[key])
}
const config = {
method: 'post',
url,
data: formData,
responseType: 'blob',
timeout: 40 * 1000,
} as AxiosRequestConfig
return axiosInstance.request(config)
const formData = new FormData()
for (const key in data) {
formData.append(key, data[key])
}
const config = {
method: 'post',
url,
data: formData,
responseType: 'blob',
timeout: 40 * 1000,
} as AxiosRequestConfig
return axiosInstance.request(config)
} }
//创建返回IApiResult类型的request //创建返回IApiResult类型的request
export const createFetchMethods = () => { export const createFetchMethods = () => {
const methods = {}
for (const method of Object.keys(axiosInstance)) {
methods[method] = async <T = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>) => {
config = config ?? {}
config.url = url
config.method = method
const isGet = method === 'get'
if (isGet) {
config.params = data
} else {
config.data = data
}
return axiosInstance(config)
.then((response: AxiosResponse<IApiResult<T>>) => {
if (response.data.code !== 200) {
throw new Error(response.data.message)
}
return response.data as IApiResult<T>
})
.catch((err) => {
throw err
})
}
const methods = {}
for (const method of Object.keys(axiosInstance)) {
methods[method] = async <T = any, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>) => {
config = config ?? {}
config.url = url
config.method = method
const isGet = method === 'get'
if (isGet) {
config.params = data
} else {
config.data = data
}
return axiosInstance(config)
.then((response: AxiosResponse<IApiResult<T>>) => {
if (response.data.code !== 200 && response.data.code !== 0) {
throw new Error(response.data.message)
}
return response.data as IApiResult<T>
})
.catch((err) => {
throw err
})
} }
}
return methods as Record<keyof RequestMethods, FetchMethod>
return methods as Record<keyof RequestMethods, FetchMethod>
} }
export const request = createFetchMethods() export const request = createFetchMethods()

4
src/service/base.ts

@ -1,5 +1,6 @@
import { request, AxiosRequestConfig } from '@/request.ts' import { request, AxiosRequestConfig } from '@/request.ts'
import { IApiResult, IPage, IPageResult } from '@/global' import { IApiResult, IPage, IPageResult } from '@/global'
import { XForm } from '@/types/x-form/model'
export const createCURD = <TParams, TResult>(api: string, options?: AxiosRequestConfig) => { export const createCURD = <TParams, TResult>(api: string, options?: AxiosRequestConfig) => {
@ -22,6 +23,9 @@ export const createCURD = <TParams, TResult>(api: string, options?: AxiosRequest
}, },
info: (id: number) => { info: (id: number) => {
return request.post<TResult>(`${api}/get`, { id }, options) return request.post<TResult>(`${api}/get`, { id }, options)
},
curd: async (params: any) => {
return request.post<XForm.IModelCURD>(`${api}/ui/curd`, { ...params })
} }
} }

13
src/service/db/vod.ts

@ -0,0 +1,13 @@
import { createCURD } from '@/service/base.ts'
import { DB } from '@/types/db/vod'
import { IPageResult } from '@/global'
import request from '@/request.ts'
const vod = {
...createCURD<any, DB.IVod>('/movie/vod'),
list: (params: any) => {
return request.post<IPageResult<DB.IVod>>(`/movie/vod/get_vod_list`, { ...params })
}
}
export default vod

10
src/service/x-form/model.ts

@ -0,0 +1,10 @@
import { createCURD } from '@/service/base.ts'
import { XForm } from '@/types/x-form/model'
const model = (api: string) => {
return {
...createCURD<any, XForm.IModel>(api),
}
}
export { model }

90
src/store/db/vod.ts

@ -0,0 +1,90 @@
import { atom } from 'jotai'
import { IApiResult, IPage } from '@/global'
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'
import { message } from 'antd'
import { t } from 'i18next'
import { DB } from '@/types/db/vod'
import dBServ from '@/service/db/vod'
type SearchParams = IPage & {
key?: string
[key: string]: any
}
export const vodIdAtom = atom(0)
export const vodIdsAtom = atom<number[]>([])
export const vodAtom = atom<DB.IVod>(undefined as unknown as DB.IVod )
export const vodSearchAtom = atom<SearchParams>({
key: '',
pageSize: 10,
page: 1,
} as SearchParams)
export const vodPageAtom = atom<IPage>({
pageSize: 10,
page: 1,
})
export const vodsAtom = atomWithQuery((get) => {
return {
queryKey: [ 'vods', get(vodSearchAtom) ],
queryFn: async ({ queryKey: [ , params ] }) => {
return await dBServ.list(params as SearchParams)
},
select: res => {
const data = res.data
data.rows = data.rows?.map(row => {
return {
...row,
//status: convertToBool(row.status)
}
})
return data
}
}
})
//saveOrUpdateAtom
export const saveOrUpdateVodAtom = atomWithMutation<IApiResult, DB.IVod>((get) => {
return {
mutationKey: [ 'updateVod' ],
mutationFn: async (data) => {
//data.status = data.status ? '1' : '0'
if (data.id === 0) {
return await dBServ.add(data)
}
return await dBServ.update(data)
},
onSuccess: (res) => {
const isAdd = !!res.data?.id
message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功'))
//更新列表
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore fix
get(queryClientAtom).invalidateQueries({ queryKey: [ 'vods', get(vodSearchAtom) ] })
return res
}
}
})
export const deleteVodAtom = atomWithMutation((get) => {
return {
mutationKey: [ 'deleteVod' ],
mutationFn: async (ids: number[]) => {
return await dBServ.batchDelete(ids ?? get(vodIdsAtom))
},
onSuccess: (res) => {
message.success('message.deleteSuccess')
//更新列表
get(queryClientAtom).invalidateQueries({ queryKey: [ 'vods', get(vodSearchAtom) ] })
return res
}
}
})

124
src/store/x-form/model.ts

@ -0,0 +1,124 @@
import { atom } from 'jotai'
import { IApiResult, IError, IPage } from '@/global'
import { atomWithMutation, atomWithQuery, queryClientAtom } from 'jotai-tanstack-query'
import { message } from 'antd'
import { t } from 'i18next'
import { XForm } from '@/types/x-form/model'
import * as modelServ from '@/service/x-form/model'
import { atomWithStorage } from 'jotai/utils'
type SearchParams = IPage & {
key?: string
api: string
}
export const apiAtom = atomWithStorage('api', '')
export const modelIdAtom = atom(0)
export const modelIdsAtom = atom<number[]>([])
export const modelAtom = atom<XForm.IModel>(undefined as unknown as XForm.IModel)
export const modelSearchAtom = atom<SearchParams>({
key: '',
pageSize: 10,
page: 1,
} as SearchParams)
export const modelPageAtom = atom<IPage>({
pageSize: 10,
page: 1,
})
export const modelCURDAtom = atomWithQuery((get) => {
const api = get(apiAtom)
return {
enabled: !!api,
queryKey: [ 'modelCURD', get(modelSearchAtom) ],
queryFn: async ({ queryKey: [ , params ] }) => {
// if (!api) {
// return Promise.reject({
// code: 400,
// message: 'api 不能为空'
// })
// }
return await modelServ.model(api).curd(params as SearchParams)
},
select: res => {
return res.data.data
}
}
})
export const modelsAtom = atomWithQuery((get) => {
const api = get(apiAtom)
const curd = get(modelCURDAtom)
return {
enabled: curd.isSuccess && !!api,
queryKey: [ 'models', get(modelSearchAtom) ],
queryFn: async ({ queryKey: [ , params ] }) => {
return await modelServ.model(api).list(params as SearchParams)
},
select: res => {
const data = res.data
data.rows = data.rows?.map(row => {
return {
...row,
//status: convertToBool(row.status)
}
})
return data
}
}
})
//saveOrUpdateAtom
export const saveOrUpdateModelAtom = atomWithMutation<IApiResult, XForm.IModel>((get) => {
return {
mutationKey: [ 'updateModel' ],
mutationFn: async (data) => {
const api = get(apiAtom)
if (!api) {
return Promise.reject('api 不能为空')
}
if (data.id === 0) {
return await modelServ.model(api).add(data)
}
return await modelServ.model(api).update(data)
},
onSuccess: (res) => {
const isAdd = !!res.data?.id
message.success(t(isAdd ? 'message.saveSuccess' : 'message.editSuccess', '保存成功'))
//更新列表
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore fix
get(queryClientAtom).invalidateQueries({ queryKey: [ 'models', get(modelSearchAtom) ] })
return res
}
}
})
export const deleteModelAtom = atomWithMutation((get) => {
return {
mutationKey: [ 'deleteModel' ],
mutationFn: async (ids: number[]) => {
const api = get(apiAtom)
if (!api) {
return Promise.reject('api 不能为空')
}
return await modelServ.model(api).batchDelete(ids ?? get(modelIdsAtom))
},
onSuccess: (res) => {
message.success('message.deleteSuccess')
//更新列表
get(queryClientAtom).invalidateQueries({ queryKey: [ 'models', get(modelSearchAtom) ] })
return res
}
}
})

42
src/types/db/vod.d.ts

@ -0,0 +1,42 @@
export namespace DB {
export interface IVod {
id: number;
title: string;
title_sub: string;
letter: string;
tag: Json;
status: string;
category: string;
pic: string;
role: Json;
remarks: string;
pubdate: string;
total: number;
serial: string;
duration: string;
area: string;
lang: string;
source_url: string;
year: number;
douban_score: number;
douban_id: number;
imdb_score: number;
imdb_id: number;
content: string;
videostatus: number;
adultstatus: number;
typestatus: number;
homepage: string;
budgetstatus: string;
revenuestatus: string;
facebook_id: string;
instagram_id: string;
twitter_id: string;
wikidata_id: string;
freebase_id: string;
tv_rage_id: string;
ver: string;
created_at: string;
updated_at: string;
}
}

61
src/types/x-form/model.d.ts

@ -0,0 +1,61 @@
export namespace XForm {
export interface IModel {
id: number;
created_at: string;
updated_at: string;
[key: string]: any;
}
export interface IModelCURD {
height: string;
calcHeight: number;
dialogType: string;
column: IColumn[];
searchMenuSpan: number;
searchIndex: number;
searchIcon: boolean;
[key: string]: any;
}
export interface IColumn {
label: string;
prop: string;
search: boolean;
type: string;
span: number;
hide: boolean;
rules: IRules[];
value: any;
colorFormat: string;
showAlpha: any;
dicUrl: string;
props: IProps;
button: boolean;
multiple: boolean;
checkStrictly: boolean;
dicMethod: string;
[key: string]: any;
}
export interface IRules {
required: boolean;
message: string;
trigger: string;
[key: string]: any;
}
export interface IProps {
label: string;
value: string;
res: string;
[key: string]: any;
}
}

14
src/utils/index.ts

@ -183,3 +183,17 @@ export const unSetColumnRules = (columns: any[]) => {
return col return col
}) })
} }
export const getColumns = (columns: any[], key: string) => {
return columns.find(col => col.key === key)
}
//生成ProTableColumns的宽度相关属性
export const genProTableColumnWidthProps = (width: string | number) => {
return {
width,
fieldProps: {
style: { width: '100%' }
},
}
}

82
vite.config.ts

@ -10,42 +10,54 @@ import jotaiReactRefresh from 'jotai/babel/plugin-react-refresh'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
// 根据当前工作目录中的 `mode` 加载 .env 文件
// 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
// @ts-ignore fix process
const env = loadEnv(mode, process.cwd(), '')
return {
//定义别名的路径
resolve: {
alias: {
'@': '/src',
},
// 根据当前工作目录中的 `mode` 加载 .env 文件
// 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
// @ts-ignore fix process
const env = loadEnv(mode, process.cwd(), '')
return {
//定义别名的路径
resolve: {
alias: {
'@': '/src',
},
},
server: {
cors: {
origin: '*'
},
proxy: {
'/api/v1/movie': {
target: 'http://47.113.117.106:10000',
changeOrigin: true,
rewrite: (path) => {
return path
}
}, },
server: {
proxy: {
'/api': {
target: env.API_URL,
changeOrigin: true,
rewrite: (path) => path
}
}
'/api': {
target: env.API_URL,
changeOrigin: true,
rewrite: (path) => {
return path
}
}
},
},
plugins: [
react({
babel: {
presets: [ 'jotai/babel/preset' ],
plugins: [ jotaiDebugLabel, jotaiReactRefresh ]
}, },
plugins: [
react({
babel: {
presets: [ 'jotai/babel/preset' ],
plugins: [ jotaiDebugLabel, jotaiReactRefresh ]
},
}),
viteMockServe({
// 是否启用 mock 功能(默认值:process.env.NODE_ENV !== 'production')
enable: false,
}),
viteMockServe({
// 是否启用 mock 功能(默认值:process.env.NODE_ENV !== 'production')
enable: false,
// mock 文件的根路径,默认值:'mocks'
mockPath: 'mock',
logger: true,
}),
//TanStackRouterVite(),
],
}
// mock 文件的根路径,默认值:'mocks'
mockPath: 'mock',
logger: true,
}),
//TanStackRouterVite(),
],
}
}) })

777
yarn.lock
File diff suppressed because it is too large
View File

Loading…
Cancel
Save