diff --git a/src/App.css b/src/App.css index 6261550..4414b48 100644 --- a/src/App.css +++ b/src/App.css @@ -21,4 +21,12 @@ .ant-drawer .ant-drawer-footer{ background-color: #fcfcfc; +} + +.hover{ + cursor: pointer; + &:hover{ + + color: #1c7ed6; + } } \ No newline at end of file diff --git a/src/components/dialog/index.tsx b/src/components/dialog/index.tsx new file mode 100644 index 0000000..cda84c2 --- /dev/null +++ b/src/components/dialog/index.tsx @@ -0,0 +1,95 @@ +import { Modal, ModalProps } from 'antd' +import { useStyle } from './style' +import { + useState, + forwardRef, + ReactNode, + useImperativeHandle, + useCallback, useRef +} from 'react' + +export { useDialog } from './useDialog' + +export interface DialogRef { + show: (data?: any) => void + close: () => void + + get data() +} + +export interface DialogProps extends Omit { + target?: ReactNode | JSX.Element + ref?: DialogRef + onCancel?: () => boolean | void + onOk?: (data?: any) => Promise | boolean | void +} + +const Dialog = forwardRef(({ open, destroyOnClose, target, ...props }: DialogProps, ref) => { + + const { styles, cx } = useStyle() + const [ innerOpen, setOpen ] = useState(() => open ?? false) + const [ submitting, setSubmitting ] = useState(false) + + const dataRef = useRef() + + useImperativeHandle(ref, () => { + return { + show: (data?: any) => { + dataRef.current = data + setOpen(true) + }, + close: () => { + setOpen(false) + }, + get data() { + return dataRef.current + } + } + }, [ setOpen ]) + + const renderTarget = useCallback(() => { + if (target) { + return { + setOpen(true) + }}> + {target} + + } + return null + + }, [ target ]) + + return ( +
+ {renderTarget()} + { + if (props.onCancel?.() === false) { + return + } + setOpen(false) + }} + onOk={async () => { + if (props.onOk) { + setSubmitting(true) + const res = await props.onOk(dataRef.current) + setSubmitting(false) + if (res === false) { + return + } + } + setOpen(false) + }} + > + {props.children} + +
+ ) +}) + +export default Dialog diff --git a/src/components/dialog/style.ts b/src/components/dialog/style.ts new file mode 100644 index 0000000..8457010 --- /dev/null +++ b/src/components/dialog/style.ts @@ -0,0 +1,33 @@ +import { createStyles } from '@/theme' + +export const useStyle = createStyles(({ token, css, cx, prefixCls }, props: any) => { + const prefix = `${prefixCls}-${token?.proPrefix}-dialog-component` + + const container = css` + + ` + + const wrap = css` + .ant-modal-content{ + padding: 20px 0 15px 0; + + .ant-modal-header{ + padding: 0 24px 10px; + border-bottom: 1px solid #eee; + } + .ant-modal-body{ + padding: 10px 24px; + } + .ant-modal-footer{ + background-color: #fcfcfc; + padding: 15px 24px 0; + border-top: 1px solid #eee; + } + } + ` + + return { + container: cx(prefix, props?.className, container), + wrap, + } +}) \ No newline at end of file diff --git a/src/components/dialog/useDialog.tsx b/src/components/dialog/useDialog.tsx new file mode 100644 index 0000000..c8466b6 --- /dev/null +++ b/src/components/dialog/useDialog.tsx @@ -0,0 +1,26 @@ +import Dialog, { DialogProps, DialogRef } from './index.tsx' +import { useRef, useCallback, useMemo } from 'react' + +export const useDialog = (props: DialogProps) => { + const dialogRef = useRef() + + const openDialog = useCallback((data?: any) => { + if (dialogRef.current) { + dialogRef.current.show(data) + } else { + console.error('Dialog ref is not defined') + } + }, []) + + const closeDialog = useCallback(() => { + if (dialogRef.current) { + dialogRef.current.close() + } else { + console.error('Dialog ref is not defined') + } + }, []) + + const dialog = useMemo(() => , [ props ]) + + return [ dialogRef, dialog, openDialog, closeDialog ] as const +} diff --git a/src/components/icon/index.tsx b/src/components/icon/index.tsx index 4bae6da..61286f4 100644 --- a/src/components/icon/index.tsx +++ b/src/components/icon/index.tsx @@ -4,13 +4,51 @@ import * as AntIcons from '@ant-design/icons/es/icons' import IconItem from './picker/IconRender.tsx' import { IconUnit } from './types.ts' +import { createStyles } from '@/theme' type Prefix = 'antd:' | 'park:'; type IconType = `${Prefix}${string}`; +const useStyles = createStyles(({ css, cx }, props: any) => { + + const keyframes = css` + @keyframes rotating { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } + ` + + const container = css` + display: inline-flex; + align-items: center; + justify-content: center; + ` + + const size = props.size ? css` + height: ${props.size}px; + width: ${props.size}px; + line-height: ${props.size}px; + ` : '' + + const isLoading = css` + animation: rotating 2s linear infinite; + ` + return { + container: cx( container, size, props.className), + isLoading: cx(keyframes, size, isLoading) + } +}) + interface IconProps extends Pick { type: IconType | IconUnit['type'] + isLoading?: boolean + + [key: string]: any } function isAntdOrParkIcon(value: string): value is IconType { @@ -18,7 +56,10 @@ function isAntdOrParkIcon(value: string): value is IconType { } export function Icon(props: IconProps) { - const { type, ...other } = props + + const { styles, cx } = useStyles(props) + + const { type, isLoading, ...other } = props if (type && isAntdOrParkIcon(type)) { const [ t, c ] = type.split(':') return @@ -40,11 +81,17 @@ export function Icon(props: IconProps) { } return ( - - - + + + ) } diff --git a/src/layout/RootLayout.tsx b/src/layout/RootLayout.tsx index 0eb8c11..34547af 100644 --- a/src/layout/RootLayout.tsx +++ b/src/layout/RootLayout.tsx @@ -116,7 +116,7 @@ export default () => { return document.getElementById('crazy-pro-layout') || document.body }} > - { items={convertToMenu((childMenuRef.current || []), (item => { return { data: item, + icon: item.icon, key: item.path || item.meta.name, label: item.title, } @@ -332,7 +333,7 @@ export default () => { > */} - + diff --git a/src/locales/lang/pages/websites/zh-CN.ts b/src/locales/lang/pages/websites/zh-CN.ts new file mode 100644 index 0000000..509a7af --- /dev/null +++ b/src/locales/lang/pages/websites/zh-CN.ts @@ -0,0 +1,21 @@ +export default { + status: { + /** + * StatusSuccess StatusEnum = "success" // 成功 + * StatusFail StatusEnum = "fail" // 失败 + * StatusEnable StatusEnum = "enable" // 启用 + * StatusPending StatusEnum = "pending" // 等待 + * StatusDelete StatusEnum = "delete" // 删除 + * StatusDisable StatusEnum = "disable" // 禁用 + * StatusSyncing StatusEnum = "syncing" // 同步 + */ + success: '成功', + fail: '失败', + enable: '启用', + pending: '等待', + delete: '删除', + disable: '禁用', + syncing: '同步', + + } +} \ No newline at end of file diff --git a/src/locales/lang/zh-CN.ts b/src/locales/lang/zh-CN.ts index 0d63291..043ec95 100644 --- a/src/locales/lang/zh-CN.ts +++ b/src/locales/lang/zh-CN.ts @@ -7,6 +7,7 @@ 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' +import websites from './pages/websites/zh-CN.ts' export default { ...antdZh, @@ -46,7 +47,11 @@ export default { home: { welcome: '欢迎使用' }, - + websites:{ + common: { + ...websites, + }, + }, system: { menus, roles diff --git a/src/pages/websites/dns/DNSList.tsx b/src/pages/websites/dns/DNSList.tsx index 2bb95d3..d214311 100644 --- a/src/pages/websites/dns/DNSList.tsx +++ b/src/pages/websites/dns/DNSList.tsx @@ -220,7 +220,7 @@ const DNSList = () => { }}>{t('actions.sync', '同步')}, { deleteDNS(record.id) }} diff --git a/src/pages/websites/domain/index.tsx b/src/pages/websites/domain/index.tsx index 7f513c6..be7e10a 100644 --- a/src/pages/websites/domain/index.tsx +++ b/src/pages/websites/domain/index.tsx @@ -1,5 +1,5 @@ import { useTranslation } from '@/i18n.ts' -import { Button, Form, Divider, Space, Tooltip, Badge } from 'antd' +import { Button, Form, Divider, Space, Tooltip, Badge, Tag, Input } from 'antd' import { useAtom, useAtomValue } from 'jotai' import { deleteWebsiteDomainAtom, @@ -19,8 +19,12 @@ import { getValueCount } from '@/utils' import { Table as ProTable } from '@/components/table' import { Link } from '@tanstack/react-router' import Popconfirm from '@/components/popconfirm' +import { accountStatus, accountStatusColor } from '@/store/websites/dns_account.ts' +import Icon from '@/components/icon' +import { useDialog } from '@/components/dialog' +import { dnsListAtom } from '@/store/websites/dns.ts' -const i18nPrefix = 'websiteDomains.list' +const i18nPrefix = 'websites.domain.list' const WebsiteDomain = () => { @@ -33,11 +37,27 @@ const WebsiteDomain = () => { const [ currentWebsiteDomain, setWebsiteDomain ] = useAtom(websiteDomainAtom) const { data, isFetching, isLoading, refetch } = useAtomValue(websiteDomainsAtom) const { mutate: deleteWebsiteDomain, isPending: isDeleting } = useAtomValue(deleteWebsiteDomainAtom) + const { data: dnsData } = useAtomValue(dnsListAtom) const [ open, setOpen ] = useState(false) const [ openFilter, setFilterOpen ] = useState(false) const [ searchKey, setSearchKey ] = useState(search?.title) + const [ , dialog, openDialog ] = useDialog({ + title: '编辑备注', + children:
+ + + +
, + onOk: () => { + const id = form.getFieldValue('id') + const remark = form.getFieldValue('remark') + return saveOrUpdate({ remark, id } as any) + } + }) + const columns = useMemo(() => { return [ { @@ -51,16 +71,51 @@ const WebsiteDomain = () => { title: t(`${i18nPrefix}.columns.name`, '域名'), dataIndex: 'name', render(_text, record) { - return {record.name} + const edit = { + form.setFieldsValue(record) + openDialog(record) + }} + style={{ paddingBlockStart: 0 }} type={'EditTwo'} size={14}/> + return + {record.name} + {edit} + } }, { title: t(`${i18nPrefix}.columns.dns_account_id`, 'DNS账号'), dataIndex: 'dns_account_id', + valueType: 'select', + fieldProps: { + options: dnsData?.rows?.map?.(item => { + return { + data: item, + label: item.name, + value: item.id, + } + }) + } }, { title: t(`${i18nPrefix}.columns.status`, '状态'), dataIndex: 'status', + hideInForm: true, + valueType: 'select', + valueEnum: accountStatus, + render(_dom, record) { + const loading = [ 'pending', 'syncing' ].includes(record.status) ? + : null + return +
+ {t(`websites.common.status.${record.status!}`, record.status + '')} + + {loading} +
+
+ } }, { title: t(`${i18nPrefix}.columns.nameservers`, 'nameservers'), @@ -69,10 +124,8 @@ const WebsiteDomain = () => { { title: t(`${i18nPrefix}.columns.created`, '创建时间'), dataIndex: 'created', - }, - { - title: t(`${i18nPrefix}.columns.modified`, '修改时间'), - dataIndex: 'modified', + hideInSearch: true, + hideInForm: true, }, { title: t(`${i18nPrefix}.columns.option`, '操作'), @@ -87,6 +140,14 @@ const WebsiteDomain = () => { setOpen(true) }}>{t('actions.edit')}, , + { + + }}>{t('actions.sync', '同步')}, + + , { ] } ] as ProColumns[] - }, [ isDeleting, currentWebsiteDomain, search ]) + }, [ isDeleting, currentWebsiteDomain, search, dnsData ]) useEffect(() => { @@ -287,6 +348,9 @@ const WebsiteDomain = () => { }} columns={columns.filter(item => !item.hideInSearch) as ProFormColumnsType[]}/> + { + dialog + } ) } diff --git a/src/store/websites/dns_account.ts b/src/store/websites/dns_account.ts index f91c4a8..14239ff 100644 --- a/src/store/websites/dns_account.ts +++ b/src/store/websites/dns_account.ts @@ -13,11 +13,41 @@ type SearchParams = IPage & { [key: string]: any } +export const accountStatusColor = { + /** + * StatusSuccess StatusEnum = "success" // 成功 + * StatusFail StatusEnum = "fail" // 失败 + * StatusEnable StatusEnum = "enable" // 启用 + * StatusPending StatusEnum = "pending" // 等待 + * StatusDelete StatusEnum = "delete" // 删除 + * StatusDisable StatusEnum = "disable" // 禁用 + * StatusSyncing StatusEnum = "syncing" // 同步 + */ + success: 'green', + fail: 'red', + enable: 'blue', + pending: 'blue', + delete: 'red', + disable: 'gray', + syncing: 'blue', +} + +export const accountStatus = [ + { label: t(`websites.common.status.success`, 'success'), value: 'success' }, + { label: t(`websites.common.status.fail`, 'fail'), value: 'fail' }, + { label: t(`websites.common.status.enable`, 'enable'), value: 'enable' }, + { label: t(`websites.common.status.pending`, 'pending'), value: 'pending' }, + { label: t(`websites.common.status.delete`, 'delete'), value: 'delete' }, + { label: t(`websites.common.status.disable`, 'disable'), value: 'disable' }, + { label: t(`websites.common.status.syncing`, 'syncing'), value: 'syncing' }, +] + + export const websiteDnsAccountIdAtom = atom(0) export const websiteDnsAccountIdsAtom = atom([]) -export const websiteDnsAccountAtom = atom(undefined as unknown as IWebsiteDnsAccount ) +export const websiteDnsAccountAtom = atom(undefined as unknown as IWebsiteDnsAccount) export const websiteDnsAccountSearchAtom = atom({ key: '',