28 changed files with 1893 additions and 71 deletions
			
			
		- 
					2src/components/switch/index.tsx
 - 
					96src/components/tag-pro/TagPro.tsx
 - 
					0src/components/tag-pro/index.ts
 - 
					2src/locales/lang/pages/cms/video/zh-CN.ts
 - 
					2src/locales/lang/pages/cms/videoCloud/zh-CN.ts
 - 
					2src/locales/lang/pages/cms/videoMagnet/zh-CN.ts
 - 
					53src/locales/lang/pages/videos/list/zh-CN.ts
 - 
					4src/locales/lang/zh-CN.ts
 - 
					67src/pages/cms/category/components/CategoryTree.tsx
 - 
					59src/pages/cms/category/components/TreeNodeRender.tsx
 - 
					191src/pages/cms/category/index.tsx
 - 
					81src/pages/cms/category/style.ts
 - 
					197src/pages/cms/video/index.tsx
 - 
					67src/pages/videos/category/components/CategoryTree.tsx
 - 
					59src/pages/videos/category/components/TreeNodeRender.tsx
 - 
					191src/pages/videos/category/index.tsx
 - 
					81src/pages/videos/category/style.ts
 - 
					391src/pages/videos/list/index.tsx
 - 
					2src/service/base.ts
 - 
					3src/service/cms.ts
 - 
					13src/service/videos.ts
 - 
					108src/store/cms/category.ts
 - 
					107src/store/videos/category.ts
 - 
					97src/store/videos/video.ts
 - 
					17src/types/cms/category.d.ts
 - 
					32src/types/index.d.ts
 - 
					17src/types/videos/category.d.ts
 - 
					23src/types/videos/video.d.ts
 
