From 73526f11a63dbe42571f2835c7ce4dd67cf25349 Mon Sep 17 00:00:00 2001 From: dark Date: Sun, 19 May 2024 23:29:45 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E5=A2=9E=E5=8A=A0=E5=88=86=E7=B1=BB?= =?UTF-8?q?=202=E3=80=81=E5=A2=9E=E5=8A=A0video=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/switch/index.tsx | 2 +- src/components/tag-pro/TagPro.tsx | 96 +++++ src/components/tag-pro/index.ts | 0 src/locales/lang/pages/cms/video/zh-CN.ts | 2 +- src/locales/lang/pages/cms/videoCloud/zh-CN.ts | 2 +- src/locales/lang/pages/cms/videoMagnet/zh-CN.ts | 2 +- src/locales/lang/pages/videos/list/zh-CN.ts | 53 +++ src/locales/lang/zh-CN.ts | 4 + src/pages/cms/category/components/CategoryTree.tsx | 67 ++++ .../cms/category/components/TreeNodeRender.tsx | 59 ++++ src/pages/cms/category/index.tsx | 191 ++++++++++ src/pages/cms/category/style.ts | 81 +++++ src/pages/cms/video/index.tsx | 197 ++++++++--- .../videos/category/components/CategoryTree.tsx | 67 ++++ .../videos/category/components/TreeNodeRender.tsx | 59 ++++ src/pages/videos/category/index.tsx | 191 ++++++++++ src/pages/videos/category/style.ts | 81 +++++ src/pages/videos/list/index.tsx | 391 +++++++++++++++++++++ src/service/base.ts | 2 +- src/service/cms.ts | 3 + src/service/videos.ts | 13 + src/store/cms/category.ts | 108 ++++++ src/store/videos/category.ts | 107 ++++++ src/store/videos/video.ts | 97 +++++ src/types/cms/category.d.ts | 17 + src/types/index.d.ts | 32 +- src/types/videos/category.d.ts | 17 + src/types/videos/video.d.ts | 23 ++ 28 files changed, 1893 insertions(+), 71 deletions(-) create mode 100644 src/components/tag-pro/TagPro.tsx create mode 100644 src/components/tag-pro/index.ts create mode 100644 src/locales/lang/pages/videos/list/zh-CN.ts create mode 100644 src/pages/cms/category/components/CategoryTree.tsx create mode 100644 src/pages/cms/category/components/TreeNodeRender.tsx create mode 100644 src/pages/cms/category/index.tsx create mode 100644 src/pages/cms/category/style.ts create mode 100644 src/pages/videos/category/components/CategoryTree.tsx create mode 100644 src/pages/videos/category/components/TreeNodeRender.tsx create mode 100644 src/pages/videos/category/index.tsx create mode 100644 src/pages/videos/category/style.ts create mode 100644 src/pages/videos/list/index.tsx create mode 100644 src/service/videos.ts create mode 100644 src/store/cms/category.ts create mode 100644 src/store/videos/category.ts create mode 100644 src/store/videos/video.ts create mode 100644 src/types/cms/category.d.ts create mode 100644 src/types/videos/category.d.ts create mode 100644 src/types/videos/video.d.ts diff --git a/src/components/switch/index.tsx b/src/components/switch/index.tsx index 97dcddf..904d6f8 100644 --- a/src/components/switch/index.tsx +++ b/src/components/switch/index.tsx @@ -3,7 +3,7 @@ import { Switch as AntSwitch, SwitchProps } from 'antd' export const Switch = ({ value, ...props }: Omit & { - value: any + value?: any }) => { return ( diff --git a/src/components/tag-pro/TagPro.tsx b/src/components/tag-pro/TagPro.tsx new file mode 100644 index 0000000..f76b291 --- /dev/null +++ b/src/components/tag-pro/TagPro.tsx @@ -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 ( + + + + { + setValue(e.target.value) + onChange?.(e.target.value) + }}/> + + { + tags?.map(item => { + return { + handleChange(checked, item) + }} + > + {item} + + }) + } + + + + ) +} + +export default TagPro \ No newline at end of file diff --git a/src/components/tag-pro/index.ts b/src/components/tag-pro/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/locales/lang/pages/cms/video/zh-CN.ts b/src/locales/lang/pages/cms/video/zh-CN.ts index 7c9ee1a..aeacfe2 100644 --- a/src/locales/lang/pages/cms/video/zh-CN.ts +++ b/src/locales/lang/pages/cms/video/zh-CN.ts @@ -17,7 +17,7 @@ export default { collect_id: '站点id', type_id: '类型', title: '标题', - title_sub: '副标', + title_sub: '又名', letter: '首字母', tag: 'TAG', lock: '锁定后显示', diff --git a/src/locales/lang/pages/cms/videoCloud/zh-CN.ts b/src/locales/lang/pages/cms/videoCloud/zh-CN.ts index 4ba67bd..995e40f 100644 --- a/src/locales/lang/pages/cms/videoCloud/zh-CN.ts +++ b/src/locales/lang/pages/cms/videoCloud/zh-CN.ts @@ -16,7 +16,7 @@ export default { collect_id: '站点id', type_id: '类型', title: '标题', - title_sub: '副标', + title_sub: '又名', letter: '首字母', tag: 'TAG', lock: '锁定后显示', diff --git a/src/locales/lang/pages/cms/videoMagnet/zh-CN.ts b/src/locales/lang/pages/cms/videoMagnet/zh-CN.ts index f548e8e..6eb4f0c 100644 --- a/src/locales/lang/pages/cms/videoMagnet/zh-CN.ts +++ b/src/locales/lang/pages/cms/videoMagnet/zh-CN.ts @@ -16,7 +16,7 @@ export default { collect_id: '站点id', type_id: '类型', title: '标题', - title_sub: '副标', + title_sub: '又名', letter: '首字母', tag: 'TAG', lock: '锁定后显示', diff --git a/src/locales/lang/pages/videos/list/zh-CN.ts b/src/locales/lang/pages/videos/list/zh-CN.ts new file mode 100644 index 0000000..f76eba0 --- /dev/null +++ b/src/locales/lang/pages/videos/list/zh-CN.ts @@ -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: '更新时间' + + } +} \ No newline at end of file diff --git a/src/locales/lang/zh-CN.ts b/src/locales/lang/zh-CN.ts index 0230ffe..0d63291 100644 --- a/src/locales/lang/zh-CN.ts +++ b/src/locales/lang/zh-CN.ts @@ -6,6 +6,7 @@ import collect from './pages/cms/collect/zh-CN.ts' import video from './pages/cms/video/zh-CN.ts' import videoCloud from './pages/cms/videoCloud/zh-CN.ts' import videoMagnet from './pages/cms/videoMagnet/zh-CN.ts' +import list from './pages/videos/list/zh-CN.ts' export default { ...antdZh, @@ -53,6 +54,9 @@ export default { cms: { collect, video, videoCloud, videoMagnet, }, + videos:{ + list, + }, actions: { news: '新增', add: '添加', diff --git a/src/pages/cms/category/components/CategoryTree.tsx b/src/pages/cms/category/components/CategoryTree.tsx new file mode 100644 index 0000000..d59589e --- /dev/null +++ b/src/pages/cms/category/components/CategoryTree.tsx @@ -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 + } + + return (<> + + { + renderEmpty() + } + { + return () + }} + 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} + /> + + + ) +} + +export default CategoryTree \ No newline at end of file diff --git a/src/pages/cms/category/components/TreeNodeRender.tsx b/src/pages/cms/category/components/TreeNodeRender.tsx new file mode 100644 index 0000000..15adb43 --- /dev/null +++ b/src/pages/cms/category/components/TreeNodeRender.tsx @@ -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 ( +
+ {name as any} + + + {/*} + 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) + + }}/>*/} + { + mutate([ (node as any).id ]) + }} + > + { + e.stopPropagation() + e.stopPropagation() + }}/> + + + +
+ ) +}) \ No newline at end of file diff --git a/src/pages/cms/category/index.tsx b/src/pages/cms/category/index.tsx new file mode 100644 index 0000000..cecdfcd --- /dev/null +++ b/src/pages/cms/category/index.tsx @@ -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() + 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 ( + + + + + +
+ + +
+ } + > + + + } + > + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ ) +} + +export default Category \ No newline at end of file diff --git a/src/pages/cms/category/style.ts b/src/pages/cms/category/style.ts new file mode 100644 index 0000000..b033703 --- /dev/null +++ b/src/pages/cms/category/style.ts @@ -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 + } +}) \ No newline at end of file diff --git a/src/pages/cms/video/index.tsx b/src/pages/cms/video/index.tsx index e51697a..2f4dec0 100644 --- a/src/pages/cms/video/index.tsx +++ b/src/pages/cms/video/index.tsx @@ -10,6 +10,9 @@ 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 TagPro from '@/components/tag-pro/TagPro.tsx' +import { useSetAtom } from 'jotai/index' +import { categoriesAtom, categoryByIdAtom, categoryIdAtom } from '@/store/cms/category.ts' const i18nPrefix = 'cms.video' @@ -22,6 +25,9 @@ const Video = () => { 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: categories, isLoading: isCateLoading } = useAtomValue(categoriesAtom) + const { data: category, isLoading: isCategoryFetching } = useAtomValue(categoryByIdAtom) const [ open, setOpen ] = useState(false) const columns = useMemo(() => { @@ -34,6 +40,15 @@ const Video = () => { formItemProps: { hidden: true } }, { + 'title': t(`${i18nPrefix}.columns.collect_id`, 'CollectId'), + 'dataIndex': 'collect_id', + hideInTable: true, + hideInSearch: true, + hideInSetting: true, + formItemProps: { hidden: true }, + }, + + { 'title': t(`${i18nPrefix}.columns.title`, 'Title'), 'dataIndex': 'title', onHeaderCell: () => { @@ -41,6 +56,9 @@ const Video = () => { width: 200, } }, + colProps: { + span: 8 + } }, { 'title': t(`${i18nPrefix}.columns.title_sub`, 'TitleSub'), @@ -50,25 +68,9 @@ const Video = () => { width: 200, } }, - }, - { - 'title': t(`${i18nPrefix}.columns.source_url`, 'SourceUrl'), - 'dataIndex': 'source_url', - ellipsis: true, - copyable: true, - onHeaderCell: () => { - return { - width: 200, - } - }, - }, - { - 'title': t(`${i18nPrefix}.columns.collect_id`, 'CollectId'), - 'dataIndex': 'collect_id', - hideInTable: true, - hideInSearch: true, - hideInSetting: true, - formItemProps: { hidden: true }, + colProps: { + span: 8 + } }, { 'title': t(`${i18nPrefix}.columns.type_id`, 'TypeId'), @@ -80,16 +82,16 @@ const Video = () => { render: (_dom, record) => { return t(`${i18nPrefix}.type_id.${record.type_id}`) }, + colProps: { + span: 8 + } + }, { - 'title': t(`${i18nPrefix}.columns.letter`, 'Letter'), - 'dataIndex': 'letter' - }, - { - 'title': t(`${i18nPrefix}.columns.tag`, 'Tag'), - 'dataIndex': 'tag', - valueType: 'textarea', + 'title': t(`${i18nPrefix}.columns.source_url`, 'SourceUrl'), + 'dataIndex': 'source_url', ellipsis: true, + copyable: true, onHeaderCell: () => { return { width: 200, @@ -97,12 +99,22 @@ const Video = () => { }, }, { + 'title': t(`${i18nPrefix}.columns.letter`, 'Letter'), + 'dataIndex': 'letter', + colProps: { + span: 4 + } + }, + { 'title': t(`${i18nPrefix}.columns.lock`, 'Lock'), 'dataIndex': 'lock', valueType: 'switch', render: (_dom, record) => { return }, + colProps: { + span: 4 + } }, { 'title': t(`${i18nPrefix}.columns.copyright`, 'Copyright'), @@ -111,6 +123,9 @@ const Video = () => { render: (_dom, record) => { return }, + colProps: { + span: 4 + } }, { 'title': t(`${i18nPrefix}.columns.is_end`, 'IsEnd'), @@ -119,6 +134,9 @@ const Video = () => { render: (_dom, record) => { return }, + colProps: { + span: 4 + } }, { 'title': t(`${i18nPrefix}.columns.status`, 'Status'), @@ -127,21 +145,12 @@ const Video = () => { render: (_dom, record) => { return }, + colProps: { + span: 4 + } }, { - 'title': t(`${i18nPrefix}.columns.category_id`, 'CategoryId'), - 'dataIndex': 'category_id', - - }, - { - 'title': t(`${i18nPrefix}.columns.pic`, 'Pic'), - 'dataIndex': 'pic', - render: (_dom, record) => { - return - }, - }, - { 'title': t(`${i18nPrefix}.columns.pic_local`, 'PicLocal'), 'dataIndex': 'pic_local', hideInSearch: true, @@ -156,6 +165,32 @@ const Video = () => { render: (_dom, record) => { return }, + colProps: { + span: 4 + } + }, + { + 'title': t(`${i18nPrefix}.columns.pic`, 'Pic'), + 'dataIndex': 'pic', + render: (_dom, record) => { + return + }, + colProps: { + span: 20 + } + }, + { + 'title': t(`${i18nPrefix}.columns.category_id`, 'CategoryId'), + 'dataIndex': 'category_id', + valueType: 'select', + fieldProps: { + loading: isCategoryFetching, + options: categories?.rows ?? [], + fieldNames:{ + label: 'name', + value: 'id' + } + }, }, { 'title': t(`${i18nPrefix}.columns.actor`, 'Actor'), @@ -183,20 +218,54 @@ const Video = () => { { 'title': t(`${i18nPrefix}.columns.pubdate`, 'Pubdate'), 'dataIndex': 'pubdate', - valueType: 'dateTime' + valueType: 'dateTime', + colProps: { + span: 4 + } }, { 'title': t(`${i18nPrefix}.columns.total`, 'Total'), 'dataIndex': 'total', - valueType: 'digit' + 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 + } }, { 'title': t(`${i18nPrefix}.columns.area`, 'Area'), @@ -207,6 +276,9 @@ const Video = () => { width: 200, } }, + renderFormItem: (schema, config) => { + return + } }, { 'title': t(`${i18nPrefix}.columns.lang`, 'Lang'), @@ -217,23 +289,32 @@ const Video = () => { width: 200, } }, + renderFormItem: (schema, config) => { + return + } }, { 'title': t(`${i18nPrefix}.columns.version`, 'Version'), - 'dataIndex': 'version' - }, - { - 'title': t(`${i18nPrefix}.columns.year`, 'Year'), - 'dataIndex': 'year', - valueType: 'dateYear' + 'dataIndex': 'version', + + renderFormItem: (schema, config) => { + return + } }, { 'title': t(`${i18nPrefix}.columns.state`, 'State'), - 'dataIndex': 'state' + 'dataIndex': 'state', + renderFormItem: (schema, config) => { + return + } }, { 'title': t(`${i18nPrefix}.columns.douban_score`, 'DoubanScore'), - 'dataIndex': 'douban_score' + 'dataIndex': 'douban_score', + hideInSearch: true, + hideInSetting: true, + formItemProps: { hidden: true }, + hideInTable: true, }, { 'title': t(`${i18nPrefix}.columns.douban_id`, 'DoubanId'), @@ -245,7 +326,11 @@ const Video = () => { }, { 'title': t(`${i18nPrefix}.columns.imdb_score`, 'ImdbScore'), - 'dataIndex': 'imdb_score' + 'dataIndex': 'imdb_score', + hideInSearch: true, + hideInSetting: true, + formItemProps: { hidden: true }, + hideInTable: true, }, { 'title': t(`${i18nPrefix}.columns.imdb_id`, 'ImdbId'), @@ -274,6 +359,7 @@ const Video = () => { { + setCategoryId(record.category_id) form.setFieldsValue(record) setOpen(true) }}>{t('actions.edit')}, @@ -291,7 +377,7 @@ const Video = () => { ] } ] as ProColumns[] - }, [ isDeleting ]) + }, [ isDeleting, category, isCategoryFetching, categories, isCateLoading ]) useEffect(() => { if (isSuccess) { @@ -353,18 +439,18 @@ const Video = () => { } }) }, - }} /> { setOpen(open) }} loading={isSubmitting} + onValuesChange={(values)=>{ + if(values.category_id) { + setCategoryId(values.category_id) + } + }} onFinish={async (values) => { // console.log('values', values) saveOrUpdate(values) diff --git a/src/pages/videos/category/components/CategoryTree.tsx b/src/pages/videos/category/components/CategoryTree.tsx new file mode 100644 index 0000000..16c66ec --- /dev/null +++ b/src/pages/videos/category/components/CategoryTree.tsx @@ -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 + } + + return (<> + + { + renderEmpty() + } + { + return () + }} + 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} + /> + + + ) +} + +export default CategoryTree \ No newline at end of file diff --git a/src/pages/videos/category/components/TreeNodeRender.tsx b/src/pages/videos/category/components/TreeNodeRender.tsx new file mode 100644 index 0000000..0f9be0b --- /dev/null +++ b/src/pages/videos/category/components/TreeNodeRender.tsx @@ -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 ( +
+ {name as any} + + + {/*} + 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) + + }}/>*/} + { + mutate([ (node as any).id ]) + }} + > + { + e.stopPropagation() + e.stopPropagation() + }}/> + + + +
+ ) +}) \ No newline at end of file diff --git a/src/pages/videos/category/index.tsx b/src/pages/videos/category/index.tsx new file mode 100644 index 0000000..231db3b --- /dev/null +++ b/src/pages/videos/category/index.tsx @@ -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() + 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 ( + + + + + +
+ + +
+ } + > + + + } + > + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ ) +} + +export default Category \ No newline at end of file diff --git a/src/pages/videos/category/style.ts b/src/pages/videos/category/style.ts new file mode 100644 index 0000000..b033703 --- /dev/null +++ b/src/pages/videos/category/style.ts @@ -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 + } +}) \ No newline at end of file diff --git a/src/pages/videos/list/index.tsx b/src/pages/videos/list/index.tsx new file mode 100644 index 0000000..a60eb6f --- /dev/null +++ b/src/pages/videos/list/index.tsx @@ -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 + } + }, + { + 'title': t(`${i18nPrefix}.columns.area`, 'Area'), + 'dataIndex': 'area', + ellipsis: true, + onHeaderCell: () => { + return { + width: 200, + } + }, + renderFormItem: (schema, config) => { + return + } + }, + { + 'title': t(`${i18nPrefix}.columns.lang`, 'Lang'), + 'dataIndex': 'lang', + ellipsis: true, + onHeaderCell: () => { + return { + width: 200, + } + }, + renderFormItem: (schema, config) => { + return + } + }, + { + 'title': t(`${i18nPrefix}.columns.version`, 'Version'), + 'dataIndex': 'version', + + renderFormItem: (schema, config) => { + return + } + }, + { + 'title': t(`${i18nPrefix}.columns.state`, 'State'), + 'dataIndex': 'state', + renderFormItem: (schema, config) => { + return + } + }, + { + '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) => [ + { + setCategoryId(record.type_id) + form.setFieldsValue(record) + setOpen(true) + }}>{t('actions.edit')}, + { + deleteVideo([ record.id ]) + }} + title={t('message.deleteConfirm')}> + + {t('actions.delete', '删除')} + + + ] + } + ] as ProColumns[] + }, [ isDeleting, category, isCategoryFetching ]) + + useEffect(() => { + if (isSuccess) { + setOpen(false) + } + }, [ isSuccess ]) + + return ( + + { + setSearch(prev => ({ + ...prev, + key: value + })) + }, + allowClear: true, + placeholder: t(`${i18nPrefix}.placeholder`, '输入视频名称') + }, + actions: [ + + ] + }} + 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, + } + }) + }, + }} + /> + { + 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[]}/> + + ) +} + +export default Video \ No newline at end of file diff --git a/src/service/base.ts b/src/service/base.ts index ea27b76..e25ace4 100644 --- a/src/service/base.ts +++ b/src/service/base.ts @@ -21,7 +21,7 @@ export const createCURD = (api: string, options?: AxiosRequest return request.post(`${api}/deletes`, { ids }, options) }, info: (id: number) => { - return request.get(`${api}/${id}`, options) + return request.post(`${api}/get`, { id }, options) } } diff --git a/src/service/cms.ts b/src/service/cms.ts index 783138a..9e2241b 100644 --- a/src/service/cms.ts +++ b/src/service/cms.ts @@ -14,6 +14,9 @@ const cmsServ = { videoMagnet: { ...createCURD('/cms/video_magnet') }, + category: { + ...createCURD('/cms/category') + }, } export default cmsServ \ No newline at end of file diff --git a/src/service/videos.ts b/src/service/videos.ts new file mode 100644 index 0000000..a655fcf --- /dev/null +++ b/src/service/videos.ts @@ -0,0 +1,13 @@ +import { createCURD } from '@/service/base.ts' +import { Videos } from '@/types' + +const videoServ = { + category: { + ...createCURD('/videos/category') + }, + video: { + ...createCURD('/videos/video') + } +} + +export default videoServ \ No newline at end of file diff --git a/src/store/cms/category.ts b/src/store/cms/category.ts new file mode 100644 index 0000000..ac5efec --- /dev/null +++ b/src/store/cms/category.ts @@ -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([]) + +export const categoryAtom = atom(undefined as unknown as Cms.ICategory) + +export const categorySearchAtom = atom({ + key: '' +} as SearchParams) + +export const categoryPageAtom = atom({ + 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((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 + } + } +}) diff --git a/src/store/videos/category.ts b/src/store/videos/category.ts new file mode 100644 index 0000000..dfa3f1a --- /dev/null +++ b/src/store/videos/category.ts @@ -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([]) + +export const categoryAtom = atom(undefined as unknown as Videos.ICategory) + +export const categorySearchAtom = atom({ + key: '' +} as SearchParams) + +export const categoryPageAtom = atom({ + 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((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 + } + } +}) diff --git a/src/store/videos/video.ts b/src/store/videos/video.ts new file mode 100644 index 0000000..5a45cd9 --- /dev/null +++ b/src/store/videos/video.ts @@ -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([]) + +export const videoAtom = atom(undefined as unknown as Videos.IVideo) + +export const videoSearchAtom = atom({ + key: '' +} as SearchParams) + +export const videoPageAtom = atom({ + 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((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 + } + } +}) diff --git a/src/types/cms/category.d.ts b/src/types/cms/category.d.ts new file mode 100644 index 0000000..5553ea9 --- /dev/null +++ b/src/types/cms/category.d.ts @@ -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; +} \ No newline at end of file diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 050dcff..6ed44eb 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -1,21 +1,27 @@ 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 { 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 { 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' } \ No newline at end of file diff --git a/src/types/videos/category.d.ts b/src/types/videos/category.d.ts new file mode 100644 index 0000000..5553ea9 --- /dev/null +++ b/src/types/videos/category.d.ts @@ -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; +} \ No newline at end of file diff --git a/src/types/videos/video.d.ts b/src/types/videos/video.d.ts new file mode 100644 index 0000000..e5f7e01 --- /dev/null +++ b/src/types/videos/video.d.ts @@ -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; + +} \ No newline at end of file