diff --git a/src/components/tag-value/TagValue.tsx b/src/components/tag-value/TagValue.tsx new file mode 100644 index 0000000..2fd5dd8 --- /dev/null +++ b/src/components/tag-value/TagValue.tsx @@ -0,0 +1,83 @@ +import { Space, Tag, TagProps } from 'antd' +import { memo, useEffect, useState } from 'react' + +type ValueType = string | number | boolean | { label: any, value: string | number | boolean } + +export interface TagValueProps extends Omit { + tags?: ValueType[]; + value?: ValueType | ValueType[]; + onChange?: (value: ValueType | ValueType[]) => void; + checked?: boolean; + wrap?: boolean; + //单选 + single?: boolean; +} + +const parserValue = (value?: ValueType | ValueType[]) => { + if (value === undefined || value === null) { + return [] + } + if (Array.isArray(value)) { + return value + } + return [ value ] +} + +const TagValue = ( + { + value, onChange, checked = true, tags = [], wrap, single, + }: TagValueProps) => { + + const Com = checked ? Tag.CheckableTag : Tag + const [ innerValue, setValue ] = useState(() => { + return parserValue(value).filter(item => { + return tags?.some(t => { + return ((t as any).value ?? t) === item + }) + }) + }) + + useEffect(() => { + const val = parserValue(value).filter(item => { + return tags?.some(t => { + return ((t as any).value ?? t) === item + }) + }) + setValue(val) + }, [ value ]) + + return ( + + {tags?.map(item => { + if (item == null || item === '' || item === undefined) return null + const val = (item as any).value ?? item + const selected = innerValue.includes(val) + return ( + { + const prevValue = parserValue(value) + const index = prevValue?.indexOf(val) + let newArr: ValueType[] = [] + if (single) { + newArr = index === -1 ? [ val ] : [] + } else { + if (index === -1) { + newArr = [ ...prevValue, val ] + } else { + const newArray = [ ...prevValue ] + newArray.splice(index, 1) + newArr = newArray + } + } + onChange?.(newArr) + setValue(newArr) + } : undefined} + checked={selected}> + {(item as any).label ?? item} + ) + })} + + ) +} + +export default memo(TagValue) \ No newline at end of file diff --git a/src/components/tag-value/index.ts b/src/components/tag-value/index.ts new file mode 100644 index 0000000..90a1b59 --- /dev/null +++ b/src/components/tag-value/index.ts @@ -0,0 +1 @@ +export * from './TagValue.tsx' \ No newline at end of file diff --git a/src/components/user-picker/List.tsx b/src/components/user-picker/List.tsx index 429a58a..514f707 100644 --- a/src/components/user-picker/List.tsx +++ b/src/components/user-picker/List.tsx @@ -1,7 +1,7 @@ import { useStyle } from './style.ts' import { Avatar, Button, Checkbox, CheckboxProps, Radio } from 'antd' import { createContext, useContext } from 'react' -import { IUser } from '@/types' +import { System } from '@/types' import { DeleteOutlined } from '@ant-design/icons' export const ListItem = (props: CheckboxProps) => { @@ -24,7 +24,7 @@ export const ListItem = (props: CheckboxProps) => { ) } -export const ListViewItem = ({ user, onDel }: { user: IUser, onDel: (user: IUser) => void }) => { +export const ListViewItem = ({ user, onDel }: { user: System.IUser, onDel: (user: System.IUser) => void }) => { const { styles, cx, theme } = useStyle() if (!user) { return null diff --git a/src/components/user-picker/UserPicker.tsx b/src/components/user-picker/UserPicker.tsx index 2853679..1c43932 100644 --- a/src/components/user-picker/UserPicker.tsx +++ b/src/components/user-picker/UserPicker.tsx @@ -8,7 +8,7 @@ import DepartmentTree from '@/components/department-tree/DepartmentTree.tsx' import { DraggablePanel } from '@/components/draggable-panel' import { useAtom, useAtomValue } from 'jotai' import { userListAtom, userSearchAtom, } from '@/store/system/user.ts' -import { IUser } from '@/types' +import { System } from '@/types' import EmptyWrap from '@/components/empty/EmptyWrap.tsx' export interface UserSelectProps extends SelectProps { @@ -62,7 +62,7 @@ const UserModel = memo(({ multiple, children, value, onChange, ...props }: UserM const [ , setSearch ] = useAtom(userSearchAtom) const { data: users, isPending } = useAtomValue(userListAtom) const [ open, setOpen ] = useState(false) - const selectUserRef = useRef([]) + const selectUserRef = useRef([]) const [ , update ] = useState({}) useEffect(() => { diff --git a/src/pages/videos/list/index.tsx b/src/pages/videos/list/index.tsx index 54eebd7..ff98510 100644 --- a/src/pages/videos/list/index.tsx +++ b/src/pages/videos/list/index.tsx @@ -1,11 +1,11 @@ import { useTranslation } from '@/i18n.ts' import { getToken } from '@/store/system.ts' -import { Button, DatePicker, Form, Image, Popconfirm } from 'antd' +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, videosAtom, videoSearchAtom, videoTypes + saveOrUpdateVideoAtom, videoAtom, videosAtom, videoSearchAtom, videoTypes } from '@/store/videos/video.ts' import { useEffect, useMemo, useState } from 'react' import Action from '@/components/action/Action.tsx' @@ -19,21 +19,28 @@ import { 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 } = useStyle() + 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 columns = useMemo(() => { return [ @@ -84,7 +91,25 @@ const Video = () => { style: { width: '100%' } }, render: (_dom, record) => { - return t(`${i18nPrefix}.type_id.${record.type_id}`) + return { + 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 @@ -110,16 +135,38 @@ const Video = () => { }, valueType: 'dateYear', colProps: { - span: 4 + span: 6 }, render: (_dom, record) => { - return record.year + return { + 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 } @@ -131,9 +178,24 @@ const Video = () => { fieldProps: { style: { width: '100%' } }, - colProps: { - span: 12 + span: 10 + }, + render: (_dom, record) => { + return { + setSearch((prev: any) => { + return { + ...prev, + class_name: values, + } + }) + setFilterOpen(true) + }} + /> }, }, { @@ -142,6 +204,21 @@ const Video = () => { hideInTable: true, colProps: { span: 6 + }, render: (_dom, record) => { + return { + setSearch((prev: any) => { + return { + ...prev, + douban_id: values[0], + } + }) + setFilterOpen(true) + }} + /> }, }, { @@ -150,6 +227,21 @@ const Video = () => { hideInTable: true, colProps: { span: 6 + }, render: (_dom, record) => { + return { + setSearch((prev: any) => { + return { + ...prev, + imdb_id: values[0], + } + }) + setFilterOpen(true) + }} + /> }, }, { @@ -158,6 +250,21 @@ const Video = () => { hideInTable: true, colProps: { span: 6 + }, render: (_dom, record) => { + return { + setSearch((prev: any) => { + return { + ...prev, + rt_id: values[0], + } + }) + setFilterOpen(true) + }} + /> }, }, { @@ -166,13 +273,28 @@ const Video = () => { hideInTable: true, colProps: { span: 6 + }, render: (_dom, record) => { + return { + setSearch((prev: any) => { + return { + ...prev, + mal_id: values[0], + } + }) + setFilterOpen(true) + }} + /> }, }, { 'title': t(`${i18nPrefix}.columns.img`, '封面'), 'dataIndex': 'pic', hideInSearch: true, - hideInTable: true, + width: 80, colProps: { span: 24 }, @@ -189,10 +311,14 @@ const Video = () => { fileList={[]} action="/api/v1/videos/image/upload" /> - } + }, + render: (_dom, record) => { + const url = `/api/v1/videos/image/${record.video_id}` + return cover + }, }, - - { 'title': t(`${i18nPrefix}.columns.actor`, 'Actor'), 'dataIndex': 'actor', @@ -201,7 +327,22 @@ const Video = () => { fieldProps: { style: { width: '100%' } }, - + render: (_dom, record) => { + return { + setSearch((prev: any) => { + return { + ...prev, + actor: values, + } + }) + setFilterOpen(true) + }} + /> + }, }, { 'title': t(`${i18nPrefix}.columns.director`, 'Director'), @@ -210,6 +351,22 @@ const Video = () => { fieldProps: { style: { width: '100%' } }, + render: (_dom, record) => { + return { + setSearch(prev => { + return { + ...prev, + director: (values as Array) + } + }) + setFilterOpen(true) + }} + /> + }, }, { 'title': t(`${i18nPrefix}.columns.writer`, 'Writer'), @@ -218,6 +375,21 @@ const Video = () => { fieldProps: { style: { width: '100%' } }, + render: (_dom, record) => { + return { + setSearch(prev => { + return { + ...prev, + writer: (values as Array) + } + }) + setFilterOpen(true) + }}/> + }, }, { 'title': t(`${i18nPrefix}.columns.content`, 'Content'), @@ -247,7 +419,22 @@ const Video = () => { renderFormItem: (schema, config) => { return - } + }, + render: (_dom, record) => { + return { + setSearch(prev => { + return { + ...prev, + tag: (values as Array) + } + }) + setFilterOpen(true) + }}/> + }, }, { 'title': t(`${i18nPrefix}.columns.area`, 'Area'), @@ -260,7 +447,23 @@ const Video = () => { renderFormItem: (schema, config) => { return - } + }, + render: (_dom, record) => { + return { + setSearch(prev => { + return { + ...prev, + area: (values as Array) + } + }) + setFilterOpen(true) + }} + /> + }, }, { 'title': t(`${i18nPrefix}.columns.lang`, 'Lang'), @@ -273,7 +476,22 @@ const Video = () => { renderFormItem: (schema, config) => { return - } + }, + render: (_dom, record) => { + return { + setSearch(prev => { + return { + ...prev, + lang: (values as Array) + } + }) + setFilterOpen(true) + }}/> + }, }, /*{ 'title': t(`${i18nPrefix}.columns.version`, 'Version'), @@ -309,8 +527,6 @@ const Video = () => { formItemProps: { hidden: true }, hideInTable: true, }, - - { title: t(`${i18nPrefix}.columns.option`, '操作'), key: 'option', @@ -338,7 +554,13 @@ const Video = () => { ] } ] as ProColumns[] - }, [ isDeleting, category, isCategoryFetching ]) + }, [ isDeleting, category, isCategoryFetching, currentVideo, search ]) + + useEffect(() => { + + filterForm.setFieldsValue(search) + + }, [ search ]) useEffect(() => { if (isSuccess) { @@ -347,7 +569,7 @@ const Video = () => { }, [ isSuccess ]) return ( - + { placeholder: t(`${i18nPrefix}.placeholder`, '输入视频名称') }, actions: [ - + + + + ) + }, + + }} + 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(',') + } + }) + + if (Object.keys(values).length === 0) { + setSearch({}) + } else { + setSearch(prev => { + return { + ...prev, + ...values + } + }) + } + }} + columns={columns.filter(item => !item.hideInSearch) as ProFormColumnsType[]}/> ) } diff --git a/src/pages/videos/list/style.ts b/src/pages/videos/list/style.ts new file mode 100644 index 0000000..683c09e --- /dev/null +++ b/src/pages/videos/list/style.ts @@ -0,0 +1,23 @@ +import { createStyles } from '@/theme' + +export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { + const prefix = `${prefixCls}-${token?.proPrefix}-video-list-page` + + const container = css` + .ant-table-cell{ + .ant-tag{ + padding-inline: 3px; + margin-inline-end: 3px; + } + } + .ant-table-empty { + .ant-table-body{ + height: calc(100vh - 350px) + } + } + ` + + return { + container: cx(prefix, props?.className, container), + } +}) \ No newline at end of file diff --git a/src/patches/x-columns.ts b/src/patches/x-columns.ts new file mode 100644 index 0000000..a4264da --- /dev/null +++ b/src/patches/x-columns.ts @@ -0,0 +1,8 @@ +export const useColumnsWidth = (width: number, formWith?: number | string) => ({ + width, + fieldProps: { + style: { + width: `${formWith ?? '100%'}` + }, + } +}) \ No newline at end of file diff --git a/src/store/videos/video.ts b/src/store/videos/video.ts index 5a45cd9..ac06599 100644 --- a/src/store/videos/video.ts +++ b/src/store/videos/video.ts @@ -17,7 +17,7 @@ export const getTypeName = (typeId: number) => { return videoTypes.find(item => item.value === typeId)?.label } -type SearchParams = IPage & { +type SearchParams = IPage & Partial & { key?: string } @@ -25,6 +25,7 @@ export const videoIdAtom = atom(0) export const videoIdsAtom = atom([]) +//选中的行 export const videoAtom = atom(undefined as unknown as Videos.IVideo) export const videoSearchAtom = atom({ @@ -40,7 +41,18 @@ export const videosAtom = atomWithQuery((get) => { return { queryKey: [ 'videos', get(videoSearchAtom) ], queryFn: async ({ queryKey: [ , params ] }) => { - return await videoServ.video.list(params as SearchParams) + + //处理数组,转成,分隔的字符串 + const p = {} as SearchParams + Object.keys(params as any).forEach(key=>{ + const value =(params as any)[key] + if (Array.isArray(value)) { + p[key] = value.join(',') + } else { + p[key] = value + } + }) + return await videoServ.video.list(p) }, select: res => { const data = res.data diff --git a/src/utils/index.ts b/src/utils/index.ts index 954ceb7..12f2251 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -112,7 +112,7 @@ export const convertToBool = (value: any): boolean => { // 处理常见 falsy 值 if (value === undefined || value === null || - value === false || value === 0 || value === '' || Number.isNaN(value)) { + value === false || value === 0 || value === '' || Number.isNaN(value)) { return false } @@ -123,4 +123,21 @@ export const convertToBool = (value: any): boolean => { // 其他情况,包括数字(非零)、字符串(已经被上述逻辑处理)和其他 truthy 值 return Boolean(value) +} + +//数组去重 +export const unique = (arr: any[]): any[] => { + return Array.from(new Set(arr)) +} + +export const getValueCount = (obj: any, filterObj: any = {}) => { + // 获取对象中所有值的数量 + let count = 0 + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key) && obj[key]) { + if (!filterObj?.[key]) + count++ + } + } + return count } \ No newline at end of file