You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

776 lines
26 KiB

import { useTranslation } from '@/i18n.ts'
import { getToken } from '@/store/system.ts'
import { Button, DatePicker, Form, Image, Popconfirm, Divider, Space, Tooltip, Badge } from 'antd'
import dayjs from 'dayjs'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
import {
deleteVideoAtom, getTypeName,
saveOrUpdateVideoAtom, videoAtom, videosAtom, videoSearchAtom, videoTypes
} from '@/store/videos/video.ts'
import { useEffect, useMemo, useState } from 'react'
import Action from '@/components/action/Action.tsx'
import {
BetaSchemaForm,
ProColumns,
ProFormColumnsType,
ProFormUploadButton,
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'
import TagValue from '@/components/tag-value/TagValue.tsx'
import { useStyle } from './style'
import { FilterOutlined } from '@ant-design/icons'
import { getValueCount } from '@/utils'
const i18nPrefix = 'videos.list'
const Video = () => {
const { styles, cx } = useStyle()
const { t } = useTranslation()
const [ form ] = Form.useForm()
const [ filterForm ] = Form.useForm()
const { mutate: saveOrUpdate, isPending: isSubmitting, isSuccess } = useAtomValue(saveOrUpdateVideoAtom)
const [ search, setSearch ] = useAtom(videoSearchAtom)
const [ currentVideo, setVideo ] = useAtom(videoAtom)
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 [ openFilter, setFilterOpen ] = useState(false)
const [ searchKey, setSearchKey ] = useState(search?.title)
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',
width: 200,
fieldProps: {
style: { width: '100%' }
},
colProps: {
span: 8
},
render: (_text, record) => {
//高亮搜索关键字, 从search.title中获取
const title = record.title?.replace?.(new RegExp(`(${search?.title})`, 'ig'), '<span class="ant-pro-table-highlight">$1</span>')
return <span dangerouslySetInnerHTML={{ __html: title }}></span>
}
},
{
'title': t(`${i18nPrefix}.columns.title_sub`, 'TitleSub'),
'dataIndex': 'title_sub',
width: 200,
fieldProps: {
style: { width: '100%' }
},
colProps: {
span: 16
},
render: (_text, record) => {
//高亮搜索关键字, 从search.title中获取
const title = record.title_sub?.replace?.(new RegExp(`(${search?.title_sub})`, 'ig'), '<span class="ant-pro-table-highlight">$1</span>')
return <span dangerouslySetInnerHTML={{ __html: title }}></span>
}
},
{
'title': t(`${i18nPrefix}.columns.type_id`, 'TypeId'),
'dataIndex': 'type_id',
valueType: 'select',
width: 100,
fieldProps: {
options: videoTypes,
style: { width: '100%' }
},
render: (_dom, record) => {
return <TagValue
tags={[ { label: t(`${i18nPrefix}.type_id.${record.type_id}`), value: record.type_id } ]}
wrap={currentVideo?.id === record.id}
value={search?.type_id}
onChange={(values) => {
setSearch((prev: any) => {
return {
...prev,
type_id: values[0],
}
})
setCategoryId(values[0])
const typeName = getTypeName(values[0])
form.setFieldsValue({
class_name: typeName,
})
setFilterOpen(true)
}}
/>
},
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.year`, 'Year'),
'dataIndex': 'year',
width: 100,
fieldProps: {
style: { width: '100%' }
},
valueType: 'dateYear',
colProps: {
span: 6
},
render: (_dom, record) => {
return <TagValue
tags={[ record.year ]}
wrap={currentVideo?.id === record.id}
value={search?.year}
single={true}
onChange={(values) => {
setSearch((prev: any) => {
return {
...prev,
year: values[0],
}
})
setFilterOpen(true)
}}
/>
},
renderFormItem: (_schema, config) => {
const props = { ...config } as any
delete props.mode
const isForm = config.type === 'form'
let value = isForm && config.value ? dayjs().set('year', config.value) : undefined
if (config.value?.$isDayjsObject) {
value = config.value as dayjs.Dayjs
}
return <DatePicker
{..._schema.formItemProps}
{...props}
picker={'year'}
value={value}
/>
}
},
{
'title': t(`${i18nPrefix}.columns.category_id`, 'CategoryId'),
'dataIndex': 'class_name',
width: 100,
fieldProps: {
style: { width: '100%' }
},
colProps: {
span: 10
},
render: (_dom, record) => {
return <TagValue
tags={record.class_name?.split(',')}
wrap={currentVideo?.id === record.id}
value={search?.class_name}
onChange={(values) => {
setSearch((prev: any) => {
return {
...prev,
class_name: values,
}
})
setFilterOpen(true)
}}
/>
},
},
{
'title': t(`${i18nPrefix}.columns.douban_id`, 'DouBanId'),
'dataIndex': 'douban_id',
hideInTable: true,
colProps: {
span: 6
}, render: (_dom, record) => {
return <TagValue
tags={[ record.douban_id ]}
wrap={currentVideo?.id === record.id}
value={search?.douban_id}
onChange={(values) => {
setSearch((prev: any) => {
return {
...prev,
douban_id: values[0],
}
})
setFilterOpen(true)
}}
/>
},
},
{
'title': t(`${i18nPrefix}.columns.imdb_id`, 'ImdbId'),
'dataIndex': 'imdb_id',
hideInTable: true,
colProps: {
span: 6
}, render: (_dom, record) => {
return <TagValue
tags={[ record.imdb_id ]}
wrap={currentVideo?.id === record.id}
value={search?.imdb_id}
onChange={(values) => {
setSearch((prev: any) => {
return {
...prev,
imdb_id: values[0],
}
})
setFilterOpen(true)
}}
/>
},
},
{
'title': t(`${i18nPrefix}.columns.rt_id`, 'RtId'),
'dataIndex': 'rt_id',
hideInTable: true,
colProps: {
span: 6
}, render: (_dom, record) => {
return <TagValue
tags={[ record.rt_id ]}
wrap={currentVideo?.id === record.id}
value={search?.rt_id}
onChange={(values) => {
setSearch((prev: any) => {
return {
...prev,
rt_id: values[0],
}
})
setFilterOpen(true)
}}
/>
},
},
{
'title': t(`${i18nPrefix}.columns.mal_id`, 'MalId'),
'dataIndex': 'mal_id',
hideInTable: true,
colProps: {
span: 6
}, render: (_dom, record) => {
return <TagValue
tags={[ record.mal_id ]}
wrap={currentVideo?.id === record.id}
value={search?.mal_id}
onChange={(values) => {
setSearch((prev: any) => {
return {
...prev,
mal_id: values[0],
}
})
setFilterOpen(true)
}}
/>
},
},
{
'title': t(`${i18nPrefix}.columns.img`, '封面'),
'dataIndex': 'pic',
hideInSearch: true,
width: 80,
colProps: {
span: 24
},
renderFormItem: (_schema, _config, form) => {
return <ProFormUploadButton
fieldProps={{
name: 'file',
listType: 'picture-card',
data: { videoId: form.getFieldValue('video_id') },
headers: {
'Authorization': `Bearer ${getToken()}`
},
}}
fileList={[]}
action="/api/v1/videos/image/upload"
/>
},
render: (_dom, record) => {
const url = `/api/v1/videos/image/${record.video_id}`
return <Image src={`${url}?width=60&height=80`} alt="cover" preview={{
src: url,
}} style={{ width: 80 }}/>
},
},
{
'title': t(`${i18nPrefix}.columns.actor`, 'Actor'),
'dataIndex': 'actor',
ellipsis: true,
width: 200,
fieldProps: {
style: { width: '100%' }
},
render: (_dom, record) => {
return <TagValue
tags={record.actor?.split(',') ?? []}
wrap={currentVideo?.id === record.id}
value={search?.actor ?? []}
onChange={(values) => {
setSearch((prev: any) => {
return {
...prev,
actor: values,
}
})
setFilterOpen(true)
}}
/>
},
},
{
'title': t(`${i18nPrefix}.columns.director`, 'Director'),
'dataIndex': 'director',
width: 200,
fieldProps: {
style: { width: '100%' }
},
render: (_dom, record) => {
return <TagValue
tags={record.director?.split(',') ?? []}
wrap={!currentVideo?.id === record.id}
value={search?.director ?? []}
onChange={(values) => {
setSearch(prev => {
return {
...prev,
director: (values as Array<any>)
}
})
setFilterOpen(true)
}}
/>
},
},
{
'title': t(`${i18nPrefix}.columns.writer`, 'Writer'),
'dataIndex': 'writer',
width: 200,
fieldProps: {
style: { width: '100%' }
},
render: (_dom, record) => {
return <TagValue
tags={record.writer?.split(',') ?? []}
wrap={!currentVideo?.id === record.id}
value={search?.writer ?? []}
onChange={(values) => {
setSearch(prev => {
return {
...prev,
writer: (values as Array<any>)
}
})
setFilterOpen(true)
}}/>
},
},
{
'title': t(`${i18nPrefix}.columns.content`, 'Content'),
'dataIndex': 'content',
valueType: 'textarea',
ellipsis: true,
width: 200,
fieldProps: {
style: { width: '100%' }
},
},
// {
// 'title': t(`${i18nPrefix}.columns.remarks`, 'Remarks'),
// valueType: 'textarea',
// ellipsis: true,
// 'dataIndex': 'remarks'
// },
{
'title': t(`${i18nPrefix}.columns.tag`, 'Tag'),
'dataIndex': 'tag',
valueType: 'textarea',
ellipsis: true,
width: 200,
fieldProps: {
style: { width: '100%' }
},
renderFormItem: (schema, config) => {
return <TagPro loading={isCategoryFetching}
tags={category?.extend?.class?.split(',') ?? []} {...config} {...schema.fieldProps} />
},
render: (_dom, record) => {
return <TagValue
tags={record.tag?.split(',') ?? []}
wrap={!currentVideo?.id === record.id}
value={search?.tag ?? []}
onChange={(values) => {
setSearch(prev => {
return {
...prev,
tag: (values as Array<any>)
}
})
setFilterOpen(true)
}}/>
},
},
{
'title': t(`${i18nPrefix}.columns.area`, 'Area'),
'dataIndex': 'area',
ellipsis: true,
width: 200,
fieldProps: {
style: { width: '100%' }
},
renderFormItem: (schema, config) => {
return <TagPro loading={isCategoryFetching}
tags={category?.extend?.area?.split(',') ?? []} {...config} {...schema.fieldProps} />
},
render: (_dom, record) => {
return <TagValue
tags={record.area?.split(',') ?? []}
wrap={!currentVideo?.id === record.id}
value={search?.area ?? []}
onChange={(values) => {
setSearch(prev => {
return {
...prev,
area: (values as Array<any>)
}
})
setFilterOpen(true)
}}
/>
},
},
{
'title': t(`${i18nPrefix}.columns.lang`, 'Lang'),
'dataIndex': 'lang',
ellipsis: true,
width: 200,
fieldProps: {
style: { width: '100%' }
},
renderFormItem: (schema, config) => {
return <TagPro loading={isCategoryFetching}
tags={category?.extend?.lang?.split(',') ?? []} {...config} {...schema.fieldProps} />
},
render: (_dom, record) => {
return <TagValue
tags={record.lang?.split(',') ?? []}
wrap={!currentVideo?.id === record.id}
value={search?.lang ?? []}
onChange={(values) => {
setSearch(prev => {
return {
...prev,
lang: (values as Array<any>)
}
})
setFilterOpen(true)
}}/>
},
},
/*{
'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.imdb_score`, 'ImdbScore'),
'dataIndex': 'imdb_score',
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, currentVideo, search ])
useEffect(() => {
setSearchKey(search?.title)
filterForm.setFieldsValue(search)
}, [ search ])
useEffect(() => {
if (isSuccess) {
setOpen(false)
}
}, [ isSuccess ])
return (
<ListPageLayout className={styles.container}>
<ProTable
rowKey="id"
headerTitle={t(`${i18nPrefix}.title`, '视频管理')}
toolbar={{
search: {
loading: isFetching && !!search.title,
onSearch: (value: string) => {
setSearch(prev => ({
...prev,
title: value
}))
},
allowClear: true,
onChange: (e) => {
setSearchKey(e.target?.value)
},
value: searchKey,
placeholder: t(`${i18nPrefix}.placeholder`, '输入视频名称')
},
actions: [
<Tooltip key={'filter'} title={t(`${i18nPrefix}.filter.tooltip`, '高级查询')}>
<Badge count={getValueCount(search)}>
<Button
onClick={() => {
setFilterOpen(true)
}}
icon={<FilterOutlined/>} shape={'circle'} size={'small'}/>
</Badge>
</Tooltip>,
<Divider type={'vertical'} key={'divider'}/>,
<Button key={'add'}
onClick={() => {
form.resetFields()
form.setFieldsValue({
id: 0,
})
setOpen(true)
}}
type={'primary'}>{t(`${i18nPrefix}.add`, '添加')}</Button>
]
}}
scroll={{
x: 2500, y: 'calc(100vh - 290px)'
}}
search={false}
onRow={(record) => {
return {
className: cx({
'ant-table-row-selected': currentVideo?.id === record.id
}),
onClick: () => {
setVideo(record)
}
}
}}
dateFormatter="string"
loading={isLoading || isFetching}
dataSource={data?.rows ?? []}
columns={columns}
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[]}/>
<BetaSchemaForm
title={t(`${i18nPrefix}.filter.title`, '视频高级查询')}
grid={true}
shouldUpdate={false}
width={500}
form={filterForm}
open={openFilter}
onOpenChange={open => {
setFilterOpen(open)
}}
layout={'vertical'}
scrollToFirstError={true}
// colProps={{ span: 24 }}
// labelCol={{ span: 8 }}
// wrapperCol={{ span: 14 }}
layoutType={'DrawerForm'}
drawerProps={{
maskClosable: false,
mask: false,
}}
submitter={{
searchConfig: {
resetText: t(`${i18nPrefix}.filter.reset`, '清空'),
submitText: t(`${i18nPrefix}.filter.submit`, '查询'),
},
onReset: () => {
filterForm.resetFields()
},
render: (props,) => {
return (
<div style={{ textAlign: 'right' }}>
<Space>
<Button onClick={() => {
props.reset()
}}>{props.searchConfig?.resetText}</Button>
<Button type="primary"
onClick={() => {
props.submit()
}}
>{props.searchConfig?.submitText}</Button>
</Space>
</div>
)
},
}}
onValuesChange={(values) => {
if (values.type_id) {
setCategoryId(values.type_id)
const typeName = getTypeName(values.type_id)
filterForm.setFieldsValue({
class_name: typeName,
})
}
}}
onFinish={async (values) => {
// console.log('values', values)
//处理,变成数组
Object.keys(values).forEach(key => {
if (typeof values[key] === 'string' && values[key].includes(',')) {
values[key] = values[key].split(',')
}
})
setSearch(values)
}}
columns={columns.filter(item => !item.hideInSearch) as ProFormColumnsType[]}/>
</ListPageLayout>
)
}
export default Video