@ -0,0 +1,96 @@ | 
			
		|||||
 | 
				import { Tag, Input, Flex, Spin } from 'antd' | 
			
		||||
 | 
				import { useEffect, useState } from 'react' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export interface TagProProps { | 
			
		||||
 | 
				  tags?: any[], | 
			
		||||
 | 
				  onChange?: (value: any) => void | 
			
		||||
 | 
				  value?: any, | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  loading?: boolean, | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  [key: string]: any | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				function getSelectedByValue(value: string, tags?: any[]) { | 
			
		||||
 | 
				  if (!value || tags?.length === 0) return [] | 
			
		||||
 | 
				  const arr = String(value)?.split(',')?.filter(Boolean) | 
			
		||||
 | 
				  const newArr: string[] = [] | 
			
		||||
 | 
				  arr.forEach(item => { | 
			
		||||
 | 
				    if (tags?.includes(item)) { | 
			
		||||
 | 
				      newArr.push(item) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  }) | 
			
		||||
 | 
				  return newArr | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				const TagPro = ({ tags = [], loading, value, onChange, ...props }: TagProProps) => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const [ innerValue, setValue ] = useState(() => value) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const [ selectedTags, setSelectedTags ] = useState(() => { | 
			
		||||
 | 
				    if (value) { | 
			
		||||
 | 
				      return getSelectedByValue(value as string, tags) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				    return [] | 
			
		||||
 | 
				  }) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  useEffect(() => { | 
			
		||||
 | 
				    setValue(value) | 
			
		||||
 | 
				    setSelectedTags(getSelectedByValue(value as string, tags)) | 
			
		||||
 | 
				  }, [ value, JSON.stringify(tags) ]) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const handleChange = (checked: boolean, tag: string) => { | 
			
		||||
 | 
				    let nextTags: string[] = [] | 
			
		||||
 | 
				    if (checked) { | 
			
		||||
 | 
				      nextTags = [ ...selectedTags, tag ] | 
			
		||||
 | 
				    } else { | 
			
		||||
 | 
				      nextTags = selectedTags.filter((t) => t !== tag) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    const arr = (innerValue as string)?.split(',') | 
			
		||||
 | 
				    //arr中可能有不在tags中的值,不能丢弃
 | 
			
		||||
 | 
				    const newArr = arr?.filter((item) => { | 
			
		||||
 | 
				      return !tags?.includes(item) | 
			
		||||
 | 
				    }) || [] | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    const val = [ ...newArr, ...nextTags ].filter(Boolean) | 
			
		||||
 | 
				    //去重
 | 
			
		||||
 | 
				    onChange?.([ ...new Set(val) ].join(',')) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    setSelectedTags(nextTags) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				  return ( | 
			
		||||
 | 
				          <Spin spinning={loading} size={'small'}> | 
			
		||||
 | 
				            <Flex flex={1} gap={3}> | 
			
		||||
 | 
				              <Flex flex={2}> | 
			
		||||
 | 
				                <Input.TextArea | 
			
		||||
 | 
				                        {...props} | 
			
		||||
 | 
				                        value={innerValue} | 
			
		||||
 | 
				                        onChange={e => { | 
			
		||||
 | 
				                          setValue(e.target.value) | 
			
		||||
 | 
				                          onChange?.(e.target.value) | 
			
		||||
 | 
				                        }}/></Flex> | 
			
		||||
 | 
				              <Flex flex={1} wrap="wrap" align="flex-start"> | 
			
		||||
 | 
				                { | 
			
		||||
 | 
				                  tags?.map(item => { | 
			
		||||
 | 
				                    return <Tag.CheckableTag | 
			
		||||
 | 
				                            key={item} | 
			
		||||
 | 
				                            checked={selectedTags.includes(item)} | 
			
		||||
 | 
				                            onChange={(checked) => { | 
			
		||||
 | 
				                              handleChange(checked, item) | 
			
		||||
 | 
				                            }} | 
			
		||||
 | 
				                    > | 
			
		||||
 | 
				                      {item} | 
			
		||||
 | 
				                    </Tag.CheckableTag> | 
			
		||||
 | 
				                  }) | 
			
		||||
 | 
				                } | 
			
		||||
 | 
				              </Flex> | 
			
		||||
 | 
				            </Flex> | 
			
		||||
 | 
				          </Spin> | 
			
		||||
 | 
				  ) | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export default TagPro | 
			
		||||
@ -0,0 +1,53 @@ | 
			
		|||||
 | 
				export default { | 
			
		||||
 | 
				  title: '视频管理', | 
			
		||||
 | 
				  description: '视频管理', | 
			
		||||
 | 
				  search: '搜索', | 
			
		||||
 | 
				  add: '新增', | 
			
		||||
 | 
				  edit: '编辑', | 
			
		||||
 | 
				  delete: '删除', | 
			
		||||
 | 
				  type_id: [ | 
			
		||||
 | 
				    '','电影', '剧集', '动漫' | 
			
		||||
 | 
				  ], | 
			
		||||
 | 
				  lock: [ | 
			
		||||
 | 
				    '未锁', '锁定' | 
			
		||||
 | 
				  ], | 
			
		||||
 | 
				  columns: { | 
			
		||||
 | 
				    id: 'ID', | 
			
		||||
 | 
				    source_url: '源站点地址', | 
			
		||||
 | 
				    collect_id: '站点id', | 
			
		||||
 | 
				    type_id: '类型', | 
			
		||||
 | 
				    title: '标题', | 
			
		||||
 | 
				    title_sub: '又名', | 
			
		||||
 | 
				    letter: '首字母', | 
			
		||||
 | 
				    tag: 'TAG', | 
			
		||||
 | 
				    lock: '锁定后显示', | 
			
		||||
 | 
				    copyright: '版权', | 
			
		||||
 | 
				    is_end: '完结', | 
			
		||||
 | 
				    status: '状态', | 
			
		||||
 | 
				    category_id: '分类', | 
			
		||||
 | 
				    pic: '图片', | 
			
		||||
 | 
				    pic_local: '图片本地路径或MD5', | 
			
		||||
 | 
				    pic_status: '图片状态', | 
			
		||||
 | 
				    actor: '演员', | 
			
		||||
 | 
				    director: '导演', | 
			
		||||
 | 
				    writer: '编剧', | 
			
		||||
 | 
				    remarks: '备注', | 
			
		||||
 | 
				    pubdate: '发布时间', | 
			
		||||
 | 
				    total: '总集数', | 
			
		||||
 | 
				    serial: '连载数', | 
			
		||||
 | 
				    duration: '视频时长', | 
			
		||||
 | 
				    area: '地区', | 
			
		||||
 | 
				    lang: '语言', | 
			
		||||
 | 
				    version: '资源版本', | 
			
		||||
 | 
				    year: '年份', | 
			
		||||
 | 
				    state: '资源类别', | 
			
		||||
 | 
				    douban_score: '豆瓣评分', | 
			
		||||
 | 
				    douban_id: '豆瓣ID', | 
			
		||||
 | 
				    imdb_score: 'imdb评分', | 
			
		||||
 | 
				    imdb_id: 'imdb的id', | 
			
		||||
 | 
				    content: '内容', | 
			
		||||
 | 
				    created_at: '创建时间', | 
			
		||||
 | 
				    updated_at: '更新时间' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,67 @@ | 
			
		|||||
 | 
				import { Empty, Spin, Tree } from 'antd' | 
			
		||||
 | 
				import { useStyle } from '../style.ts' | 
			
		||||
 | 
				import { useTranslation } from '@/i18n.ts' | 
			
		||||
 | 
				import { useSetAtom } from 'jotai' | 
			
		||||
 | 
				import { FormInstance } from 'antd/lib' | 
			
		||||
 | 
				import { useAtomValue } from 'jotai' | 
			
		||||
 | 
				import { TreeNodeRender } from './TreeNodeRender.tsx' | 
			
		||||
 | 
				import { useEffect } from 'react' | 
			
		||||
 | 
				import { categoriesAtom, categoryAtom } from '@/store/cms/category.ts' | 
			
		||||
 | 
				import { Cms } from '@/types' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const CategoryTree = ({ form }: { form: FormInstance }) => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const { styles } = useStyle() | 
			
		||||
 | 
				  const { t } = useTranslation() | 
			
		||||
 | 
				  const setCurrent = useSetAtom(categoryAtom) | 
			
		||||
 | 
				  const { data, isLoading } = useAtomValue(categoriesAtom) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  useEffect(() => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    return () => { | 
			
		||||
 | 
				      setCurrent({} as Cms.ICategory) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  }, []) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const renderEmpty = () => { | 
			
		||||
 | 
				    if ((data?.rows ?? []).length > 0 || isLoading) return null | 
			
		||||
 | 
				    return <Empty description={t('message.emptyDataAdd', '暂无数据,点击添加')}/> | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  return (<> | 
			
		||||
 | 
				            <Spin spinning={isLoading} style={{ minHeight: 200 }}> | 
			
		||||
 | 
				              { | 
			
		||||
 | 
				                renderEmpty() | 
			
		||||
 | 
				              } | 
			
		||||
 | 
				              <Tree.DirectoryTree | 
			
		||||
 | 
				                      className={styles.tree} | 
			
		||||
 | 
				                      treeData={data?.rows ?? []} | 
			
		||||
 | 
				                      defaultExpandAll={true} | 
			
		||||
 | 
				                      // draggable={true}
 | 
			
		||||
 | 
				                      titleRender={(node) => { | 
			
		||||
 | 
				                        return (<TreeNodeRender node={node as any} form={form}/>) | 
			
		||||
 | 
				                      }} | 
			
		||||
 | 
				                      fieldNames={{ | 
			
		||||
 | 
				                        title: 'name', | 
			
		||||
 | 
				                        key: 'id' | 
			
		||||
 | 
				                      }} | 
			
		||||
 | 
				                      onSelect={(item) => { | 
			
		||||
 | 
				                        const current = data?.rows?.find((cate) => cate.id === item[0]) | 
			
		||||
 | 
				                        if (!current) return | 
			
		||||
 | 
				                        const { extend, ...other } = current as Cms.ICategory | 
			
		||||
 | 
				                        const cate = other as Cms.ICategory | 
			
		||||
 | 
				                        if (extend) { | 
			
		||||
 | 
				                          cate.extend = JSON.parse(extend) | 
			
		||||
 | 
				                        } | 
			
		||||
 | 
				                        setCurrent(cate) | 
			
		||||
 | 
				                        form.setFieldsValue(cate) | 
			
		||||
 | 
				                      }} | 
			
		||||
 | 
				                      // checkable={true}
 | 
			
		||||
 | 
				                      showIcon={false} | 
			
		||||
 | 
				              /> | 
			
		||||
 | 
				            </Spin> | 
			
		||||
 | 
				          </> | 
			
		||||
 | 
				  ) | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export default CategoryTree | 
			
		||||
@ -0,0 +1,59 @@ | 
			
		|||||
 | 
				import { memo } from 'react' | 
			
		||||
 | 
				import { MenuItem } from '@/global' | 
			
		||||
 | 
				import { Popconfirm, Space, TreeDataNode } from 'antd' | 
			
		||||
 | 
				import { FormInstance } from 'antd/lib' | 
			
		||||
 | 
				import { useTranslation } from '@/i18n.ts' | 
			
		||||
 | 
				import { useStyle } from '../style.ts' | 
			
		||||
 | 
				import { useAtomValue } from 'jotai' | 
			
		||||
 | 
				import { DeleteAction } from '@/components/icon/action' | 
			
		||||
 | 
				import { deleteCategoryAtom } from '@/store/cms/category.ts' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const TreeNodeRender = memo(({ node }: { | 
			
		||||
 | 
				  node: MenuItem & TreeDataNode, form?: FormInstance | 
			
		||||
 | 
				}) => { | 
			
		||||
 | 
				  const { name } = node | 
			
		||||
 | 
				  const { t } = useTranslation() | 
			
		||||
 | 
				  const { styles } = useStyle() | 
			
		||||
 | 
				  const { mutate } = useAtomValue(deleteCategoryAtom) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  // const setCurrent = useSetAtom(categoryAtom)
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  return ( | 
			
		||||
 | 
				          <div className={styles.treeNode}> | 
			
		||||
 | 
				            <span>{name as any}</span> | 
			
		||||
 | 
				            <span className={'actions'}> | 
			
		||||
 | 
				                    <Space size={'middle'}> | 
			
		||||
 | 
				                        {/*<ActionIcon | 
			
		||||
 | 
				                          size={12} | 
			
		||||
 | 
				                          icon={<PlusOutlined/>} | 
			
		||||
 | 
				                          title={t('actions.add', '添加')} | 
			
		||||
 | 
				                          onClick={(e) => { | 
			
		||||
 | 
				                            // console.log('add')
 | 
			
		||||
 | 
				                            e.stopPropagation() | 
			
		||||
 | 
				                            e.preventDefault() | 
			
		||||
 | 
				                            const data = { | 
			
		||||
 | 
				                              id: 0, | 
			
		||||
 | 
				                              parent_id: node.id, | 
			
		||||
 | 
				                            } | 
			
		||||
 | 
				                            setCurrent(data) | 
			
		||||
 | 
				                            form.setFieldsValue(data) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                          }}/>*/} | 
			
		||||
 | 
				                      <Popconfirm | 
			
		||||
 | 
				                              title={t('message.deleteConfirm', '确定要删除吗?')} | 
			
		||||
 | 
				                              onConfirm={() => { | 
			
		||||
 | 
				                                mutate([ (node as any).id ]) | 
			
		||||
 | 
				                              }} | 
			
		||||
 | 
				                      > | 
			
		||||
 | 
				                          <DeleteAction | 
			
		||||
 | 
				                                  size={12} | 
			
		||||
 | 
				                                  onClick={(e) => { | 
			
		||||
 | 
				                                    e.stopPropagation() | 
			
		||||
 | 
				                                    e.stopPropagation() | 
			
		||||
 | 
				                                  }}/> | 
			
		||||
 | 
				                        </Popconfirm> | 
			
		||||
 | 
				                    </Space> | 
			
		||||
 | 
				                </span> | 
			
		||||
 | 
				          </div> | 
			
		||||
 | 
				  ) | 
			
		||||
 | 
				}) | 
			
		||||
@ -0,0 +1,191 @@ | 
			
		|||||
 | 
				import { useEffect, useRef } from 'react' | 
			
		||||
 | 
				import { useTranslation } from '@/i18n.ts' | 
			
		||||
 | 
				import { useStyle } from './style.ts' | 
			
		||||
 | 
				import { | 
			
		||||
 | 
				  Alert, | 
			
		||||
 | 
				  Button, | 
			
		||||
 | 
				  Divider, | 
			
		||||
 | 
				  Form, | 
			
		||||
 | 
				  Input, | 
			
		||||
 | 
				  InputNumber, | 
			
		||||
 | 
				  InputRef, | 
			
		||||
 | 
				  notification, | 
			
		||||
 | 
				  Flex | 
			
		||||
 | 
				} from 'antd' | 
			
		||||
 | 
				import { useAtom, useAtomValue } from 'jotai' | 
			
		||||
 | 
				import TwoColPageLayout from '@/layout/TwoColPageLayout.tsx' | 
			
		||||
 | 
				import { ProCard } from '@ant-design/pro-components' | 
			
		||||
 | 
				import { PlusOutlined } from '@ant-design/icons' | 
			
		||||
 | 
				import Glass from '@/components/glass' | 
			
		||||
 | 
				import { categoryAtom, saveOrUpdateCategoryAtom } from '@/store/cms/category.ts' | 
			
		||||
 | 
				import Switch from '@/components/switch' | 
			
		||||
 | 
				import CategoryTree from './components/CategoryTree.tsx' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				const Category = () => { | 
			
		||||
 | 
				  const { t } = useTranslation() | 
			
		||||
 | 
				  const { styles, cx } = useStyle() | 
			
		||||
 | 
				  const [ form ] = Form.useForm() | 
			
		||||
 | 
				  const inputRef = useRef<InputRef>() | 
			
		||||
 | 
				  const { mutate, isPending, isError, error } = useAtomValue(saveOrUpdateCategoryAtom) | 
			
		||||
 | 
				  const [ current, setCurrent ] = useAtom(categoryAtom) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  useEffect(() => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    if (isError) { | 
			
		||||
 | 
				      notification.error({ | 
			
		||||
 | 
				        message: t('message.error', '错误'), | 
			
		||||
 | 
				        description: (error as any).message ?? t('message.saveFail', '保存失败'), | 
			
		||||
 | 
				      }) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  }, [ isError ]) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  useEffect(() => { | 
			
		||||
 | 
				    if (current?.id === 0 && inputRef.current) { | 
			
		||||
 | 
				      inputRef.current.focus() | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  }, [ current ]) | 
			
		||||
 | 
				  return ( | 
			
		||||
 | 
				          <TwoColPageLayout | 
			
		||||
 | 
				                  leftPanel={<> | 
			
		||||
 | 
				                    <ProCard title={t('videos.category.title', '分类')}> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                      <CategoryTree form={form}/> | 
			
		||||
 | 
				                    </ProCard> | 
			
		||||
 | 
				                    <div className={styles.treeActions}> | 
			
		||||
 | 
				                      <Divider style={{ flex: 1, margin: '8px 0' }}/> | 
			
		||||
 | 
				                      <Button style={{ flex: 1 }} size={'small'} | 
			
		||||
 | 
				                              block={true} type={'dashed'} | 
			
		||||
 | 
				                              icon={<PlusOutlined/>} | 
			
		||||
 | 
				                              onClick={() => { | 
			
		||||
 | 
				                                const data = { | 
			
		||||
 | 
				                                  name: '', | 
			
		||||
 | 
				                                  extend: { | 
			
		||||
 | 
				                                    class: '', | 
			
		||||
 | 
				                                    area: '', | 
			
		||||
 | 
				                                    lang: '', | 
			
		||||
 | 
				                                    year: '', | 
			
		||||
 | 
				                                    tag: '', | 
			
		||||
 | 
				                                    state: '', | 
			
		||||
 | 
				                                    version: '', | 
			
		||||
 | 
				                                  }, | 
			
		||||
 | 
				                                  sort: 0, | 
			
		||||
 | 
				                                  status: 1, | 
			
		||||
 | 
				                                  parent_id: 0, | 
			
		||||
 | 
				                                  id: 0, | 
			
		||||
 | 
				                                } | 
			
		||||
 | 
				                                setCurrent(data) | 
			
		||||
 | 
				                                form.setFieldsValue(data) | 
			
		||||
 | 
				                              }} | 
			
		||||
 | 
				                      >{t('actions.news')}</Button> | 
			
		||||
 | 
				                    </div> | 
			
		||||
 | 
				                  </>} | 
			
		||||
 | 
				          > | 
			
		||||
 | 
				            <Glass | 
			
		||||
 | 
				                    enabled={current?.id === undefined} | 
			
		||||
 | 
				                    description={<> | 
			
		||||
 | 
				                      <Alert | 
			
		||||
 | 
				                              message={t('message.infoTitle', '提示')} | 
			
		||||
 | 
				                              description={t('videos.category.form.empty', '请从左侧选择一行数据操作')} | 
			
		||||
 | 
				                              type="info" | 
			
		||||
 | 
				                      /> | 
			
		||||
 | 
				                    </>} | 
			
		||||
 | 
				            > | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				              <ProCard title={t('videos.category.setting', '编辑')}> | 
			
		||||
 | 
				                <Form form={form} | 
			
		||||
 | 
				                      initialValues={current!} | 
			
		||||
 | 
				                      labelAlign="right" | 
			
		||||
 | 
				                      labelWrap | 
			
		||||
 | 
				                      colon={false} | 
			
		||||
 | 
				                      className={cx(styles.form, { | 
			
		||||
 | 
				                        [styles.emptyForm]: current?.id === undefined | 
			
		||||
 | 
				                      })} | 
			
		||||
 | 
				                > | 
			
		||||
 | 
				                  <Form.Item hidden={true} label={'id'} name={'id'}> | 
			
		||||
 | 
				                    <Input disabled={true}/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				                  <Form.Item | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                          rules={[ | 
			
		||||
 | 
				                            { required: true, message: t('rules.required') } | 
			
		||||
 | 
				                          ]} | 
			
		||||
 | 
				                          label={t('videos.category.form.name', '名称')} name={'name'}> | 
			
		||||
 | 
				                    <Input ref={inputRef as any} | 
			
		||||
 | 
				                           placeholder={t('videos.category.form.name', '名称')}/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                  <Form.Item label={t('videos.category.form.tag', 'Tag')} | 
			
		||||
 | 
				                             name={[ 'extend', 'class' ]} | 
			
		||||
 | 
				                  > | 
			
		||||
 | 
				                    <Input/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				                  <Form.Item label={t('videos.category.form.area', '地区')} | 
			
		||||
 | 
				                             name={[ 'extend', 'area' ]} | 
			
		||||
 | 
				                  > | 
			
		||||
 | 
				                    <Input/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				                  <Form.Item label={t('videos.category.form.area', '语言')} | 
			
		||||
 | 
				                             name={[ 'extend', 'lang' ]} | 
			
		||||
 | 
				                  > | 
			
		||||
 | 
				                    <Input/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				                  <Form.Item label={t('videos.category.form.area', '年份')} | 
			
		||||
 | 
				                             name={[ 'extend', 'year' ]} | 
			
		||||
 | 
				                  > | 
			
		||||
 | 
				                    <Input/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				                  <Form.Item label={t('videos.category.form.area', '资源')} | 
			
		||||
 | 
				                             name={[ 'extend', 'state' ]} | 
			
		||||
 | 
				                  > | 
			
		||||
 | 
				                    <Input/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				                  <Form.Item label={t('videos.category.form.area', '版本')} | 
			
		||||
 | 
				                             name={[ 'extend', 'version' ]} | 
			
		||||
 | 
				                  > | 
			
		||||
 | 
				                    <Input/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                  <Flex flex={1}> | 
			
		||||
 | 
				                    <Flex flex={1}> | 
			
		||||
 | 
				                      <Form.Item label={t('videos.category.form.sort', '排序')} name={'sort'}> | 
			
		||||
 | 
				                        <InputNumber/> | 
			
		||||
 | 
				                      </Form.Item> | 
			
		||||
 | 
				                    </Flex> | 
			
		||||
 | 
				                    <Flex flex={1}> | 
			
		||||
 | 
				                      <Form.Item label={t('videos.category.form.status', '状态')} | 
			
		||||
 | 
				                                 name={'status'}> | 
			
		||||
 | 
				                        <Switch/> | 
			
		||||
 | 
				                      </Form.Item> | 
			
		||||
 | 
				                    </Flex> | 
			
		||||
 | 
				                  </Flex> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                  <Form.Item label={' '}> | 
			
		||||
 | 
				                    <Button type="primary" | 
			
		||||
 | 
				                            htmlType={'submit'} | 
			
		||||
 | 
				                            loading={isPending} | 
			
		||||
 | 
				                            onClick={() => { | 
			
		||||
 | 
				                              form.validateFields().then((values) => { | 
			
		||||
 | 
				                                mutate({ | 
			
		||||
 | 
				                                  ...values, | 
			
		||||
 | 
				                                  status: values.status ? 1 : 0, | 
			
		||||
 | 
				                                  extend: JSON.stringify(values.extend), | 
			
		||||
 | 
				                                }) | 
			
		||||
 | 
				                              }) | 
			
		||||
 | 
				                            }} | 
			
		||||
 | 
				                    > | 
			
		||||
 | 
				                      {t('videos.category.form.save', '保存')} | 
			
		||||
 | 
				                    </Button> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                </Form> | 
			
		||||
 | 
				              </ProCard> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				            </Glass> | 
			
		||||
 | 
				          </TwoColPageLayout> | 
			
		||||
 | 
				  ) | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export default Category | 
			
		||||
@ -0,0 +1,81 @@ | 
			
		|||||
 | 
				import { createStyles } from '@/theme' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { | 
			
		||||
 | 
				    const prefix = `${prefixCls}-${token?.proPrefix}-category-page` | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    const tree = css`
 | 
			
		||||
 | 
				        .ant-tree { | 
			
		||||
 | 
				            overflow: auto; | 
			
		||||
 | 
				            height: 100%; | 
			
		||||
 | 
				            border-right: 1px solid ${token.colorBorder}; | 
			
		||||
 | 
				            background: ${token.colorBgContainer}; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        .ant-tree-directory .ant-tree-treenode-selected::before { | 
			
		||||
 | 
				            background: ${token.colorBgTextHover}; | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        .ant-tree-treenode:before { | 
			
		||||
 | 
				            border-radius: ${token.borderRadius}px; | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    const treeNode = css`
 | 
			
		||||
 | 
				        display: flex; | 
			
		||||
 | 
				        justify-content: space-between; | 
			
		||||
 | 
				        align-items: center; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        .actions { | 
			
		||||
 | 
				            display: none; | 
			
		||||
 | 
				            padding: 0 10px; | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        &:hover .actions { | 
			
		||||
 | 
				        { | 
			
		||||
 | 
				            display: flex; | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				    const treeActions = css`
 | 
			
		||||
 | 
				        padding: 0 24px 16px; | 
			
		||||
 | 
				        display: flex; | 
			
		||||
 | 
				        flex-direction: column; | 
			
		||||
 | 
				        position: sticky; | 
			
		||||
 | 
				        bottom: 0; | 
			
		||||
 | 
				        z-index: 10; | 
			
		||||
 | 
				        background: ${token.colorBgContainer}; | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    const box = css`
 | 
			
		||||
 | 
				        flex: 1; | 
			
		||||
 | 
				        background: ${token.colorBgContainer}; | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				    const form = css`
 | 
			
		||||
 | 
				        //display: flex;
 | 
			
		||||
 | 
				        //flex-wrap: wrap;
 | 
			
		||||
 | 
				        min-width: 300px; | 
			
		||||
 | 
				        //max-width: 800px;
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				      .ant-form-item-label{ | 
			
		||||
 | 
				        width: 100px; | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				    const emptyForm = css`
 | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				    const authHeight = css`
 | 
			
		||||
 | 
				        min-height: calc(100vh - 122px); | 
			
		||||
 | 
				        background-color: ${token.colorBgContainer}; | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				    return { | 
			
		||||
 | 
				        container: cx(prefix), | 
			
		||||
 | 
				        authHeight, | 
			
		||||
 | 
				        box, | 
			
		||||
 | 
				        form, | 
			
		||||
 | 
				        emptyForm, | 
			
		||||
 | 
				        tree, | 
			
		||||
 | 
				        treeNode, | 
			
		||||
 | 
				        treeActions | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				}) | 
			
		||||
@ -0,0 +1,67 @@ | 
			
		|||||
 | 
				import { Empty, Spin, Tree } from 'antd' | 
			
		||||
 | 
				import { useStyle } from '../style.ts' | 
			
		||||
 | 
				import { useTranslation } from '@/i18n.ts' | 
			
		||||
 | 
				import { useSetAtom } from 'jotai' | 
			
		||||
 | 
				import { FormInstance } from 'antd/lib' | 
			
		||||
 | 
				import { useAtomValue } from 'jotai' | 
			
		||||
 | 
				import { TreeNodeRender } from './TreeNodeRender.tsx' | 
			
		||||
 | 
				import { useEffect } from 'react' | 
			
		||||
 | 
				import { categoriesAtom, categoryAtom } from '@/store/videos/category.ts' | 
			
		||||
 | 
				import { Videos } from '@/types' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const CategoryTree = ({ form }: { form: FormInstance }) => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const { styles } = useStyle() | 
			
		||||
 | 
				  const { t } = useTranslation() | 
			
		||||
 | 
				  const setCurrent = useSetAtom(categoryAtom) | 
			
		||||
 | 
				  const { data, isLoading } = useAtomValue(categoriesAtom) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  useEffect(() => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    return () => { | 
			
		||||
 | 
				      setCurrent({} as Videos.ICategory) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  }, []) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const renderEmpty = () => { | 
			
		||||
 | 
				    if ((data?.rows ?? []).length > 0 || isLoading) return null | 
			
		||||
 | 
				    return <Empty description={t('message.emptyDataAdd', '暂无数据,点击添加')}/> | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  return (<> | 
			
		||||
 | 
				            <Spin spinning={isLoading} style={{ minHeight: 200 }}> | 
			
		||||
 | 
				              { | 
			
		||||
 | 
				                renderEmpty() | 
			
		||||
 | 
				              } | 
			
		||||
 | 
				              <Tree.DirectoryTree | 
			
		||||
 | 
				                      className={styles.tree} | 
			
		||||
 | 
				                      treeData={data?.rows ?? []} | 
			
		||||
 | 
				                      defaultExpandAll={true} | 
			
		||||
 | 
				                      // draggable={true}
 | 
			
		||||
 | 
				                      titleRender={(node) => { | 
			
		||||
 | 
				                        return (<TreeNodeRender node={node as any} form={form}/>) | 
			
		||||
 | 
				                      }} | 
			
		||||
 | 
				                      fieldNames={{ | 
			
		||||
 | 
				                        title: 'name', | 
			
		||||
 | 
				                        key: 'id' | 
			
		||||
 | 
				                      }} | 
			
		||||
 | 
				                      onSelect={(item) => { | 
			
		||||
 | 
				                        const current = data?.rows?.find((cate) => cate.id === item[0]) | 
			
		||||
 | 
				                        if (!current) return | 
			
		||||
 | 
				                        const { extend, ...other } = current as Videos.ICategory | 
			
		||||
 | 
				                        const cate = other as Videos.ICategory | 
			
		||||
 | 
				                        if (extend) { | 
			
		||||
 | 
				                          cate.extend = JSON.parse(extend) | 
			
		||||
 | 
				                        } | 
			
		||||
 | 
				                        setCurrent(cate) | 
			
		||||
 | 
				                        form.setFieldsValue(cate) | 
			
		||||
 | 
				                      }} | 
			
		||||
 | 
				                      // checkable={true}
 | 
			
		||||
 | 
				                      showIcon={false} | 
			
		||||
 | 
				              /> | 
			
		||||
 | 
				            </Spin> | 
			
		||||
 | 
				          </> | 
			
		||||
 | 
				  ) | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export default CategoryTree | 
			
		||||
@ -0,0 +1,59 @@ | 
			
		|||||
 | 
				import { memo } from 'react' | 
			
		||||
 | 
				import { MenuItem } from '@/global' | 
			
		||||
 | 
				import { Popconfirm, Space, TreeDataNode } from 'antd' | 
			
		||||
 | 
				import { FormInstance } from 'antd/lib' | 
			
		||||
 | 
				import { useTranslation } from '@/i18n.ts' | 
			
		||||
 | 
				import { useStyle } from '../style.ts' | 
			
		||||
 | 
				import { useAtomValue } from 'jotai' | 
			
		||||
 | 
				import { DeleteAction } from '@/components/icon/action' | 
			
		||||
 | 
				import { deleteCategoryAtom } from '@/store/videos/category.ts' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const TreeNodeRender = memo(({ node }: { | 
			
		||||
 | 
				  node: MenuItem & TreeDataNode, form?: FormInstance | 
			
		||||
 | 
				}) => { | 
			
		||||
 | 
				  const { name } = node | 
			
		||||
 | 
				  const { t } = useTranslation() | 
			
		||||
 | 
				  const { styles } = useStyle() | 
			
		||||
 | 
				  const { mutate } = useAtomValue(deleteCategoryAtom) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  // const setCurrent = useSetAtom(categoryAtom)
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  return ( | 
			
		||||
 | 
				          <div className={styles.treeNode}> | 
			
		||||
 | 
				            <span>{name as any}</span> | 
			
		||||
 | 
				            <span className={'actions'}> | 
			
		||||
 | 
				                    <Space size={'middle'}> | 
			
		||||
 | 
				                        {/*<ActionIcon | 
			
		||||
 | 
				                          size={12} | 
			
		||||
 | 
				                          icon={<PlusOutlined/>} | 
			
		||||
 | 
				                          title={t('actions.add', '添加')} | 
			
		||||
 | 
				                          onClick={(e) => { | 
			
		||||
 | 
				                            // console.log('add')
 | 
			
		||||
 | 
				                            e.stopPropagation() | 
			
		||||
 | 
				                            e.preventDefault() | 
			
		||||
 | 
				                            const data = { | 
			
		||||
 | 
				                              id: 0, | 
			
		||||
 | 
				                              parent_id: node.id, | 
			
		||||
 | 
				                            } | 
			
		||||
 | 
				                            setCurrent(data) | 
			
		||||
 | 
				                            form.setFieldsValue(data) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                          }}/>*/} | 
			
		||||
 | 
				                      <Popconfirm | 
			
		||||
 | 
				                              title={t('message.deleteConfirm', '确定要删除吗?')} | 
			
		||||
 | 
				                              onConfirm={() => { | 
			
		||||
 | 
				                                mutate([ (node as any).id ]) | 
			
		||||
 | 
				                              }} | 
			
		||||
 | 
				                      > | 
			
		||||
 | 
				                          <DeleteAction | 
			
		||||
 | 
				                                  size={12} | 
			
		||||
 | 
				                                  onClick={(e) => { | 
			
		||||
 | 
				                                    e.stopPropagation() | 
			
		||||
 | 
				                                    e.stopPropagation() | 
			
		||||
 | 
				                                  }}/> | 
			
		||||
 | 
				                        </Popconfirm> | 
			
		||||
 | 
				                    </Space> | 
			
		||||
 | 
				                </span> | 
			
		||||
 | 
				          </div> | 
			
		||||
 | 
				  ) | 
			
		||||
 | 
				}) | 
			
		||||
@ -0,0 +1,191 @@ | 
			
		|||||
 | 
				import { useEffect, useRef } from 'react' | 
			
		||||
 | 
				import { useTranslation } from '@/i18n.ts' | 
			
		||||
 | 
				import { useStyle } from './style.ts' | 
			
		||||
 | 
				import { | 
			
		||||
 | 
				  Alert, | 
			
		||||
 | 
				  Button, | 
			
		||||
 | 
				  Divider, | 
			
		||||
 | 
				  Form, | 
			
		||||
 | 
				  Input, | 
			
		||||
 | 
				  InputNumber, | 
			
		||||
 | 
				  InputRef, | 
			
		||||
 | 
				  notification, | 
			
		||||
 | 
				  Flex | 
			
		||||
 | 
				} from 'antd' | 
			
		||||
 | 
				import { useAtom, useAtomValue } from 'jotai' | 
			
		||||
 | 
				import TwoColPageLayout from '@/layout/TwoColPageLayout.tsx' | 
			
		||||
 | 
				import { ProCard } from '@ant-design/pro-components' | 
			
		||||
 | 
				import { PlusOutlined } from '@ant-design/icons' | 
			
		||||
 | 
				import Glass from '@/components/glass' | 
			
		||||
 | 
				import { categoryAtom, saveOrUpdateCategoryAtom } from '@/store/videos/category.ts' | 
			
		||||
 | 
				import Switch from '@/components/switch' | 
			
		||||
 | 
				import CategoryTree from './components/CategoryTree.tsx' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				const Category = () => { | 
			
		||||
 | 
				  const { t } = useTranslation() | 
			
		||||
 | 
				  const { styles, cx } = useStyle() | 
			
		||||
 | 
				  const [ form ] = Form.useForm() | 
			
		||||
 | 
				  const inputRef = useRef<InputRef>() | 
			
		||||
 | 
				  const { mutate, isPending, isError, error } = useAtomValue(saveOrUpdateCategoryAtom) | 
			
		||||
 | 
				  const [ current, setCurrent ] = useAtom(categoryAtom) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  useEffect(() => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    if (isError) { | 
			
		||||
 | 
				      notification.error({ | 
			
		||||
 | 
				        message: t('message.error', '错误'), | 
			
		||||
 | 
				        description: (error as any).message ?? t('message.saveFail', '保存失败'), | 
			
		||||
 | 
				      }) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  }, [ isError ]) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  useEffect(() => { | 
			
		||||
 | 
				    if (current?.id === 0 && inputRef.current) { | 
			
		||||
 | 
				      inputRef.current.focus() | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  }, [ current ]) | 
			
		||||
 | 
				  return ( | 
			
		||||
 | 
				          <TwoColPageLayout | 
			
		||||
 | 
				                  leftPanel={<> | 
			
		||||
 | 
				                    <ProCard title={t('videos.category.title', '分类')}> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                      <CategoryTree form={form}/> | 
			
		||||
 | 
				                    </ProCard> | 
			
		||||
 | 
				                    <div className={styles.treeActions}> | 
			
		||||
 | 
				                      <Divider style={{ flex: 1, margin: '8px 0' }}/> | 
			
		||||
 | 
				                      <Button style={{ flex: 1 }} size={'small'} | 
			
		||||
 | 
				                              block={true} type={'dashed'} | 
			
		||||
 | 
				                              icon={<PlusOutlined/>} | 
			
		||||
 | 
				                              onClick={() => { | 
			
		||||
 | 
				                                const data = { | 
			
		||||
 | 
				                                  name: '', | 
			
		||||
 | 
				                                  extend: { | 
			
		||||
 | 
				                                    class: '', | 
			
		||||
 | 
				                                    area: '', | 
			
		||||
 | 
				                                    lang: '', | 
			
		||||
 | 
				                                    year: '', | 
			
		||||
 | 
				                                    tag: '', | 
			
		||||
 | 
				                                    state: '', | 
			
		||||
 | 
				                                    version: '', | 
			
		||||
 | 
				                                  }, | 
			
		||||
 | 
				                                  sort: 0, | 
			
		||||
 | 
				                                  status: 1, | 
			
		||||
 | 
				                                  parent_id: 0, | 
			
		||||
 | 
				                                  id: 0, | 
			
		||||
 | 
				                                } | 
			
		||||
 | 
				                                setCurrent(data) | 
			
		||||
 | 
				                                form.setFieldsValue(data) | 
			
		||||
 | 
				                              }} | 
			
		||||
 | 
				                      >{t('actions.news')}</Button> | 
			
		||||
 | 
				                    </div> | 
			
		||||
 | 
				                  </>} | 
			
		||||
 | 
				          > | 
			
		||||
 | 
				            <Glass | 
			
		||||
 | 
				                    enabled={current?.id === undefined} | 
			
		||||
 | 
				                    description={<> | 
			
		||||
 | 
				                      <Alert | 
			
		||||
 | 
				                              message={t('message.infoTitle', '提示')} | 
			
		||||
 | 
				                              description={t('videos.category.form.empty', '请从左侧选择一行数据操作')} | 
			
		||||
 | 
				                              type="info" | 
			
		||||
 | 
				                      /> | 
			
		||||
 | 
				                    </>} | 
			
		||||
 | 
				            > | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				              <ProCard title={t('videos.category.setting', '编辑')}> | 
			
		||||
 | 
				                <Form form={form} | 
			
		||||
 | 
				                      initialValues={current!} | 
			
		||||
 | 
				                      labelAlign="right" | 
			
		||||
 | 
				                      labelWrap | 
			
		||||
 | 
				                      colon={false} | 
			
		||||
 | 
				                      className={cx(styles.form, { | 
			
		||||
 | 
				                        [styles.emptyForm]: current?.id === undefined | 
			
		||||
 | 
				                      })} | 
			
		||||
 | 
				                > | 
			
		||||
 | 
				                  <Form.Item hidden={true} label={'id'} name={'id'}> | 
			
		||||
 | 
				                    <Input disabled={true}/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				                  <Form.Item | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                          rules={[ | 
			
		||||
 | 
				                            { required: true, message: t('rules.required') } | 
			
		||||
 | 
				                          ]} | 
			
		||||
 | 
				                          label={t('videos.category.form.name', '名称')} name={'name'}> | 
			
		||||
 | 
				                    <Input ref={inputRef as any} | 
			
		||||
 | 
				                           placeholder={t('videos.category.form.name', '名称')}/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                  <Form.Item label={t('videos.category.form.tag', 'Tag')} | 
			
		||||
 | 
				                             name={[ 'extend', 'class' ]} | 
			
		||||
 | 
				                  > | 
			
		||||
 | 
				                    <Input/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				                  <Form.Item label={t('videos.category.form.area', '地区')} | 
			
		||||
 | 
				                             name={[ 'extend', 'area' ]} | 
			
		||||
 | 
				                  > | 
			
		||||
 | 
				                    <Input/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				                  <Form.Item label={t('videos.category.form.lang', '语言')} | 
			
		||||
 | 
				                             name={[ 'extend', 'lang' ]} | 
			
		||||
 | 
				                  > | 
			
		||||
 | 
				                    <Input/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				                  <Form.Item label={t('videos.category.form.year', '年份')} | 
			
		||||
 | 
				                             name={[ 'extend', 'year' ]} | 
			
		||||
 | 
				                  > | 
			
		||||
 | 
				                    <Input/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				                  <Form.Item label={t('videos.category.form.state', '资源')} | 
			
		||||
 | 
				                             name={[ 'extend', 'state' ]} | 
			
		||||
 | 
				                  > | 
			
		||||
 | 
				                    <Input/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				                  <Form.Item label={t('videos.category.form.version', '版本')} | 
			
		||||
 | 
				                             name={[ 'extend', 'version' ]} | 
			
		||||
 | 
				                  > | 
			
		||||
 | 
				                    <Input/> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                  <Flex flex={1}> | 
			
		||||
 | 
				                    <Flex flex={1}> | 
			
		||||
 | 
				                      <Form.Item label={t('videos.category.form.sort', '排序')} name={'sort'}> | 
			
		||||
 | 
				                        <InputNumber/> | 
			
		||||
 | 
				                      </Form.Item> | 
			
		||||
 | 
				                    </Flex> | 
			
		||||
 | 
				                    <Flex flex={1}> | 
			
		||||
 | 
				                      <Form.Item label={t('videos.category.form.status', '状态')} | 
			
		||||
 | 
				                                 name={'status'}> | 
			
		||||
 | 
				                        <Switch/> | 
			
		||||
 | 
				                      </Form.Item> | 
			
		||||
 | 
				                    </Flex> | 
			
		||||
 | 
				                  </Flex> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                  <Form.Item label={' '}> | 
			
		||||
 | 
				                    <Button type="primary" | 
			
		||||
 | 
				                            htmlType={'submit'} | 
			
		||||
 | 
				                            loading={isPending} | 
			
		||||
 | 
				                            onClick={() => { | 
			
		||||
 | 
				                              form.validateFields().then((values) => { | 
			
		||||
 | 
				                                mutate({ | 
			
		||||
 | 
				                                  ...values, | 
			
		||||
 | 
				                                  status: values.status ? 1 : 0, | 
			
		||||
 | 
				                                  extend: JSON.stringify(values.extend), | 
			
		||||
 | 
				                                }) | 
			
		||||
 | 
				                              }) | 
			
		||||
 | 
				                            }} | 
			
		||||
 | 
				                    > | 
			
		||||
 | 
				                      {t('videos.category.form.save', '保存')} | 
			
		||||
 | 
				                    </Button> | 
			
		||||
 | 
				                  </Form.Item> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                </Form> | 
			
		||||
 | 
				              </ProCard> | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				            </Glass> | 
			
		||||
 | 
				          </TwoColPageLayout> | 
			
		||||
 | 
				  ) | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export default Category | 
			
		||||
@ -0,0 +1,81 @@ | 
			
		|||||
 | 
				import { createStyles } from '@/theme' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const useStyle = createStyles(({ token, css, cx, prefixCls }) => { | 
			
		||||
 | 
				    const prefix = `${prefixCls}-${token?.proPrefix}-category-page` | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    const tree = css`
 | 
			
		||||
 | 
				        .ant-tree { | 
			
		||||
 | 
				            overflow: auto; | 
			
		||||
 | 
				            height: 100%; | 
			
		||||
 | 
				            border-right: 1px solid ${token.colorBorder}; | 
			
		||||
 | 
				            background: ${token.colorBgContainer}; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        .ant-tree-directory .ant-tree-treenode-selected::before { | 
			
		||||
 | 
				            background: ${token.colorBgTextHover}; | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        .ant-tree-treenode:before { | 
			
		||||
 | 
				            border-radius: ${token.borderRadius}px; | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    const treeNode = css`
 | 
			
		||||
 | 
				        display: flex; | 
			
		||||
 | 
				        justify-content: space-between; | 
			
		||||
 | 
				        align-items: center; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        .actions { | 
			
		||||
 | 
				            display: none; | 
			
		||||
 | 
				            padding: 0 10px; | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        &:hover .actions { | 
			
		||||
 | 
				        { | 
			
		||||
 | 
				            display: flex; | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				    const treeActions = css`
 | 
			
		||||
 | 
				        padding: 0 24px 16px; | 
			
		||||
 | 
				        display: flex; | 
			
		||||
 | 
				        flex-direction: column; | 
			
		||||
 | 
				        position: sticky; | 
			
		||||
 | 
				        bottom: 0; | 
			
		||||
 | 
				        z-index: 10; | 
			
		||||
 | 
				        background: ${token.colorBgContainer}; | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				    const box = css`
 | 
			
		||||
 | 
				        flex: 1; | 
			
		||||
 | 
				        background: ${token.colorBgContainer}; | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				    const form = css`
 | 
			
		||||
 | 
				        //display: flex;
 | 
			
		||||
 | 
				        //flex-wrap: wrap;
 | 
			
		||||
 | 
				        min-width: 300px; | 
			
		||||
 | 
				        //max-width: 800px;
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				      .ant-form-item-label{ | 
			
		||||
 | 
				        width: 100px; | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				    const emptyForm = css`
 | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				    const authHeight = css`
 | 
			
		||||
 | 
				        min-height: calc(100vh - 122px); | 
			
		||||
 | 
				        background-color: ${token.colorBgContainer}; | 
			
		||||
 | 
				    `
 | 
			
		||||
 | 
				    return { | 
			
		||||
 | 
				        container: cx(prefix), | 
			
		||||
 | 
				        authHeight, | 
			
		||||
 | 
				        box, | 
			
		||||
 | 
				        form, | 
			
		||||
 | 
				        emptyForm, | 
			
		||||
 | 
				        tree, | 
			
		||||
 | 
				        treeNode, | 
			
		||||
 | 
				        treeActions | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				}) | 
			
		||||
@ -0,0 +1,391 @@ | 
			
		|||||
 | 
				import { useTranslation } from '@/i18n.ts' | 
			
		||||
 | 
				import { Button, Form, Image, Popconfirm } from 'antd' | 
			
		||||
 | 
				import { useAtom, useAtomValue, useSetAtom } from 'jotai' | 
			
		||||
 | 
				import { | 
			
		||||
 | 
				  deleteVideoAtom, getTypeName, | 
			
		||||
 | 
				  saveOrUpdateVideoAtom, videosAtom, videoSearchAtom, videoTypes | 
			
		||||
 | 
				} from '@/store/videos/video.ts' | 
			
		||||
 | 
				import { useEffect, useMemo, useState } from 'react' | 
			
		||||
 | 
				import Switch from '@/components/switch' | 
			
		||||
 | 
				import Action from '@/components/action/Action.tsx' | 
			
		||||
 | 
				import { BetaSchemaForm, ProColumns, ProFormColumnsType, ProTable } from '@ant-design/pro-components' | 
			
		||||
 | 
				import ListPageLayout from '@/layout/ListPageLayout.tsx' | 
			
		||||
 | 
				import { categoryByIdAtom, categoryIdAtom } from '@/store/videos/category.ts' | 
			
		||||
 | 
				import TagPro from '@/components/tag-pro/TagPro.tsx' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				const i18nPrefix = 'videos.list' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				const Video = () => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  // const { styles } = useStyle()
 | 
			
		||||
 | 
				  const { t } = useTranslation() | 
			
		||||
 | 
				  const [ form ] = Form.useForm() | 
			
		||||
 | 
				  const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateVideoAtom) | 
			
		||||
 | 
				  const [ search, setSearch ] = useAtom(videoSearchAtom) | 
			
		||||
 | 
				  const { data, isFetching, isLoading, refetch } = useAtomValue(videosAtom) | 
			
		||||
 | 
				  const { mutate: deleteVideo, isPending: isDeleting } = useAtomValue(deleteVideoAtom) | 
			
		||||
 | 
				  const setCategoryId = useSetAtom(categoryIdAtom) | 
			
		||||
 | 
				  const { data: category, isLoading: isCategoryFetching } = useAtomValue(categoryByIdAtom) | 
			
		||||
 | 
				  const [ open, setOpen ] = useState(false) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  const columns = useMemo(() => { | 
			
		||||
 | 
				    return [ | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: 'ID', | 
			
		||||
 | 
				        dataIndex: 'id', | 
			
		||||
 | 
				        hideInTable: true, | 
			
		||||
 | 
				        hideInSearch: true, | 
			
		||||
 | 
				        formItemProps: { hidden: true } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.video_id`, 'VideoId'), | 
			
		||||
 | 
				        'dataIndex': 'video_id', | 
			
		||||
 | 
				        hideInTable: true, | 
			
		||||
 | 
				        hideInSearch: true, | 
			
		||||
 | 
				        hideInSetting: true, | 
			
		||||
 | 
				        formItemProps: { hidden: true }, | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.title`, 'Title'), | 
			
		||||
 | 
				        'dataIndex': 'title', | 
			
		||||
 | 
				        onHeaderCell: () => { | 
			
		||||
 | 
				          return { | 
			
		||||
 | 
				            width: 200, | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        colProps: { | 
			
		||||
 | 
				          span: 8 | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.title_sub`, 'TitleSub'), | 
			
		||||
 | 
				        'dataIndex': 'title_sub', | 
			
		||||
 | 
				        onHeaderCell: () => { | 
			
		||||
 | 
				          return { | 
			
		||||
 | 
				            width: 200, | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        colProps: { | 
			
		||||
 | 
				          span: 8 | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.type_id`, 'TypeId'), | 
			
		||||
 | 
				        'dataIndex': 'type_id', | 
			
		||||
 | 
				        valueType: 'select', | 
			
		||||
 | 
				        fieldProps: { | 
			
		||||
 | 
				          options: videoTypes, | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        render: (_dom, record) => { | 
			
		||||
 | 
				          return t(`${i18nPrefix}.type_id.${record.type_id}`) | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        colProps: { | 
			
		||||
 | 
				          span: 8 | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.source_url`, 'SourceUrl'), | 
			
		||||
 | 
				        'dataIndex': 'source_url', | 
			
		||||
 | 
				        ellipsis: true, | 
			
		||||
 | 
				        copyable: true, | 
			
		||||
 | 
				        onHeaderCell: () => { | 
			
		||||
 | 
				          return { | 
			
		||||
 | 
				            width: 200, | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.category_id`, 'CategoryId'), | 
			
		||||
 | 
				        'dataIndex': 'class_name', | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.actor`, 'Actor'), | 
			
		||||
 | 
				        'dataIndex': 'actor', | 
			
		||||
 | 
				        ellipsis: true, | 
			
		||||
 | 
				        onHeaderCell: () => { | 
			
		||||
 | 
				          return { | 
			
		||||
 | 
				            width: 200, | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.director`, 'Director'), | 
			
		||||
 | 
				        'dataIndex': 'director' | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.content`, 'Content'), | 
			
		||||
 | 
				        'dataIndex': 'content', | 
			
		||||
 | 
				        valueType: 'textarea', | 
			
		||||
 | 
				        ellipsis: true, | 
			
		||||
 | 
				        onHeaderCell: () => ({ | 
			
		||||
 | 
				          width: 200, | 
			
		||||
 | 
				        }), | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.writer`, 'Writer'), | 
			
		||||
 | 
				        'dataIndex': 'writer' | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.remarks`, 'Remarks'), | 
			
		||||
 | 
				        'dataIndex': 'remarks' | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.pubdate`, 'Pubdate'), | 
			
		||||
 | 
				        'dataIndex': 'pubdate', | 
			
		||||
 | 
				        valueType: 'dateTime', | 
			
		||||
 | 
				        colProps: { | 
			
		||||
 | 
				          span: 4 | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.total`, 'Total'), | 
			
		||||
 | 
				        'dataIndex': 'total', | 
			
		||||
 | 
				        valueType: 'digit', | 
			
		||||
 | 
				        colProps: { | 
			
		||||
 | 
				          span: 4 | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.serial`, 'Serial'), | 
			
		||||
 | 
				        'dataIndex': 'serial', | 
			
		||||
 | 
				        colProps: { | 
			
		||||
 | 
				          span: 4 | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.duration`, 'Duration'), | 
			
		||||
 | 
				        'dataIndex': 'duration', | 
			
		||||
 | 
				        colProps: { | 
			
		||||
 | 
				          span: 4 | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.year`, 'Year'), | 
			
		||||
 | 
				        'dataIndex': 'year', | 
			
		||||
 | 
				        valueType: 'dateYear', | 
			
		||||
 | 
				        colProps: { | 
			
		||||
 | 
				          span: 4 | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.tag`, 'Tag'), | 
			
		||||
 | 
				        'dataIndex': 'tag', | 
			
		||||
 | 
				        valueType: 'textarea', | 
			
		||||
 | 
				        ellipsis: true, | 
			
		||||
 | 
				        onHeaderCell: () => { | 
			
		||||
 | 
				          return { | 
			
		||||
 | 
				            width: 200, | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        renderFormItem: (schema, config) => { | 
			
		||||
 | 
				          return <TagPro loading={isCategoryFetching}  tags={category?.extend?.class?.split(',') ?? []} {...config} {...schema.fieldProps} /> | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.area`, 'Area'), | 
			
		||||
 | 
				        'dataIndex': 'area', | 
			
		||||
 | 
				        ellipsis: true, | 
			
		||||
 | 
				        onHeaderCell: () => { | 
			
		||||
 | 
				          return { | 
			
		||||
 | 
				            width: 200, | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        renderFormItem: (schema, config) => { | 
			
		||||
 | 
				          return <TagPro loading={isCategoryFetching}  tags={category?.extend?.area?.split(',') ?? []} {...config} {...schema.fieldProps} /> | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.lang`, 'Lang'), | 
			
		||||
 | 
				        'dataIndex': 'lang', | 
			
		||||
 | 
				        ellipsis: true, | 
			
		||||
 | 
				        onHeaderCell: () => { | 
			
		||||
 | 
				          return { | 
			
		||||
 | 
				            width: 200, | 
			
		||||
 | 
				          } | 
			
		||||
 | 
				        }, | 
			
		||||
 | 
				        renderFormItem: (schema, config) => { | 
			
		||||
 | 
				          return <TagPro loading={isCategoryFetching}  tags={category?.extend?.lang?.split(',') ?? []} {...config} {...schema.fieldProps} /> | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.version`, 'Version'), | 
			
		||||
 | 
				        'dataIndex': 'version', | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				        renderFormItem: (schema, config) => { | 
			
		||||
 | 
				          return <TagPro loading={isCategoryFetching}  tags={category?.extend?.version?.split(',') ?? []} {...config} {...schema.fieldProps} /> | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.state`, 'State'), | 
			
		||||
 | 
				        'dataIndex': 'state', | 
			
		||||
 | 
				        renderFormItem: (schema, config) => { | 
			
		||||
 | 
				          return <TagPro loading={isCategoryFetching} tags={category?.extend?.state?.split(',') ?? []} {...config} {...schema.fieldProps} /> | 
			
		||||
 | 
				        } | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.douban_score`, 'DoubanScore'), | 
			
		||||
 | 
				        'dataIndex': 'douban_score', | 
			
		||||
 | 
				        hideInSearch: true, | 
			
		||||
 | 
				        hideInSetting: true, | 
			
		||||
 | 
				        formItemProps: { hidden: true }, | 
			
		||||
 | 
				        hideInTable: true, | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.douban_id`, 'DoubanId'), | 
			
		||||
 | 
				        'dataIndex': 'douban_id', | 
			
		||||
 | 
				        hideInSearch: true, | 
			
		||||
 | 
				        hideInSetting: true, | 
			
		||||
 | 
				        formItemProps: { hidden: true }, | 
			
		||||
 | 
				        hideInTable: true, | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.imdb_score`, 'ImdbScore'), | 
			
		||||
 | 
				        'dataIndex': 'imdb_score', | 
			
		||||
 | 
				        hideInSearch: true, | 
			
		||||
 | 
				        hideInSetting: true, | 
			
		||||
 | 
				        formItemProps: { hidden: true }, | 
			
		||||
 | 
				        hideInTable: true, | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        'title': t(`${i18nPrefix}.columns.imdb_id`, 'ImdbId'), | 
			
		||||
 | 
				        'dataIndex': 'imdb_id', | 
			
		||||
 | 
				        hideInSearch: true, | 
			
		||||
 | 
				        hideInSetting: true, | 
			
		||||
 | 
				        formItemProps: { hidden: true }, | 
			
		||||
 | 
				        hideInTable: true, | 
			
		||||
 | 
				      }, | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				      { | 
			
		||||
 | 
				        title: t(`${i18nPrefix}.columns.option`, '操作'), | 
			
		||||
 | 
				        key: 'option', | 
			
		||||
 | 
				        valueType: 'option', | 
			
		||||
 | 
				        fixed: 'right', | 
			
		||||
 | 
				        render: (_, record) => [ | 
			
		||||
 | 
				          <Action key="edit" | 
			
		||||
 | 
				                  as={'a'} | 
			
		||||
 | 
				                  onClick={() => { | 
			
		||||
 | 
				                    setCategoryId(record.type_id) | 
			
		||||
 | 
				                    form.setFieldsValue(record) | 
			
		||||
 | 
				                    setOpen(true) | 
			
		||||
 | 
				                  }}>{t('actions.edit')}</Action>, | 
			
		||||
 | 
				          <Popconfirm | 
			
		||||
 | 
				                  key={'del_confirm'} | 
			
		||||
 | 
				                  disabled={isDeleting} | 
			
		||||
 | 
				                  onConfirm={() => { | 
			
		||||
 | 
				                    deleteVideo([ record.id ]) | 
			
		||||
 | 
				                  }} | 
			
		||||
 | 
				                  title={t('message.deleteConfirm')}> | 
			
		||||
 | 
				            <a key="del"> | 
			
		||||
 | 
				              {t('actions.delete', '删除')} | 
			
		||||
 | 
				            </a> | 
			
		||||
 | 
				          </Popconfirm> | 
			
		||||
 | 
				        ] | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				    ] as ProColumns[] | 
			
		||||
 | 
				  }, [ isDeleting, category, isCategoryFetching ]) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  useEffect(() => { | 
			
		||||
 | 
				    if (isSuccess) { | 
			
		||||
 | 
				      setOpen(false) | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  }, [ isSuccess ]) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  return ( | 
			
		||||
 | 
				          <ListPageLayout> | 
			
		||||
 | 
				            <ProTable | 
			
		||||
 | 
				                    rowKey="id" | 
			
		||||
 | 
				                    headerTitle={t(`${i18nPrefix}.title`, '视频管理')} | 
			
		||||
 | 
				                    toolbar={{ | 
			
		||||
 | 
				                      search: { | 
			
		||||
 | 
				                        loading: isFetching && !!search.key, | 
			
		||||
 | 
				                        onSearch: (value: string) => { | 
			
		||||
 | 
				                          setSearch(prev => ({ | 
			
		||||
 | 
				                            ...prev, | 
			
		||||
 | 
				                            key: value | 
			
		||||
 | 
				                          })) | 
			
		||||
 | 
				                        }, | 
			
		||||
 | 
				                        allowClear: true, | 
			
		||||
 | 
				                        placeholder: t(`${i18nPrefix}.placeholder`, '输入视频名称') | 
			
		||||
 | 
				                      }, | 
			
		||||
 | 
				                      actions: [ | 
			
		||||
 | 
				                        <Button | 
			
		||||
 | 
				                                onClick={() => { | 
			
		||||
 | 
				                                  form.resetFields() | 
			
		||||
 | 
				                                  form.setFieldsValue({ | 
			
		||||
 | 
				                                    id: 0, | 
			
		||||
 | 
				                                  }) | 
			
		||||
 | 
				                                  setOpen(true) | 
			
		||||
 | 
				                                }} | 
			
		||||
 | 
				                                type={'primary'}>{t(`${i18nPrefix}.add`, '添加')}</Button> | 
			
		||||
 | 
				                      ] | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    scroll={{ | 
			
		||||
 | 
				                      x: 3500, | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    loading={isLoading || isFetching} | 
			
		||||
 | 
				                    dataSource={data?.rows ?? []} | 
			
		||||
 | 
				                    columns={columns} | 
			
		||||
 | 
				                    search={false} | 
			
		||||
 | 
				                    options={{ | 
			
		||||
 | 
				                      reload: () => { | 
			
		||||
 | 
				                        refetch() | 
			
		||||
 | 
				                      }, | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    pagination={{ | 
			
		||||
 | 
				                      total: data?.total, | 
			
		||||
 | 
				                      pageSize: search.pageSize, | 
			
		||||
 | 
				                      current: search.page, | 
			
		||||
 | 
				                      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={t(`${i18nPrefix}.title_${form.getFieldValue('id') !== 0 ? 'edit' : 'add'}`, form.getFieldValue('id') !== 0 ? '视频编辑' : '视频添加')} | 
			
		||||
 | 
				                    // colProps={{ span: 24 }}
 | 
			
		||||
 | 
				                    // labelCol={{ span: 8 }}
 | 
			
		||||
 | 
				                    // wrapperCol={{ span: 14 }}
 | 
			
		||||
 | 
				                    layoutType={'DrawerForm'} | 
			
		||||
 | 
				                    open={open} | 
			
		||||
 | 
				                    drawerProps={{ | 
			
		||||
 | 
				                      maskClosable: false, | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    onOpenChange={(open) => { | 
			
		||||
 | 
				                      setOpen(open) | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    loading={isSubmitting} | 
			
		||||
 | 
				                    onValuesChange={(values) => { | 
			
		||||
 | 
				                      if(values.type_id) { | 
			
		||||
 | 
				                        setCategoryId(values.type_id) | 
			
		||||
 | 
				                        const typeName = getTypeName(values.type_id) | 
			
		||||
 | 
				                        form.setFieldsValue({ | 
			
		||||
 | 
				                          class_name: typeName, | 
			
		||||
 | 
				                        }) | 
			
		||||
 | 
				                      } | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    onFinish={async (values) => { | 
			
		||||
 | 
				                      // console.log('values', values)
 | 
			
		||||
 | 
				                      saveOrUpdate(values) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				                    }} | 
			
		||||
 | 
				                    columns={columns as ProFormColumnsType[]}/> | 
			
		||||
 | 
				          </ListPageLayout> | 
			
		||||
 | 
				  ) | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export default Video | 
			
		||||
@ -0,0 +1,13 @@ | 
			
		|||||
 | 
				import { createCURD } from '@/service/base.ts' | 
			
		||||
 | 
				import { Videos } from '@/types' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				const videoServ = { | 
			
		||||
 | 
				  category: { | 
			
		||||
 | 
				    ...createCURD<any, Videos.ICategory>('/videos/category') | 
			
		||||
 | 
				  }, | 
			
		||||
 | 
				  video: { | 
			
		||||
 | 
				    ...createCURD<any, Videos.IVideo>('/videos/video') | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export default videoServ | 
			
		||||
@ -0,0 +1,108 @@ | 
			
		|||||
 | 
				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 { Cms } from '@/types' | 
			
		||||
 | 
				import cmsServ from '@/service/cms.ts' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type SearchParams = IPage & { | 
			
		||||
 | 
				  key?: string | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const categoryIdAtom = atom(0) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const categoryIdsAtom = atom<number[]>([]) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const categoryAtom = atom<Cms.ICategory>(undefined as unknown as Cms.ICategory) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const categorySearchAtom = atom<SearchParams>({ | 
			
		||||
 | 
				  key: '' | 
			
		||||
 | 
				} as SearchParams) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const categoryPageAtom = atom<IPage>({ | 
			
		||||
 | 
				  pageSize: 10, | 
			
		||||
 | 
				  page: 1, | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const categoriesAtom = atomWithQuery((get) => { | 
			
		||||
 | 
				  return { | 
			
		||||
 | 
				    queryKey: [ 'categories', get(categorySearchAtom) ], | 
			
		||||
 | 
				    queryFn: async ({ queryKey: [ , params ] }) => { | 
			
		||||
 | 
				      return await cmsServ.category.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 saveOrUpdateCategoryAtom = atomWithMutation<IApiResult, Cms.ICategory>((get) => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  return { | 
			
		||||
 | 
				    mutationKey: [ 'updateCategory' ], | 
			
		||||
 | 
				    mutationFn: async (data) => { | 
			
		||||
 | 
				      //data.status = data.status ? '1' : '0'
 | 
			
		||||
 | 
				      if (data.id === 0) { | 
			
		||||
 | 
				        return await cmsServ.category.add(data) | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				      return await cmsServ.category.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: [ 'categories', get(categorySearchAtom) ] }) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				      return res | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const deleteCategoryAtom = atomWithMutation((get) => { | 
			
		||||
 | 
				  return { | 
			
		||||
 | 
				    mutationKey: [ 'deleteCategory' ], | 
			
		||||
 | 
				    mutationFn: async (ids: number[]) => { | 
			
		||||
 | 
				      return await cmsServ.category.batchDelete(ids ?? get(categoryIdsAtom)) | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    onSuccess: (res) => { | 
			
		||||
 | 
				      message.success('message.deleteSuccess') | 
			
		||||
 | 
				      //更新列表
 | 
			
		||||
 | 
				      get(queryClientAtom).invalidateQueries({ queryKey: [ 'categories', get(categorySearchAtom) ] }) | 
			
		||||
 | 
				      return res | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				//getById
 | 
			
		||||
 | 
				export const categoryByIdAtom = atomWithQuery((get) => { | 
			
		||||
 | 
				  return { | 
			
		||||
 | 
				    enabled: !!get(categoryIdAtom), | 
			
		||||
 | 
				    queryKey: [ 'category', get(categoryIdAtom) ], | 
			
		||||
 | 
				    queryFn: async ({ queryKey: [ , id ] }) => { | 
			
		||||
 | 
				      const res = await cmsServ.category.info(id as number) | 
			
		||||
 | 
				      //res.data.status = convertToBool(res.data.status)
 | 
			
		||||
 | 
				      return res | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    select: (res) => { | 
			
		||||
 | 
				      const data = res.data | 
			
		||||
 | 
				      if (data.extend) { | 
			
		||||
 | 
				        data.extend = JSON.parse(data.extend) | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				      return data | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				}) | 
			
		||||
@ -0,0 +1,107 @@ | 
			
		|||||
 | 
				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 { Videos } from '@/types' | 
			
		||||
 | 
				import videoServ from '@/service/videos.ts' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type SearchParams = IPage & { | 
			
		||||
 | 
				  key?: string | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const categoryIdAtom = atom(0) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const categoryIdsAtom = atom<number[]>([]) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const categoryAtom = atom<Videos.ICategory>(undefined as unknown as Videos.ICategory) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const categorySearchAtom = atom<SearchParams>({ | 
			
		||||
 | 
				  key: '' | 
			
		||||
 | 
				} as SearchParams) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const categoryPageAtom = atom<IPage>({ | 
			
		||||
 | 
				  pageSize: 10, | 
			
		||||
 | 
				  page: 1, | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const categoriesAtom = atomWithQuery((get) => { | 
			
		||||
 | 
				  return { | 
			
		||||
 | 
				    queryKey: [ 'categories', get(categorySearchAtom) ], | 
			
		||||
 | 
				    queryFn: async ({ queryKey: [ , params ] }) => { | 
			
		||||
 | 
				      return await videoServ.category.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 saveOrUpdateCategoryAtom = atomWithMutation<IApiResult, Videos.ICategory>((get) => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  return { | 
			
		||||
 | 
				    mutationKey: [ 'updateCategory' ], | 
			
		||||
 | 
				    mutationFn: async (data) => { | 
			
		||||
 | 
				      //data.status = data.status ? '1' : '0'
 | 
			
		||||
 | 
				      if (data.id === 0) { | 
			
		||||
 | 
				        return await videoServ.category.add(data) | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				      return await videoServ.category.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: [ 'categories', get(categorySearchAtom) ] }) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				      return res | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const deleteCategoryAtom = atomWithMutation((get) => { | 
			
		||||
 | 
				  return { | 
			
		||||
 | 
				    mutationKey: [ 'deleteCategory' ], | 
			
		||||
 | 
				    mutationFn: async (ids: number[]) => { | 
			
		||||
 | 
				      return await videoServ.category.batchDelete(ids ?? get(categoryIdsAtom)) | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    onSuccess: (res) => { | 
			
		||||
 | 
				      message.success('message.deleteSuccess') | 
			
		||||
 | 
				      //更新列表
 | 
			
		||||
 | 
				      get(queryClientAtom).invalidateQueries({ queryKey: [ 'categories', get(categorySearchAtom) ] }) | 
			
		||||
 | 
				      return res | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				//getById
 | 
			
		||||
 | 
				export const categoryByIdAtom = atomWithQuery((get) => { | 
			
		||||
 | 
				  return { | 
			
		||||
 | 
				    enabled: !!get(categoryIdAtom), | 
			
		||||
 | 
				    queryKey: [ 'category', get(categoryIdAtom) ], | 
			
		||||
 | 
				    queryFn: async ({ queryKey: [ , id ] }) => { | 
			
		||||
 | 
				      const res = await videoServ.category.info(id as number) | 
			
		||||
 | 
				      //res.data.status = convertToBool(res.data.status)
 | 
			
		||||
 | 
				      return res | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    select: (res) => { | 
			
		||||
 | 
				      const data = res.data | 
			
		||||
 | 
				      if (data.extend) { | 
			
		||||
 | 
				        data.extend = JSON.parse(data.extend) | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				      return data | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				}) | 
			
		||||
@ -0,0 +1,97 @@ | 
			
		|||||
 | 
				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 { Videos } from '@/types' | 
			
		||||
 | 
				import videoServ from '@/service/videos.ts' | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				const i18nPrefix = 'videos.list' | 
			
		||||
 | 
				export const videoTypes = [ | 
			
		||||
 | 
				  { label: t(`${i18nPrefix}.type_id.1`), value: 1 }, | 
			
		||||
 | 
				  { label: t(`${i18nPrefix}.type_id.2`), value: 2 }, | 
			
		||||
 | 
				  { label: t(`${i18nPrefix}.type_id.3`), value: 3 }, | 
			
		||||
 | 
				] | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const getTypeName = (typeId: number) => { | 
			
		||||
 | 
				  return videoTypes.find(item => item.value === typeId)?.label | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				type SearchParams = IPage & { | 
			
		||||
 | 
				  key?: string | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const videoIdAtom = atom(0) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const videoIdsAtom = atom<number[]>([]) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const videoAtom = atom<Videos.IVideo>(undefined as unknown as Videos.IVideo) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const videoSearchAtom = atom<SearchParams>({ | 
			
		||||
 | 
				  key: '' | 
			
		||||
 | 
				} as SearchParams) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const videoPageAtom = atom<IPage>({ | 
			
		||||
 | 
				  pageSize: 10, | 
			
		||||
 | 
				  page: 1, | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const videosAtom = atomWithQuery((get) => { | 
			
		||||
 | 
				  return { | 
			
		||||
 | 
				    queryKey: [ 'videos', get(videoSearchAtom) ], | 
			
		||||
 | 
				    queryFn: async ({ queryKey: [ , params ] }) => { | 
			
		||||
 | 
				      return await videoServ.video.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 saveOrUpdateVideoAtom = atomWithMutation<IApiResult, Videos.IVideo>((get) => { | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				  return { | 
			
		||||
 | 
				    mutationKey: [ 'updateVideo' ], | 
			
		||||
 | 
				    mutationFn: async (data) => { | 
			
		||||
 | 
				      //data.status = data.status ? '1' : '0'
 | 
			
		||||
 | 
				      if (data.id === 0) { | 
			
		||||
 | 
				        return await videoServ.video.add(data) | 
			
		||||
 | 
				      } | 
			
		||||
 | 
				      return await videoServ.video.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: [ 'videos', get(videoSearchAtom) ] }) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				      return res | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				}) | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export const deleteVideoAtom = atomWithMutation((get) => { | 
			
		||||
 | 
				  return { | 
			
		||||
 | 
				    mutationKey: [ 'deleteVideo' ], | 
			
		||||
 | 
				    mutationFn: async (ids: number[]) => { | 
			
		||||
 | 
				      return await videoServ.video.batchDelete(ids ?? get(videoIdsAtom)) | 
			
		||||
 | 
				    }, | 
			
		||||
 | 
				    onSuccess: (res) => { | 
			
		||||
 | 
				      message.success('message.deleteSuccess') | 
			
		||||
 | 
				      //更新列表
 | 
			
		||||
 | 
				      get(queryClientAtom).invalidateQueries({ queryKey: [ 'videos', get(videoSearchAtom) ] }) | 
			
		||||
 | 
				      return res | 
			
		||||
 | 
				    } | 
			
		||||
 | 
				  } | 
			
		||||
 | 
				}) | 
			
		||||
@ -0,0 +1,17 @@ | 
			
		|||||
 | 
				export interface ICategory { | 
			
		||||
 | 
				  id: number; | 
			
		||||
 | 
				  parent_id: number; | 
			
		||||
 | 
				  name: string; | 
			
		||||
 | 
				  union: string; | 
			
		||||
 | 
				  sort: number; | 
			
		||||
 | 
				  status: number; | 
			
		||||
 | 
				  seo_title: string; | 
			
		||||
 | 
				  seo_key: string; | 
			
		||||
 | 
				  seo_des: string; | 
			
		||||
 | 
				  tpl_index: string; | 
			
		||||
 | 
				  tpl_list: string; | 
			
		||||
 | 
				  tpl_detail: string; | 
			
		||||
 | 
				  tpl_down: string; | 
			
		||||
 | 
				  tpl_play: string; | 
			
		||||
 | 
				  extend: string; | 
			
		||||
 | 
				} | 
			
		||||
@ -1,21 +1,27 @@ | 
			
		|||||
export namespace System { | 
				export namespace System { | 
			
		||||
    export { IDepartment } from './system/department' | 
				 | 
			
		||||
    export { IUser, IUserInfo } from './system/user' | 
				 | 
			
		||||
    export { LoginRequest, LoginResponse } from './system/login' | 
				 | 
			
		||||
    export { IRole } from './system/roles' | 
				 | 
			
		||||
    export { IMenu } from './system/menus' | 
				 | 
			
		||||
 | 
				  export { IDepartment } from './system/department' | 
			
		||||
 | 
				  export { IUser, IUserInfo } from './system/user' | 
			
		||||
 | 
				  export { LoginRequest, LoginResponse } from './system/login' | 
			
		||||
 | 
				  export { IRole } from './system/roles' | 
			
		||||
 | 
				  export { IMenu } from './system/menus' | 
			
		||||
} | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
export namespace WebSite { | 
				export namespace WebSite { | 
			
		||||
    export { IAcmeAccount } from './website/acme' | 
				 | 
			
		||||
    export { ICA, ISSLObtainByCA } from './website/ca' | 
				 | 
			
		||||
    export { IDnsAccount } from './website/dns' | 
				 | 
			
		||||
    export { ISSL, ProviderType, SSLSearchParam, SSLUploadDto } from './website/ssl' | 
				 | 
			
		||||
 | 
				  export { IAcmeAccount } from './website/acme' | 
			
		||||
 | 
				  export { ICA, ISSLObtainByCA } from './website/ca' | 
			
		||||
 | 
				  export { IDnsAccount } from './website/dns' | 
			
		||||
 | 
				  export { ISSL, ProviderType, SSLSearchParam, SSLUploadDto } from './website/ssl' | 
			
		||||
} | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
export namespace Cms { | 
				export namespace Cms { | 
			
		||||
    export { ICollect } from './cms/collect' | 
				 | 
			
		||||
    export { IVideo } from './cms/video' | 
				 | 
			
		||||
    export { IVideoMagnet } from './cms/video_magnet' | 
				 | 
			
		||||
    export { IVideoCloud } from './cms/video_cloud' | 
				 | 
			
		||||
 | 
				  export { ICollect } from './cms/collect' | 
			
		||||
 | 
				  export { IVideo } from './cms/video' | 
			
		||||
 | 
				  export { IVideoMagnet } from './cms/video_magnet' | 
			
		||||
 | 
				  export { IVideoCloud } from './cms/video_cloud' | 
			
		||||
 | 
				  export { ICategory } from './videos/category' | 
			
		||||
 | 
				} | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				export namespace Videos { | 
			
		||||
 | 
				  export { ICategory } from './videos/category' | 
			
		||||
 | 
				  export { IVideo } from './videos/video' | 
			
		||||
} | 
				} | 
			
		||||
@ -0,0 +1,17 @@ | 
			
		|||||
 | 
				export interface ICategory { | 
			
		||||
 | 
				  id: number; | 
			
		||||
 | 
				  parent_id: number; | 
			
		||||
 | 
				  name: string; | 
			
		||||
 | 
				  union: string; | 
			
		||||
 | 
				  sort: number; | 
			
		||||
 | 
				  status: number; | 
			
		||||
 | 
				  seo_title: string; | 
			
		||||
 | 
				  seo_key: string; | 
			
		||||
 | 
				  seo_des: string; | 
			
		||||
 | 
				  tpl_index: string; | 
			
		||||
 | 
				  tpl_list: string; | 
			
		||||
 | 
				  tpl_detail: string; | 
			
		||||
 | 
				  tpl_down: string; | 
			
		||||
 | 
				  tpl_play: string; | 
			
		||||
 | 
				  extend: string; | 
			
		||||
 | 
				} | 
			
		||||
@ -0,0 +1,23 @@ | 
			
		|||||
 | 
				export interface IVideo { | 
			
		||||
 | 
				  id: number; | 
			
		||||
 | 
				  video_id: string; | 
			
		||||
 | 
				  type_id: number; | 
			
		||||
 | 
				  title: string; | 
			
		||||
 | 
				  title_sub: string; | 
			
		||||
 | 
				  name: string; | 
			
		||||
 | 
				  class_name: string; | 
			
		||||
 | 
				  tag: string; | 
			
		||||
 | 
				  actor: string; | 
			
		||||
 | 
				  director: string; | 
			
		||||
 | 
				  content: string; | 
			
		||||
 | 
				  duration: string; | 
			
		||||
 | 
				  area: string; | 
			
		||||
 | 
				  lang: string; | 
			
		||||
 | 
				  year: number; | 
			
		||||
 | 
				  douban_id: string; | 
			
		||||
 | 
				  imdb_id: string; | 
			
		||||
 | 
				  rt_id: string; | 
			
		||||
 | 
				  mal_id: string; | 
			
		||||
 | 
				  updated_at: any; | 
			
		||||
 | 
				
 | 
			
		||||
 | 
				} | 
			
		||||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue