From b52aadaf509f8007fd6817a6b54b0ce9bd58eff7 Mon Sep 17 00:00:00 2001 From: yinyuchen Date: Sun, 30 Jun 2024 22:04:00 +0800 Subject: [PATCH] matter list hooks refactor 60% --- .prettierrc | 5 + CHANGELOG.md | 3 + craco.config.js | 23 +- ide-editor-config.xml | 2 +- package.json | 12 +- prettier.config.js | 5 - src/App.tsx | 21 +- src/apis/matter.api.ts | 16 + src/apis/util.ts | 19 ++ src/common/menu/MenuManager.tsx | 115 ++----- src/common/model/share/Share.ts | 154 ++++----- src/common/util/SafeUtil.ts | 26 +- src/consts/router.const.ts | 1 + src/contexts/globalContext.tsx | 42 +++ src/hooks/useMatterItemHandles.tsx | 87 +++++ src/hooks/useModal.tsx | 26 ++ src/hooks/usePreviewer.ts | 67 ++++ src/index.tsx | 1 + src/models/Base.ts | 82 +++++ src/models/Matter.ts | 166 ++++++++++ src/models/Preference.ts | 55 ++++ src/models/Share.ts | 15 + src/models/Space.ts | 14 + src/models/SpaceMember.ts | 17 + src/models/User.ts | 14 + src/models/sub/PreviewEngine.ts | 77 +++++ src/pages-hook/matter/List.less | 30 ++ src/pages-hook/matter/List.tsx | 221 +++++++++++++ .../matter/components/MatterCreateItem.less | 44 +++ .../matter/components/MatterCreateItem.tsx | 37 +++ .../matter/components/MatterDeleteModal.tsx | 38 +++ .../matter/components/MatterItem.less | 66 ++++ .../matter/components/MatterItem.tsx | 300 ++++++++++++++++++ .../components/MatterListBreadcrumb.tsx | 35 ++ .../matter/components/MatterListHeader.tsx | 5 + src/pages/Frame.tsx | 220 ++++++------- src/setupAxios.ts | 72 +++++ src/typings/index.ts | 1 + src/utils/dom.util.ts | 14 + tsconfig.json | 5 +- yarn.lock | 149 ++++++++- 41 files changed, 1959 insertions(+), 343 deletions(-) create mode 100644 .prettierrc delete mode 100644 prettier.config.js create mode 100644 src/apis/matter.api.ts create mode 100644 src/apis/util.ts create mode 100644 src/consts/router.const.ts create mode 100644 src/contexts/globalContext.tsx create mode 100644 src/hooks/useMatterItemHandles.tsx create mode 100644 src/hooks/useModal.tsx create mode 100644 src/hooks/usePreviewer.ts create mode 100644 src/models/Base.ts create mode 100644 src/models/Matter.ts create mode 100644 src/models/Preference.ts create mode 100644 src/models/Share.ts create mode 100644 src/models/Space.ts create mode 100644 src/models/SpaceMember.ts create mode 100644 src/models/User.ts create mode 100644 src/models/sub/PreviewEngine.ts create mode 100644 src/pages-hook/matter/List.less create mode 100644 src/pages-hook/matter/List.tsx create mode 100644 src/pages-hook/matter/components/MatterCreateItem.less create mode 100644 src/pages-hook/matter/components/MatterCreateItem.tsx create mode 100644 src/pages-hook/matter/components/MatterDeleteModal.tsx create mode 100644 src/pages-hook/matter/components/MatterItem.less create mode 100644 src/pages-hook/matter/components/MatterItem.tsx create mode 100644 src/pages-hook/matter/components/MatterListBreadcrumb.tsx create mode 100644 src/pages-hook/matter/components/MatterListHeader.tsx create mode 100644 src/setupAxios.ts create mode 100644 src/typings/index.ts create mode 100644 src/utils/dom.util.ts diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..ed35d2c --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 150, + "semi": false, + "singleQuote": true +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c650e1..5f6f24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 4.1.0 +- [ ] MatterItem抽出通用组件 + # 4.0.4 1. 修复已知问题 diff --git a/craco.config.js b/craco.config.js index 20e2ceb..528d5f1 100644 --- a/craco.config.js +++ b/craco.config.js @@ -1,4 +1,6 @@ -const CracoLessPlugin = require('craco-less'); +const CracoLessPlugin = require('craco-less') +const { CracoAliasPlugin } = require('react-app-alias') +const path = require('node:path') module.exports = { devServer: { @@ -6,7 +8,8 @@ module.exports = { // proxy for local development. proxy: { '/api': { - target: 'http://localhost:6010', + // target: 'http://localhost:6010', + target: 'https://tank.ycyin.cn', changeOrigin: true, pathRewrite: { '^/api': '/api', @@ -21,10 +24,8 @@ module.exports = { options: { // resolve-url-loader只对sass生效,craco-less默认使用sass配置,所以这里手动过滤掉resolve-url-loader modifyLessRule: (lessRule) => { - lessRule.use = lessRule.use.filter( - (i) => !i.loader.includes('resolve-url-loader') - ); - return lessRule; + lessRule.use = lessRule.use.filter((i) => !i.loader.includes('resolve-url-loader')) + return lessRule }, lessLoaderOptions: { lessOptions: { @@ -34,5 +35,13 @@ module.exports = { }, }, }, + { + plugin: CracoAliasPlugin, + options: { + alias: { + '@': path.resolve(__dirname, 'src'), + }, + }, + }, ], -}; +} diff --git a/ide-editor-config.xml b/ide-editor-config.xml index 89564f2..7fe9923 100644 --- a/ide-editor-config.xml +++ b/ide-editor-config.xml @@ -1 +1 @@ - + diff --git a/package.json b/package.json index 57d7f9e..4e72cb8 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "4.0.4", "private": true, "dependencies": { + "@ahooksjs/use-url-state": "^3.5.1", "@ant-design/icons": "4.7.0", "@craco/craco": "6.4.3", "@types/echarts": "4.6.1", @@ -11,9 +12,11 @@ "@types/react": "17.0.2", "@types/react-dom": "17.0.2", "@types/react-router-dom": "^5.3.3", + "ahooks": "^3.8.0", "antd": "4.19.2", - "axios": "^0.26.1", + "axios": "^1.7.2", "comma-separated-values": "^3.6.4", + "copy-to-clipboard": "^3.3.3", "craco-less": "2.0.0", "echarts": "4.8.0", "echarts-for-react": "2.0.16", @@ -46,7 +49,9 @@ "devDependencies": { "husky": "^8.0.3", "lint-staged": "^13.2.2", - "prettier": "^2.8.8" + "postcss-flexbugs-fixes": "^5.0.2", + "prettier": "^3.2.5", + "react-app-alias": "^2.2.2" }, "husky": { "hooks": { @@ -58,5 +63,6 @@ "prettier --write", "git add" ] - } + }, + "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" } diff --git a/prettier.config.js b/prettier.config.js deleted file mode 100644 index 196eefa..0000000 --- a/prettier.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - tabWidth: 2, - singleQuote: true, - endOfLine: 'auto', -}; diff --git a/src/App.tsx b/src/App.tsx index 4898b9b..822bfb2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,20 +1,23 @@ -import React from 'react'; -import { BrowserRouter as Router } from 'react-router-dom'; -import { ConfigProvider } from 'antd'; -import Frame from './pages/Frame'; -import en_US from 'antd/es/locale/en_US'; +import React from 'react' +import { BrowserRouter as Router } from 'react-router-dom' +import { ConfigProvider } from 'antd' +import Frame from './pages/Frame' +import en_US from 'antd/es/locale/en_US' // global less 须放在Frame组件之后,不然会出现按需加载的less文件覆盖样式问题 -import './App.less'; +import './App.less' +import GlobalContextProvider from '@/contexts/globalContext' function App() { return ( - + + + - ); + ) } -export default App; +export default App diff --git a/src/apis/matter.api.ts b/src/apis/matter.api.ts new file mode 100644 index 0000000..68477c3 --- /dev/null +++ b/src/apis/matter.api.ts @@ -0,0 +1,16 @@ +import { generateCRUDApi } from './util' +import { IMatter } from '@/models/Matter' +import { axios } from '@/setupAxios' + +const API_PREFIX = 'matter' + +export const matterCRUDApi = generateCRUDApi<{ + listResponse: { + data: IMatter[] + total: number + } +}>(API_PREFIX) + +export const updateMatterPrivacy = (body: { spaceUuid: string; uuid: string; privacy: boolean }) => { + return axios.post(`${API_PREFIX}/change/privacy`, body) +} diff --git a/src/apis/util.ts b/src/apis/util.ts new file mode 100644 index 0000000..7986a3d --- /dev/null +++ b/src/apis/util.ts @@ -0,0 +1,19 @@ +import { axios } from '@/setupAxios' + +export const generateCRUDApi = < + T extends { + filters?: Record + listResponse?: any // list接口返回值 + }, +>( + apiPrefix: string, +) => { + return { + list: (filters?: T['filters']): Promise => { + return axios.get(`${apiPrefix}/page`) + }, + get: () => {}, + save: () => {}, + delete: () => {}, + } +} diff --git a/src/common/menu/MenuManager.tsx b/src/common/menu/MenuManager.tsx index ef1bb64..b43d3a1 100644 --- a/src/common/menu/MenuManager.tsx +++ b/src/common/menu/MenuManager.tsx @@ -1,12 +1,12 @@ /** * 管理当前所有的菜单 */ -import React from 'react'; -import MenuItem from './MenuItem'; -import Moon from '../model/global/Moon'; -import User from '../model/user/User'; -import { UserRole } from '../model/user/UserRole'; -import Sun from '../../common/model/global/Sun'; +import React from 'react' +import MenuItem from './MenuItem' +import Moon from '../model/global/Moon' +import User from '../model/user/User' +import { UserRole } from '../model/user/UserRole' +import Sun from '../../common/model/global/Sun' import { AppstoreOutlined, CloudSyncOutlined, @@ -16,23 +16,23 @@ import { SettingOutlined, ShareAltOutlined, TeamOutlined, -} from '@ant-design/icons'; -import { LoginOutlined } from '@ant-design/icons/lib'; -import Preference from '../model/preference/Preference'; -import Lang from '../model/global/Lang'; +} from '@ant-design/icons' +import { LoginOutlined } from '@ant-design/icons/lib' +import Preference from '../model/preference/Preference' +import Lang from '../model/global/Lang' export default class MenuManager { //单例模式 - private static singleton: MenuManager; + private static singleton: MenuManager constructor() {} static getSingleton(): MenuManager { if (!MenuManager.singleton) { //初始化一个mainLand. - MenuManager.singleton = new MenuManager(); + MenuManager.singleton = new MenuManager() } - return MenuManager.singleton; + return MenuManager.singleton } /** @@ -41,13 +41,13 @@ export default class MenuManager { getSelectedKeys(): string[] { let keys: string[] = this.getMenuItems() .filter((menuItem: MenuItem, index: number) => { - return menuItem.active; + return menuItem.active }) .map((menuItem: MenuItem, index: number) => { - return menuItem.url; - }); + return menuItem.url + }) - return keys; + return keys } /** @@ -55,82 +55,35 @@ export default class MenuManager { */ selectMenu(url: string) { this.getMenuItems().forEach((menuItem: MenuItem, index: number) => { - menuItem.active = menuItem.url === url; - }); + menuItem.active = menuItem.url === url + }) } getMenuItems(): MenuItem[] { - let user: User = Moon.getSingleton().user; - let preference: Preference = Moon.getSingleton().preference; + let user: User = Moon.getSingleton().user + let preference: Preference = Moon.getSingleton().preference - let menuItems: MenuItem[] = []; + let menuItems: MenuItem[] = [] if (!preference.installed) { - menuItems = [ - new MenuItem( - Lang.t('layout.install'), - '/install/index', - - ), - ]; + menuItems = [new MenuItem(Lang.t('layout.install'), '/install/index', )] } else if (user.role === UserRole.GUEST) { - menuItems = [ - new MenuItem(Lang.t('user.login'), '/user/login', ), - ]; + menuItems = [new MenuItem(Lang.t('user.login'), '/user/login', )] } else { - menuItems.push( - new MenuItem( - Lang.t('layout.allFiles'), - '/matter/list', - - ) - ); - menuItems.push( - new MenuItem(Lang.t('layout.space'), '/space', ) - ); - menuItems.push( - new MenuItem( - Lang.t('layout.myShare'), - '/share/list', - - ) - ); - menuItems.push( - new MenuItem(Lang.t('layout.bin'), '/bin/list', ) - ); + menuItems.push(new MenuItem(Lang.t('layout.allFiles'), '/matter/list', )) + menuItems.push(new MenuItem(Lang.t('layout.allFiles'), '/new/matter/list', )) + menuItems.push(new MenuItem(Lang.t('layout.space'), '/space', )) + menuItems.push(new MenuItem(Lang.t('layout.myShare'), '/share/list', )) + menuItems.push(new MenuItem(Lang.t('layout.bin'), '/bin/list', )) if (user.role === UserRole.ADMINISTRATOR) { - menuItems.push( - new MenuItem( - Lang.t('layout.setting'), - '/preference/index', - - ) - ); - menuItems.push( - new MenuItem( - Lang.t('layout.dashboard'), - '/dashboard/index', - - ) - ); - menuItems.push( - new MenuItem( - Lang.t('layout.users'), - `${Sun.getSingleton().isMobile ? '/mobile' : ''}/user/list`, - - ) - ); + menuItems.push(new MenuItem(Lang.t('layout.setting'), '/preference/index', )) + menuItems.push(new MenuItem(Lang.t('layout.dashboard'), '/dashboard/index', )) + menuItems.push(new MenuItem(Lang.t('layout.users'), `${Sun.getSingleton().isMobile ? '/mobile' : ''}/user/list`, )) } - menuItems.push( - new MenuItem( - Lang.t('layout.logout'), - '/user/logout', - - ) - ); + menuItems.push(new MenuItem(Lang.t('layout.logout'), '/user/logout', )) } - return menuItems; + return menuItems } } diff --git a/src/common/model/share/Share.ts b/src/common/model/share/Share.ts index 4f4b392..ff81c8a 100644 --- a/src/common/model/share/Share.ts +++ b/src/common/model/share/Share.ts @@ -1,177 +1,157 @@ -import BaseEntity from '../base/BaseEntity'; -import Matter from '../matter/Matter'; -import FileUtil from '../../util/FileUtil'; -import EnvUtil from '../../util/EnvUtil'; -import { ShareType } from './ShareType'; -import { ShareExpireOption, ShareExpireOptionMap } from './ShareExpireOption'; -import DateUtil from '../../util/DateUtil'; -import SafeUtil from '../../util/SafeUtil'; -import MessageBoxUtil from '../../util/MessageBoxUtil'; +import BaseEntity from '../base/BaseEntity' +import Matter from '../matter/Matter' +import FileUtil from '../../util/FileUtil' +import EnvUtil from '../../util/EnvUtil' +import { ShareType } from './ShareType' +import { ShareExpireOption, ShareExpireOptionMap } from './ShareExpireOption' +import DateUtil from '../../util/DateUtil' +import SafeUtil from '../../util/SafeUtil' +import MessageBoxUtil from '../../util/MessageBoxUtil' export default class Share extends BaseEntity { - name: string | null = null; - shareType: ShareType = ShareType.MIX; - userUuid: string | null = null; - username: string | null = null; - downloadTimes: number = 0; - code: string | null = null; - expireInfinity: boolean = false; - expireTime: Date | null = null; + name: string | null = null + shareType: ShareType = ShareType.MIX + userUuid: string | null = null + username: string | null = null + downloadTimes: number = 0 + code: string | null = null + expireInfinity: boolean = false + expireTime: Date | null = null //当前正在查看的文件夹 - dirMatter: Matter = new Matter(); + dirMatter: Matter = new Matter() //当前share对应的matters - matters: Matter[] = []; - spaceUuid: string | null = null; + matters: Matter[] = [] + spaceUuid: string | null = null //本地临时字段 - expireOption: ShareExpireOption = ShareExpireOption.MONTH; + expireOption: ShareExpireOption = ShareExpireOption.MONTH - static URL_CREATE = '/api/share/create'; - static URL_BROWSE = '/api/share/browse'; - static URL_DELETE_BATCH = '/api/share/delete/batch'; - static URL_ZIP = '/api/share/zip'; - static URL_MATTER_PAGE = '/api/share/matter/page'; + static URL_CREATE = '/api/share/create' + static URL_BROWSE = '/api/share/browse' + static URL_DELETE_BATCH = '/api/share/delete/batch' + static URL_ZIP = '/api/share/zip' + static URL_MATTER_PAGE = '/api/share/matter/page' constructor(reactComponent?: React.Component) { - super(reactComponent); + super(reactComponent) } assign(obj: any) { - super.assign(obj); - super.assignEntity('expireTime', Date); - super.assignEntity('dirMatter', Matter); - super.assignList('matters', Matter); + super.assign(obj) + super.assignEntity('expireTime', Date) + super.assignEntity('dirMatter', Matter) + super.assignList('matters', Matter) } getTAG(): string { - return 'share'; + return 'share' } getFilters() { - return [...super.getFilters()]; + return [...super.getFilters()] } getForm() { return { name: this.name, uuid: this.uuid ? this.uuid : null, - }; + } } getIcon() { if (this.shareType === ShareType.MIX) { - return FileUtil.getIcon('zip', false); + return FileUtil.getIcon('zip', false) } else { - return FileUtil.getIcon( - this.name, - this.shareType === ShareType.DIRECTORY - ); + return FileUtil.getIcon(this.name as any, this.shareType === ShareType.DIRECTORY) } } getLink() { - return EnvUtil.currentHost() + '/share/detail/' + this.uuid; + return EnvUtil.currentHost() + '/share/detail/' + this.uuid } hasExpired() { if (this.expireInfinity) { - return false; + return false } else { if (this.expireTime) { - return new Date(this.expireTime).getTime() < new Date().getTime(); + return new Date(this.expireTime).getTime() < new Date().getTime() } else { - return false; + return false } } } //获取过期时间 getExpireTime() { - const delta = ShareExpireOptionMap[this.expireOption].deltaMillisecond; - const now = new Date(); - return new Date(now.getTime() + delta); + const delta = ShareExpireOptionMap[this.expireOption].deltaMillisecond + const now = new Date() + return new Date(now.getTime() + delta) } //下载zip包 downloadZip(rootUuid: string, puuid?: string) { - window.open( - `${EnvUtil.currentHost()}${Share.URL_ZIP}?shareUuid=${this.uuid}&code=${ - this.code - }&puuid=${puuid}&rootUuid=${rootUuid}` - ); + window.open(`${EnvUtil.currentHost()}${Share.URL_ZIP}?shareUuid=${this.uuid}&code=${this.code}&puuid=${puuid}&rootUuid=${rootUuid}`) } //创建一个分享matterUuids要求为数组,expireTime要求为时间对象 - httpCreate( - matterUuids: string, - successCallback?: () => any, - errorCallback?: () => any - ) { - let that = this; + httpCreate(matterUuids: string, successCallback?: () => any, errorCallback?: () => any) { + let that = this const form = { matterUuids, spaceUuid: this.spaceUuid, expireInfinity: this.expireOption === ShareExpireOption.INFINITY, expireTime: DateUtil.simpleDateTime(this.getExpireTime()), - }; + } this.httpPost( Share.URL_CREATE, form, function (response: any) { - that.assign(response.data.data); - SafeUtil.safeCallback(successCallback)(response); + that.assign(response.data.data) + SafeUtil.safeCallback(successCallback)(response) }, - errorCallback - ); + errorCallback, + ) } - httpDeleteBatch( - uuids: string, - successCallback?: () => any, - errorCallback?: () => any - ) { + httpDeleteBatch(uuids: string, successCallback?: () => any, errorCallback?: () => any) { this.httpPost( Share.URL_DELETE_BATCH, { uuids: uuids }, function (response: any) { - SafeUtil.safeCallback(successCallback)(response); + SafeUtil.safeCallback(successCallback)(response) }, - errorCallback - ); + errorCallback, + ) } - httpBrowse( - puuid: string, - rootUuid: string, - successCallback?: () => any, - errorCallback?: () => any - ) { - let that = this; + httpBrowse(puuid: string, rootUuid: string, successCallback?: () => any, errorCallback?: () => any) { + let that = this const form = { puuid, rootUuid, shareUuid: this.uuid, code: this.code, - }; + } - this.detailLoading = true; + this.detailLoading = true this.httpPost( Share.URL_BROWSE, form, function (response: any) { - that.assign(response.data.data); - that.detailLoading = false; - SafeUtil.safeCallback(successCallback)(response); + that.assign(response.data.data) + that.detailLoading = false + SafeUtil.safeCallback(successCallback)(response) }, function (errorMsg: any) { - that.detailLoading = false; - MessageBoxUtil.error(errorMsg); - SafeUtil.safeCallback(errorCallback)(errorMsg); - } - ); + that.detailLoading = false + MessageBoxUtil.error(errorMsg) + SafeUtil.safeCallback(errorCallback)(errorMsg) + }, + ) } } diff --git a/src/common/util/SafeUtil.ts b/src/common/util/SafeUtil.ts index a82bd84..8f480e3 100644 --- a/src/common/util/SafeUtil.ts +++ b/src/common/util/SafeUtil.ts @@ -2,35 +2,43 @@ export default class SafeUtil { //安全的调用某个函数,函数不存在,创建一个空函数。 static safeCallback(callback: any) { if (typeof callback === 'function') { - return callback; + return callback } else { - return SafeUtil.noop; + return SafeUtil.noop } } //空函数 - static noop = () => {}; + static noop = () => {} //停止事件冒泡 static stopPropagation(e: any) { if (!e) { - return; + return } if (e.stopPropagation) { //系统的点击事件 - e.stopPropagation(); + e.stopPropagation() } else if (e.domEvent && e.domEvent.stopPropagation) { //antd的事件 - e.domEvent.stopPropagation(); + e.domEvent.stopPropagation() } } // 停止事件冒泡包装函数 static stopPropagationWrap(e: any) { - SafeUtil.stopPropagation(e); + SafeUtil.stopPropagation(e) return (func?: any) => { - SafeUtil.safeCallback(func); - }; + SafeUtil.safeCallback(func) + } + } + + static jsonParse(str: any) { + try { + return JSON.parse(str) + } catch (e) { + return {} + } } } diff --git a/src/consts/router.const.ts b/src/consts/router.const.ts new file mode 100644 index 0000000..8116fe3 --- /dev/null +++ b/src/consts/router.const.ts @@ -0,0 +1 @@ +export const SPACE_PATH = '/space' diff --git a/src/contexts/globalContext.tsx b/src/contexts/globalContext.tsx new file mode 100644 index 0000000..8a832f5 --- /dev/null +++ b/src/contexts/globalContext.tsx @@ -0,0 +1,42 @@ +import React, { createContext, useState } from 'react' +import Preference, { IPreference } from '@/models/Preference' +import User, { IUser } from '@/models/User' +import { useMount } from 'ahooks' + +interface IGlobalContext { + preference?: IPreference + user?: IUser + updateCapacity: () => void // 更新全局容量 +} + +export const GlobalContext = createContext(null as any) + +const GlobalContextProvider: React.FC = ({ children }) => { + const [preference, setPreference] = useState() + const [user, setUser] = useState() + + // todo 更新全局容量 + const updateCapacity = () => {} + + useMount(async () => { + // 初始化preference + const preference = await Preference.httpFetch().then((res) => new Preference(res)) + console.log('preference', preference) + setPreference(preference) + preference.updateTitleAndFavicon() + }) + + return ( + + {children} + + ) +} + +export default GlobalContextProvider diff --git a/src/hooks/useMatterItemHandles.tsx b/src/hooks/useMatterItemHandles.tsx new file mode 100644 index 0000000..3194c88 --- /dev/null +++ b/src/hooks/useMatterItemHandles.tsx @@ -0,0 +1,87 @@ +import { IMatter } from '@/models/Matter' +import { EMatterItemFrom } from '@/pages-hook/matter/components/MatterItem' +import { SpaceMemberRole } from '@/common/model/space/member/SpaceMemberRole' +import { ISpaceMember } from '@/models/SpaceMember' +import Lang from '@/common/model/global/Lang' +import { DownloadOutlined, InfoCircleOutlined, LinkOutlined, LockOutlined, RedoOutlined, UnlockOutlined } from '@ant-design/icons' +import { Tooltip } from 'antd' +import React from 'react' +import { DeleteOutlined, EditOutlined } from '@ant-design/icons/lib' +import { stopPropagation } from '@/utils/dom.util' + +interface Props { + from: EMatterItemFrom + matter: IMatter + spaceMember?: ISpaceMember // from === EMatterItemFrom.Space 时会传入 +} +const useMatterItemHandles = ({ from, matter, spaceMember }: Props) => { + // 空间内文件的部分操作需要权限 + const hasSpaceHigherPermission = + from === EMatterItemFrom.Space && !!spaceMember && [SpaceMemberRole.ADMIN, SpaceMemberRole.READ_WRITE].includes(spaceMember.role) + + /* + * 操作项 + * 默认/空间下:设置公私有(空间下校验权限)、文件详情、重命名(空间下校验权限)、复制路径、下载、删除(空间下校验权限)、文件大小、修改时间 + * 分享:下载、文件大小、修改时间 + * 回收站:恢复、文件详情、硬删除、文件大小、软删除时间 + * */ + return { + canSetPrivacy: (from === EMatterItemFrom.Default || hasSpaceHigherPermission) && !matter.dir, + canRecover: from === EMatterItemFrom.Bin, + canGoDetail: [EMatterItemFrom.Default, EMatterItemFrom.Space, EMatterItemFrom.Bin].includes(from), + canRename: from === EMatterItemFrom.Default || hasSpaceHigherPermission, + canCopyPath: [EMatterItemFrom.Default, EMatterItemFrom.Space].includes(from) && !matter.dir, // 文件夹不支持复制路径 安全问题 + canDownload: [EMatterItemFrom.Default, EMatterItemFrom.Space, EMatterItemFrom.Shared].includes(from), + canDelete: from === EMatterItemFrom.Default || hasSpaceHigherPermission, + canHardDelete: from === EMatterItemFrom.Bin, + canViewUpdateTime: [EMatterItemFrom.Default, EMatterItemFrom.Space, EMatterItemFrom.Shared].includes(from), + canViewDeleteTime: from === EMatterItemFrom.Bin, + } +} + +export default useMatterItemHandles + +interface IMatterItemHandleProps { + onClick: () => void +} + +export const getMatterItemHandles = (platform: 'pc' | 'mobile') => { + const isPc = platform === 'pc' + const renderHandle = (Icon: React.ForwardRefExoticComponent, title: string, onClick: () => void) => { + return isPc ? ( + + + + ) : ( +
+ + {title} +
+ ) + } + + const renderDangerHandle = (Icon: React.ForwardRefExoticComponent, title: string, onClick: () => void) => { + return isPc ? ( + + + + ) : ( +
+ + {title} +
+ ) + } + + return { + SetPrivacy: (props: IMatterItemHandleProps) => renderHandle(LockOutlined, Lang.t('matter.setPrivate'), props.onClick), + SetUnPrivacy: (props: IMatterItemHandleProps) => renderHandle(UnlockOutlined, Lang.t('matter.setPublic'), props.onClick), + Recover: (props: IMatterItemHandleProps) => renderHandle(RedoOutlined, Lang.t('matter.recovery'), props.onClick), + GoDetail: (props: IMatterItemHandleProps) => renderHandle(InfoCircleOutlined, Lang.t('matter.fileDetail'), props.onClick), + Rename: (props: IMatterItemHandleProps) => renderHandle(EditOutlined, Lang.t('matter.rename'), props.onClick), + CopyPath: (props: IMatterItemHandleProps) => renderHandle(LinkOutlined, Lang.t('matter.copyPath'), props.onClick), + Download: (props: IMatterItemHandleProps) => renderHandle(DownloadOutlined, Lang.t('matter.download'), props.onClick), + Delete: (props: IMatterItemHandleProps) => renderDangerHandle(DeleteOutlined, Lang.t('matter.delete'), props.onClick), + HardDelete: (props: IMatterItemHandleProps) => renderDangerHandle(DeleteOutlined, Lang.t('matter.hardDelete'), props.onClick), + } +} diff --git a/src/hooks/useModal.tsx b/src/hooks/useModal.tsx new file mode 100644 index 0000000..04d1e72 --- /dev/null +++ b/src/hooks/useModal.tsx @@ -0,0 +1,26 @@ +import { createElement, FunctionComponent, useRef } from 'react' +import { render, unmountComponentAtNode } from 'react-dom' + +// modal中用到context时不要使用,静态方法之痛 https://ant-design.antgroup.com/docs/blog/why-not-static-cn +const useModal = (component: FunctionComponent) => { + const elRef = useRef() + + const openModal = (modalProps: T) => { + elRef.current = document.createElement('div') + document.body.appendChild(elRef.current) + const el = createElement(component, modalProps) + render(el, elRef.current) + } + + const closeModal = () => { + elRef.current && unmountComponentAtNode(elRef.current) && document.body.removeChild(elRef.current) + elRef.current = null + } + + return { + openModal, + closeModal, + } +} + +export default useModal diff --git a/src/hooks/usePreviewer.ts b/src/hooks/usePreviewer.ts new file mode 100644 index 0000000..f3b67af --- /dev/null +++ b/src/hooks/usePreviewer.ts @@ -0,0 +1,67 @@ +import MimeUtil from '@/common/util/MimeUtil' +import { message } from 'antd' +import { useContext } from 'react' +import { GlobalContext } from '@/contexts/globalContext' +import Preference from '@/models/Preference' +import PreviewEngine, { IPreviewEngine } from '@/models/sub/PreviewEngine' +import BrowserPreviewer from '@/pages/widget/previewer/BrowserPreviewer' + +const usePreviewer = () => { + const { preference } = useContext(GlobalContext) + + // todo needToken的处理,交给外层 + const previewFile = (options: { name: string; size: number; url: string }) => { + const { name: fileName, size: fileSize, url: fileUrl } = options + console.log('previewFile', fileName, fileSize, fileUrl) + + const extension = MimeUtil.getExtensionWithoutDot(fileName) + if (!extension) { + message.warning(fileName + ' 没有后缀名,无法预览') + return + } + + if (!preference) return + const preferenceEngines = (new Preference(preference).getPreviewConfig().previewEngines || []).map((en) => new PreviewEngine(en)) + + let previewEngine: PreviewEngine | undefined = undefined + // 先寻找用户自定义的预览引擎 + for (let engine of preferenceEngines) { + if (engine.canPreview(fileName)) { + previewEngine = engine + break + } + } + + // 寻找官方默认的预览引擎 + if (!previewEngine) { + const defaultEngines = PreviewEngine.defaultPreviewEngines() + for (let engine of defaultEngines) { + if (engine.canPreview(fileName)) { + previewEngine = engine + break + } + } + } + + if (!previewEngine) { + message.warning(fileName + ' 无法预览') + return + } + + let targetUrl = previewEngine.url + targetUrl = targetUrl.replace('{originUrl}', fileUrl) + targetUrl = targetUrl.replace('{url}', encodeURIComponent(fileUrl)) + + if (previewEngine.previewInSite) { + BrowserPreviewer.show(fileName, targetUrl, fileSize) + } else { + window.open(targetUrl) + } + } + + return { + previewFile, + } +} + +export default usePreviewer diff --git a/src/index.tsx b/src/index.tsx index e14bf29..3675c57 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,5 +5,6 @@ import 'react-app-polyfill/stable'; import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; +import './setupAxios'; ReactDOM.render(, document.getElementById('root')); diff --git a/src/models/Base.ts b/src/models/Base.ts new file mode 100644 index 0000000..ea96d9b --- /dev/null +++ b/src/models/Base.ts @@ -0,0 +1,82 @@ +import ObjectUtil from '@/common/util/ObjectUtil' +import DateUtil from '@/common/util/DateUtil' + +export interface IBase { + uuid: string + sort: number + createTime: Date | null + updateTime: Date | null +} + +export default class Base implements IBase { + /** + * 唯一标识 + */ + uuid = '' + + /** + * 排序值 + */ + sort = 0 + + /** + * 创建时间 + */ + createTime = null + + /** + * 修改时间 + */ + updateTime = null + + //把obj中的属性,赋值到this中来。采用深拷贝。 + assign(obj: any) { + ObjectUtil.extend(this, obj) + + this.assignEntity('createTime', Date) + this.assignEntity('updateTime', Date) + } + + //直接render出一个Entity. field字段名,Clazz类名。 + assignEntity(field: any, Clazz: any) { + let thisObj: any = this + + let obj: any = thisObj[field] + if (!obj) { + if (Clazz) { + let EntityClazz: any = this.constructor + obj = new EntityClazz()[field] + } else { + return + } + } + + if (Clazz === Date) { + thisObj[field] = DateUtil.str2Date(obj) + } else if (Clazz.prototype instanceof Base) { + //可能此处的该项属性做了特殊处理的。 + //1024*1024 以及 "图片尺寸不超过1M"用let bean = new Clazz(); 就无法反映出来。因为父类assign的时候已经将avatar给变成了Object. + let bean = new thisObj.constructor()[field] + if (!bean) { + bean = new Clazz() + } + + if (typeof obj === 'string') { + try { + obj = JSON.parse(obj) + } catch (e) { + console.error('JSON parse obj error', e) + } + } + + if (obj !== null) { + bean.assign(obj) + thisObj[field] = bean + } + } else { + console.error('调用错误!') + } + } + + httpPage() {} +} diff --git a/src/models/Matter.ts b/src/models/Matter.ts new file mode 100644 index 0000000..170005a --- /dev/null +++ b/src/models/Matter.ts @@ -0,0 +1,166 @@ +import FileUtil from '@/common/util/FileUtil' +import ImageUtil from '@/common/util/ImageUtil' +import EnvUtil from '@/common/util/EnvUtil' +import Base, { IBase } from '@/models/Base' +import { axios } from '@/setupAxios' +import SortDirection from '@/common/model/base/SortDirection' + +export interface IMatter extends IBase { + puuid: string // 父级uuid + userUuid: string + dir: boolean + alien: boolean + name: string + md5: string + size: number + privacy: boolean + path: string + deleted: boolean + spaceUuid: string // 空间 + deleteTime: Date | null +} + +export interface IMatterDetail extends IMatter { + parent: IMatterDetail | null // 父级,null表示根目录 +} + +export default class Matter extends Base implements IMatter { + puuid = '' // 父级,'root'表示根目录 + userUuid = '' + dir = false + alien = false + name = '' + md5 = '' + size = 0 + privacy = false + path = '' + deleted = false + spaceUuid = '' // 空间 + deleteTime = null + + static URL_MATTER = '/matter' + static URL_MATTER_PAGE = `${Matter.URL_MATTER}/page` + static URL_MATTER_CREATE_DIRECTORY = `${Matter.URL_MATTER}/create/directory` + static URL_MATTER_DETAIL = `${Matter.URL_MATTER}/detail` + static URL_MATTER_SOFT_DELETE = `${Matter.URL_MATTER}/soft/delete` + static URL_MATTER_SOFT_DELETE_BATCH = `${Matter.URL_MATTER}/soft/delete/batch` + static URL_MATTER_RECOVERY = `${Matter.URL_MATTER}/recovery` + static URL_MATTER_RECOVERY_BATCH = `${Matter.URL_MATTER}/recovery/batch` + static URL_MATTER_DELETE = `${Matter.URL_MATTER}/delete` + static URL_MATTER_DELETE_BATCH = `${Matter.URL_MATTER}/delete/batch` + static URL_MATTER_RENAME = `${Matter.URL_MATTER}/rename` + static URL_CHANGE_PRIVACY = `${Matter.URL_MATTER}/change/privacy` + static URL_MATTER_MOVE = `${Matter.URL_MATTER}/move` + static URL_MATTER_UPLOAD = `${Matter.URL_MATTER}/upload` + static URL_MATTER_ZIP = `${Matter.URL_MATTER}/zip` + static URL_MATTER_CRAWL = `${Matter.URL_MATTER}/crawl` + + static MATTER_ROOT = 'root' + + constructor(obj?: IMatter) { + super() + obj && this.assign(obj) + } + + // assign(obj: any) { + // super.assign(obj) + // // this.assignEntity('parent', Matter); + // // this.assignEntity('user', User); + // // this.assignEntity('visitTime', Date); + // // this.assignEntity('deleteTime', Date); + // } + + isImage() { + return FileUtil.isImage(this.name) + } + + getPreviewUrl(downloadTokenUuid?: string): string { + return `${EnvUtil.currentHost()}/api/alien/preview/${this.uuid}/${this.name}${downloadTokenUuid ? '?downloadTokenUuid=' + downloadTokenUuid : ''}` + } + + getIcon() { + if (FileUtil.isImage(this.name)) { + return ImageUtil.handleImageUrl(this.getPreviewUrl(), false, 100, 100) + } else { + return FileUtil.getIcon(this.name, this.dir) + } + } + + getDownloadUrl(downloadTokenUuid?: string): string { + return `${EnvUtil.currentHost()}/api/alien/download/${this.uuid}/${ + this.name + }${downloadTokenUuid ? '?downloadTokenUuid=' + downloadTokenUuid : ''}` + } + + getSharePreviewUrl(shareUuid: string, shareCode: string, shareRootUuid: string): string { + return `${EnvUtil.currentHost()}/api/share/matter/preview?matterUuid=${ + this.uuid + }&shareUuid=${shareUuid}&shareCode=${shareCode}&shareRootUuid=${shareRootUuid}` + } + + getShareDownloadUrl(shareUuid: string, shareCode: string, shareRootUuid: string): string { + return `${EnvUtil.currentHost()}/api/share/matter/download?matterUuid=${ + this.uuid + }&shareUuid=${shareUuid}&shareCode=${shareCode}&shareRootUuid=${shareRootUuid}` + } + + preview() {} + + httpTogglePrivacy() { + return axios.post(Matter.URL_CHANGE_PRIVACY, { + uuid: this.uuid, + privacy: !this.privacy, + spaceUuid: this.spaceUuid, + }) + } + + httpSoftDelete() { + return axios.post(Matter.URL_MATTER_SOFT_DELETE, { uuid: this.uuid, spaceUuid: this.spaceUuid }) + } + + httpHardDelete() { + return axios.post(Matter.URL_MATTER_DELETE, { uuid: this.uuid, spaceUuid: this.spaceUuid }) + } + + httpRecover() { + return axios.post(Matter.URL_MATTER_RECOVERY, { + uuid: this.uuid, + }) + } + + httpRename(name: string) { + return axios.post(Matter.URL_MATTER_RENAME, { + uuid: this.uuid, + spaceUuid: this.spaceUuid, + name, + }) + } + + static httpCreateDirectory(body: { spaceUuid: string; puuid: string; name: string }) { + return axios.post(Matter.URL_MATTER_CREATE_DIRECTORY, body) + } + + static httpDetail(spaceUuid: string, uuid: string): Promise { + return axios.get(Matter.URL_MATTER_DETAIL, { + params: { + spaceUuid, + uuid, + }, + }) + } + + static httpList(params?: { + page?: number + pageSize?: number + puuid?: string + orderCreateTime?: SortDirection + orderDir?: SortDirection + deleted?: boolean + spaceUuid?: string + }): Promise<{ + data: IMatter[] + total: number + }> { + return axios.get(Matter.URL_MATTER_PAGE, { params }) + } +} diff --git a/src/models/Preference.ts b/src/models/Preference.ts new file mode 100644 index 0000000..b77f2a2 --- /dev/null +++ b/src/models/Preference.ts @@ -0,0 +1,55 @@ +import Base, { IBase } from '@/models/Base' +import { StringifyType } from '@/typings' +import { IPreviewEngine } from '@/models/sub/PreviewEngine' +import SafeUtil from '@/common/util/SafeUtil' +import { axios } from '@/setupAxios' + +export interface IPreference extends IBase { + name: string + faviconUrl: string + deletedKeepDays: number + previewConfig: StringifyType<{ + previewEngines?: IPreviewEngine[] + }> +} + +export default class Preference extends Base implements IPreference { + name = '' + faviconUrl = '' + deletedKeepDays = 0 + previewConfig = '{}' + + static URL_PREFERENCE = '/preference' + static URL_PREFERENCE_FETCH = `${Preference.URL_PREFERENCE}/fetch` + + constructor(obj?: IPreference) { + super() + obj && this.assign(obj) + } + + getRecycleBinStatus() { + return this.deletedKeepDays > 0 + } + + getPreviewConfig(): { previewEngines?: IPreviewEngine[] } { + return SafeUtil.jsonParse(this.previewConfig) + } + + //修改title和favicon + updateTitleAndFavicon() { + if (this.faviconUrl) { + //修改favicon + let link: any = document.querySelector("link[rel*='icon']") || document.createElement('link') + link.type = 'image/x-icon' + link.rel = 'shortcut icon' + link.href = this.faviconUrl + document.getElementsByTagName('head')[0].appendChild(link) + } + + document.title = this.name + } + + static httpFetch(): Promise { + return axios.post(Preference.URL_PREFERENCE_FETCH) + } +} diff --git a/src/models/Share.ts b/src/models/Share.ts new file mode 100644 index 0000000..03f25c8 --- /dev/null +++ b/src/models/Share.ts @@ -0,0 +1,15 @@ +import Base, { IBase } from '@/models/Base' +import { IMatter } from '@/models/Matter' + +export interface IShare extends IBase { + code: string +} + +export default class Share extends Base implements IShare { + code = '' + + constructor(obj?: IShare) { + super() + obj && this.assign(obj) + } +} diff --git a/src/models/Space.ts b/src/models/Space.ts new file mode 100644 index 0000000..5b79e89 --- /dev/null +++ b/src/models/Space.ts @@ -0,0 +1,14 @@ +import Base, { IBase } from '@/models/Base' + +export interface ISpace extends IBase { + name: string +} + +export default class Space extends Base implements ISpace { + name = '' + + constructor(obj?: ISpace) { + super() + obj && this.assign(obj) + } +} diff --git a/src/models/SpaceMember.ts b/src/models/SpaceMember.ts new file mode 100644 index 0000000..342ca90 --- /dev/null +++ b/src/models/SpaceMember.ts @@ -0,0 +1,17 @@ +import Base, { IBase } from '@/models/Base' +import { SpaceMemberRole } from '@/common/model/space/member/SpaceMemberRole' + +export interface ISpaceMember extends IBase { + spaceUuid: string + role: SpaceMemberRole +} + +export default class SpaceMember extends Base implements ISpaceMember { + spaceUuid = '' + role = SpaceMemberRole.READ_ONLY + + constructor(obj?: ISpaceMember) { + super() + obj && this.assign(obj) + } +} diff --git a/src/models/User.ts b/src/models/User.ts new file mode 100644 index 0000000..6e51baf --- /dev/null +++ b/src/models/User.ts @@ -0,0 +1,14 @@ +import Base, { IBase } from '@/models/Base' + +export interface IUser extends IBase { + spaceUuid: string +} + +export default class User extends Base implements IUser { + spaceUuid = '' + + constructor(obj?: IUser) { + super() + obj && this.assign(obj) + } +} diff --git a/src/models/sub/PreviewEngine.ts b/src/models/sub/PreviewEngine.ts new file mode 100644 index 0000000..2490a0d --- /dev/null +++ b/src/models/sub/PreviewEngine.ts @@ -0,0 +1,77 @@ +import MimeUtil from '@/common/util/MimeUtil' +import ObjectUtil from '@/common/util/ObjectUtil' + +export interface IPreviewEngine { + /** + * 此预览引擎的url + * {originUrl} 文件原始的url地址。eg: https://tanker.eyeblue.cn/api/alien/download/2a0ceee1-744c-4c82-4215-69c382597a50/abstract-free-photo-2210x1473.jpg + * {url} 对于公有文件publicUrl=originUrl,对于私有文件publicUrl是originUrl带上downloadToken。 eg: https://tanker.eyeblue.cn/api/alien/download/2a0ceee1-744c-4c82-4215-69c382597a50/abstract-free-photo-2210x1473.jpg?downloadTokenUuid=6bdba52f-af6b-49ae-5a5d-fd80bfb01d3b + * {b64Url} 文件原始地址 + */ + url: string + + /** + * 此预览引擎支持的文件后缀名。 + * 星好*表示匹配所有的后缀名。 + * 逗号分隔,不带. 例如: xls,doc,ppt,xlsx,docx,pptx + */ + extensions: string + + /** + * 本站预览 or 新标签打开 + */ + previewInSite: boolean +} +export default class PreviewEngine implements IPreviewEngine { + url = '' + extensions = '' + previewInSite = true + + constructor(obj?: IPreviewEngine) { + obj && ObjectUtil.extend(this, obj) + } + + //提供几个默认的预览引擎。 + static defaultPreviewEngines(): PreviewEngine[] { + const defaultEngines: PreviewEngine[] = [] + + //添加office默认预览引擎,使用微软开放接口 + const officeEngine: PreviewEngine = new PreviewEngine() + officeEngine.url = 'https://view.officeapps.live.com/op/embed.aspx?src={url}' + officeEngine.extensions = 'doc,ppt,xls,docx,pptx,xlsx' + officeEngine.previewInSite = true + defaultEngines.push(officeEngine) + + //添加全局预览引擎,使用浏览器预览能力。 + const globalEngine: PreviewEngine = new PreviewEngine() + globalEngine.url = '{originUrl}' + globalEngine.extensions = '*' + globalEngine.previewInSite = true + defaultEngines.push(globalEngine) + + return defaultEngines + } + + //对于某个文件,是否能够预览 + canPreview(fileName: string): boolean { + let extension: string = MimeUtil.getExtensionWithoutDot(fileName) + if (!extension) { + return false + } + + let canHandle = false + if (this.extensions === '*') { + canHandle = true + } else { + let parts: string[] = this.extensions.split(',') + for (let part of parts) { + if (extension === part.toLowerCase() || '.' + extension === part.toLowerCase()) { + canHandle = true + break + } + } + } + + return canHandle + } +} diff --git a/src/pages-hook/matter/List.less b/src/pages-hook/matter/List.less new file mode 100644 index 0000000..4541f36 --- /dev/null +++ b/src/pages-hook/matter/List.less @@ -0,0 +1,30 @@ +.matter-list { + min-height: calc(100% + 20px); + margin: -10px; + padding: 10px; + position: relative; + .obscure { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 100; + display: flex; + justify-content: center; + align-items: center; + background: rgba(0, 0, 0, 0.5); + } + + .ant-upload { + display: inline-block; + } + + .buttons { + display: flex; + flex-wrap: wrap; + } + .matter-pagination { + margin-top: 10px; + } +} diff --git a/src/pages-hook/matter/List.tsx b/src/pages-hook/matter/List.tsx new file mode 100644 index 0000000..33be6c3 --- /dev/null +++ b/src/pages-hook/matter/List.tsx @@ -0,0 +1,221 @@ +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react' +import './List.less' +import { FolderOutlined } from '@ant-design/icons' +import { Button, Col, message, Pagination, Row, Skeleton } from 'antd' +import { useAsyncEffect, useLockFn, useMount, usePagination } from 'ahooks' +import MatterItem, { EMatterItemFrom } from './components/MatterItem' +import Matter, { IMatter, IMatterDetail } from '@/models/Matter' +import { Params } from 'ahooks/es/useAntdTable/types' +import MatterListHeader from '@/pages-hook/matter/components/MatterListHeader' +import { GlobalContext } from '@/contexts/globalContext' +import Lang from '@/common/model/global/Lang' +import { ISpaceMember } from '@/models/SpaceMember' +import MatterCreateItem from '@/pages-hook/matter/components/MatterCreateItem' +import MatterListBreadcrumb from '@/pages-hook/matter/components/MatterListBreadcrumb' +import { ISpace } from '@/models/Space' +import { Route as BreadcrumbRoute } from 'antd/lib/breadcrumb/Breadcrumb' +import useUrlState from '@ahooksjs/use-url-state' +import ImagePreviewer from '@/pages/widget/previewer/ImagePreviewer' +import usePreviewer from '@/hooks/usePreviewer' + +interface Props { + spaceUuid?: string // 是否在空间模式下 +} + +/* + * 考虑函数式还是面向对象来解决函数封装问题 + * 答:将二者结合起来进行使用 + * */ + +const List = ({ spaceUuid }: Props) => { + const { user, updateCapacity } = useContext(GlobalContext) + const isInSpace = !!spaceUuid // 是否在共享空间下 + const matterListPath = isInSpace ? `/space/${spaceUuid}/matter/list` : '/new/matter/list' // todo 链接替换 + const matterSpaceUuid = isInSpace ? spaceUuid : user?.spaceUuid! // 非空间模式下,使用用户个人的spaceUuid + const wrapperRef = useRef(null) + const [dragEnterCount, setDragEnterCount] = useState(0) // todo change + const [space, setSpace] = useState(undefined) // 当前共享空间 + const [spaceMember, setSpaceMember] = useState(undefined) + const [isCreateDir, setIsCreateDir] = useState(false) + const [currentDir, setCurrentDir] = useState(null) // 当前目录文件夹 + + const { previewFile } = usePreviewer() + const [urlQuery, setUrlQuery] = useUrlState<{ + puuid?: string + }>({ + puuid: Matter.MATTER_ROOT, + }) + + const fetchData = async ({ + current, + pageSize, + }: Params[0]): Promise<{ + list: IMatter[] + total: number + }> => { + // search query + // fetch data + + const { puuid } = urlQuery + const res = await Matter.httpList({ + page: current - 1, + pageSize, + puuid, + }) + console.log('res', res) + + return { + list: res.data, + total: res.total, + } + } + + const { data, loading, refresh, mutate } = usePagination(fetchData, { + refreshDeps: [urlQuery], + }) + + // 局部更新,不更新列表,提高性能 + const partialUpdate = (matter: IMatter, field: keyof IMatter, updateValue: any) => { + mutate((oldData) => { + const index = oldData?.list.findIndex((item) => item.uuid === matter.uuid) ?? -1 + if (index !== -1) { + oldData!.list[index][field] = updateValue as never + } + return oldData + }) + } + + const handleMatterTogglePrivacy = useLockFn(async (matter: IMatter) => { + await new Matter(matter).httpTogglePrivacy() + partialUpdate(matter, 'privacy', !matter.privacy) + message.success(Lang.t('operationSuccess')) + }) + + const handleMatterRename = useLockFn(async (matter: IMatter, name: string) => { + await new Matter(matter).httpRename(name) + partialUpdate(matter, 'name', name) + message.success(Lang.t('operationSuccess')) + }) + + const refreshAndUpdateCapacity = () => { + refresh() + updateCapacity() + } + + const handleCreateDirectory = async (name: string) => { + console.log('create dir', name) + try { + await Matter.httpCreateDirectory({ spaceUuid: matterSpaceUuid, puuid: urlQuery.puuid, name }) + refreshAndUpdateCapacity() + } finally { + setIsCreateDir(false) + } + } + + // 考虑是否用useMemo包裹 + const getBreadcrumbs = useMemo((): BreadcrumbRoute[] => { + const breadcrumbs: BreadcrumbRoute[] = [] + + // 递归遍历父级,直到根目录,uuid = null + let parent: IMatterDetail | null = currentDir + + while (parent) { + breadcrumbs.unshift({ + breadcrumbName: parent.name, + path: parent.uuid === currentDir?.uuid ? '' : `${matterListPath}?puuid=${parent.uuid}`, + }) + parent = parent.parent + } + + breadcrumbs.unshift({ + breadcrumbName: Lang.t('matter.allFiles'), + path: currentDir ? matterListPath : '', + }) + + console.log('breadcrumbs', breadcrumbs) + + return breadcrumbs + }, [currentDir]) + + const handleClickRow = (matter: IMatter) => { + const m = new Matter(matter) + if (matter.dir) { + // 进入文件夹 + setUrlQuery({ puuid: matter.uuid }) + } else if (m.isImage()) { + // 预览图片 + const imageMatters = data?.list.filter((item) => new Matter(item).isImage()) || [] + const startIndex = imageMatters.findIndex((item) => item.uuid === matter.uuid) + const imageMatterUrls = imageMatters.map((item) => new Matter(item).getPreviewUrl()) + ImagePreviewer.showMultiPhoto(imageMatterUrls, startIndex) + } else { + previewFile({ + name: matter.name, + size: matter.size, + url: m.getPreviewUrl(), + }) + } + } + + const refreshCurrentDirDetail = async (dirUuid: string) => { + if (dirUuid === Matter.MATTER_ROOT) { + setCurrentDir(null) + return + } + const res = await Matter.httpDetail(matterSpaceUuid, dirUuid) + console.log('detail', res) + setCurrentDir(res) + } + + useEffect(() => { + refreshCurrentDirDetail(urlQuery.puuid) + }, [urlQuery.puuid]) + + if (isInSpace && !space && !spaceMember) return + + return ( +
+ {/*todo drag file*/} + {/*{dragEnterCount > 0 ? ( +
+ +
+ ) : null}*/} + + + + {/*handle context*/} + + + + + + + {/*uploader context*/} + {/*list context*/} + + +
+ {isCreateDir && handleCreateDirectory(name)} />} + {data?.list.map((matter) => ( + handleMatterTogglePrivacy(matter)} + onDeleted={refreshAndUpdateCapacity} + onBlurInput={(name) => handleMatterRename(matter, name)} + onClickRow={() => handleClickRow(matter)} + /> + ))} +
+ + +
+ ) +} + +export default List diff --git a/src/pages-hook/matter/components/MatterCreateItem.less b/src/pages-hook/matter/components/MatterCreateItem.less new file mode 100644 index 0000000..49091c3 --- /dev/null +++ b/src/pages-hook/matter/components/MatterCreateItem.less @@ -0,0 +1,44 @@ +.widget-matter-create-item { + border-top: 1px solid #eee; + background-color: white; + display: flex; + align-content: center; + flex-wrap: nowrap; + line-height: 48px; + + .checkbox-wrapper { + margin-left: 10px; + position: relative; + flex-shrink: 0; + &:after { + position: absolute; + top: 0; + right: -10px; + bottom: 0; + left: -10px; + content: ''; + } + } + + .icon-wrapper { + margin-left: 10px; + width: 24px; + flex-shrink: 0; + display: flex; + align-items: center; + img { + width: 24px; + } + } + + .name-wrapper { + flex-grow: 1; + margin-left: 10px; + overflow: hidden; + .name-input { + width: calc(100% - 20px); + height: 26px; + padding: 6px; + } + } +} \ No newline at end of file diff --git a/src/pages-hook/matter/components/MatterCreateItem.tsx b/src/pages-hook/matter/components/MatterCreateItem.tsx new file mode 100644 index 0000000..933828e --- /dev/null +++ b/src/pages-hook/matter/components/MatterCreateItem.tsx @@ -0,0 +1,37 @@ +import { Checkbox } from 'antd' +import React from 'react' +import FileUtil from '@/common/util/FileUtil' +import Lang from '@/common/model/global/Lang' +import './MatterCreateItem.less' + +interface Props { + onBlurInput: (name: string) => void +} + +const MatterCreateItem = ({ onBlurInput }: Props) => { + return ( +
+
+ +
+ +
+ {'dir'} +
+
+ { + if (e.key.toLowerCase() === 'enter') { + e.currentTarget.blur() + } + }} + onBlur={(e) => onBlurInput(e.target.value.trim())} + /> +
+
+ ) +} + +export default MatterCreateItem diff --git a/src/pages-hook/matter/components/MatterDeleteModal.tsx b/src/pages-hook/matter/components/MatterDeleteModal.tsx new file mode 100644 index 0000000..4cb72f2 --- /dev/null +++ b/src/pages-hook/matter/components/MatterDeleteModal.tsx @@ -0,0 +1,38 @@ +import { Button, Modal, Space } from 'antd' +import Lang from '@/common/model/global/Lang' +import React from 'react' + +interface Props { + onClose: () => void + onHardDel: () => void + onSoftDel: () => void + allowSoftDel: boolean // 是否允许软删除,非空间下 & 开启回收站功能 +} + +const MatterDeleteModal = ({ onClose, onHardDel, onSoftDel, allowSoftDel }: Props) => { + return ( + + + + {allowSoftDel && ( + + )} + + } + > +

{Lang.t('actionDeleteConfirm')}

+
+ ) +} + +export default MatterDeleteModal diff --git a/src/pages-hook/matter/components/MatterItem.less b/src/pages-hook/matter/components/MatterItem.less new file mode 100644 index 0000000..636058f --- /dev/null +++ b/src/pages-hook/matter/components/MatterItem.less @@ -0,0 +1,66 @@ +.widget-matter-item { + border-top: 1px solid #eee; + background-color: white; + display: flex; + align-content: center; + flex-wrap: nowrap; + line-height: 48px; + + &:hover { + cursor: pointer; + background: aliceblue; + } + + .checkbox-wrapper { + margin-left: 10px; + position: relative; + flex-shrink: 0; + &:after { + position: absolute; + top: 0; + right: -10px; + bottom: 0; + left: -10px; + content: ''; + } + } + + .icon-wrapper { + margin-left: 10px; + width: 24px; + flex-shrink: 0; + display: flex; + align-items: center; + img { + width: 24px; + } + } + + .name-wrapper { + flex-grow: 1; + margin-left: 10px; + overflow: hidden; + .name-input { + width: 100%; + height: 26px; + padding: 6px; + } + .name-info { + max-width: 100%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + .icon { + width: 11px; + font-size: 11px; + margin-left: 3px; + } + } + } + + .handles { + flex-shrink: 0; + margin: 0 10px; + } + +} \ No newline at end of file diff --git a/src/pages-hook/matter/components/MatterItem.tsx b/src/pages-hook/matter/components/MatterItem.tsx new file mode 100644 index 0000000..309d291 --- /dev/null +++ b/src/pages-hook/matter/components/MatterItem.tsx @@ -0,0 +1,300 @@ +import React, { useCallback, useContext, useRef, useState } from 'react' +import './MatterItem.less' +import { Checkbox, Input, InputRef, message, Modal, Space, Tooltip } from 'antd' +import Lang from '@/common/model/global/Lang' +import { EllipsisOutlined, ExclamationCircleFilled, UnlockOutlined } from '@ant-design/icons' +import { stopPropagation } from '@/utils/dom.util' +import Matter, { IMatter } from '@/models/Matter' +import { ISpaceMember } from '@/models/SpaceMember' +import Expanding from '@/pages/widget/Expanding' +import { getMatterItemHandles } from '@/hooks/useMatterItemHandles' +import StringUtil from '@/common/util/StringUtil' +import DateUtil from '@/common/util/DateUtil' +import { SpaceMemberRole } from '@/common/model/space/member/SpaceMemberRole' +import copy from 'copy-to-clipboard' +import ImageUtil from '@/common/util/ImageUtil' +import { IShare } from '@/models/Share' +import useModal from '@/hooks/useModal' +import MatterDeleteModal from '@/pages-hook/matter/components/MatterDeleteModal' +import { GlobalContext } from '@/contexts/globalContext' +import Preference from '@/models/Preference' + +export enum EMatterItemFrom { + Default = 'default', // 文件列表 + Space = 'space', // 空间文件列表 + Bin = 'bin', // 回收站 + Shared = 'shared', // 分享 +} + +interface IBaseProps { + from: EMatterItemFrom + matter: IMatter + onClickRow: () => void +} + +interface IDefaultProps extends IBaseProps { + from: EMatterItemFrom.Default + onTogglePrivacy: () => void + onDeleted: () => void + onBlurInput: (name: string) => void +} + +interface ISpaceProps extends Omit { + from: EMatterItemFrom.Space + spaceMember: ISpaceMember +} + +interface ISharedProps extends IBaseProps { + from: EMatterItemFrom.Shared + share: IShare + currentShareRootUuid: string + onGoToDirectory: () => void +} + +interface IBinProps extends IBaseProps { + from: EMatterItemFrom.Bin + onDeleted: () => void + onRecovered: () => void +} + +type Props = IDefaultProps | ISpaceProps | ISharedProps | IBinProps + +const MatterItem = (props: Props) => { + const { preference } = useContext(GlobalContext) + const { from, matter, onClickRow } = props + const m = new Matter(matter) // matter对象,包含一些方法以及封装的属性 + const p = new Preference(preference) + const [inputMode, setInputMode] = useState(false) // 是否是输入态 + const [expand, setExpand] = useState(false) + + const inputRef = useCallback((node: HTMLInputElement | null) => { + console.log('node', node) + if (!node) return + node.value = matter.name + node.focus() + const dotIndex = matter.name.lastIndexOf('.') + node.setSelectionRange(0, dotIndex === -1 ? matter.name.length : dotIndex) + }, []) + + const { openModal: openDeleteModal, closeModal: closeDeleteModal } = useModal(MatterDeleteModal) + + // 空间内文件的部分操作需要权限 + const hasSpaceHigherPermission = + from === EMatterItemFrom.Space && [SpaceMemberRole.ADMIN, SpaceMemberRole.READ_WRITE].includes(props.spaceMember.role) + + /* + * 操作项 + * 默认/空间下:设置公私有(空间下校验权限)、文件详情、重命名(空间下校验权限)、复制路径、下载、删除(空间下校验权限)、文件大小、修改时间 + * 分享:下载、文件大小、修改时间 + * 回收站:恢复、文件详情、硬删除、文件大小、软删除时间 + * */ + const canSetPrivacy = (from === EMatterItemFrom.Default || hasSpaceHigherPermission) && !matter.dir + const canRecover = from === EMatterItemFrom.Bin + const canGoDetail = [EMatterItemFrom.Default, EMatterItemFrom.Space, EMatterItemFrom.Bin].includes(from) + const canRename = from === EMatterItemFrom.Default || hasSpaceHigherPermission + const canCopyPath = [EMatterItemFrom.Default, EMatterItemFrom.Space].includes(from) && !matter.dir // 文件夹不支持复制路径 安全问题 + const canDownload = [EMatterItemFrom.Default, EMatterItemFrom.Space, EMatterItemFrom.Shared].includes(from) + const canDelete = from === EMatterItemFrom.Default || hasSpaceHigherPermission + const canHardDelete = from === EMatterItemFrom.Bin + const canViewUpdateTime = [EMatterItemFrom.Default, EMatterItemFrom.Space, EMatterItemFrom.Shared].includes(from) + const canViewDeleteTime = from === EMatterItemFrom.Bin + + // todo + const handleToggleCheck = () => { + console.log('toggle click') + } + + // todo + const handleGoDetail = () => { + console.log('handleGoDetail') + } + + const handleBlurInput = (e: React.FocusEvent) => { + const name = e.target.value.trim() + if (from === EMatterItemFrom.Default || from === EMatterItemFrom.Space) { + props.onBlurInput(name) + } + setInputMode(false) + } + + /* + * 个人空间/团队空间:文件预览/进入文件夹 + * 分享:文件预览/进入文件夹 + * 回收站:文件预览 + * */ + // const handleClickRow = () => { + // if (matter.dir) { + // if (from === EMatterItemFrom.Default || from === EMatterItemFrom.Space || from === EMatterItemFrom.Shared) { + // props.onGoToDirectory() + // } + // return + // } else { + // // 文件预览 + // if (m.isImage()) { + // } else { + // } + // } + // } + + const handleCopy = () => { + if (copy(m.getDownloadUrl())) { + message.success(Lang.t('copySuccess')) + } else { + message.error(Lang.t('copyError')) + } + } + + const handleDownload = () => { + if ([EMatterItemFrom.Default, EMatterItemFrom.Space].includes(from)) return window.open(m.getDownloadUrl()) + if (from === EMatterItemFrom.Shared) return window.open(m.getShareDownloadUrl(props.share.uuid, props.share.code, props.currentShareRootUuid)) + } + + const handleDelete = () => { + openDeleteModal({ + onClose: closeDeleteModal, + onSoftDel: async () => { + await m.httpSoftDelete() + closeDeleteModal() + if (from === EMatterItemFrom.Default || from === EMatterItemFrom.Space) { + props.onDeleted() + } + }, + onHardDel: async () => { + await m.httpHardDelete() + closeDeleteModal() + if (from === EMatterItemFrom.Default || from === EMatterItemFrom.Space) { + props.onDeleted() + } + }, + allowSoftDel: from === EMatterItemFrom.Default && p.getRecycleBinStatus(), + }) + } + + const handleHardDelete = () => { + Modal.confirm({ + title: Lang.t('actionCanNotRevertConfirm'), + icon: , + onOk: async () => { + await m.httpHardDelete() + message.success(Lang.t('operationSuccess')) + from === EMatterItemFrom.Bin && props.onDeleted() + }, + }) + } + + const handleRecover = () => { + Modal.confirm({ + title: Lang.t('actionRecoveryConfirm'), + icon: , + onOk: async () => { + await m.httpRecover() + message.success(Lang.t('operationSuccess')) + from === EMatterItemFrom.Bin && props.onRecovered() + }, + }) + } + + const renderPcOperations = () => { + const Handles = getMatterItemHandles('pc') + return ( + + {canSetPrivacy ? ( + matter.privacy ? ( + + ) : ( + + ) + ) : null} + {canRecover && } + {canGoDetail && } + {canRename && setInputMode(true)} />} + {canCopyPath && } + {canDownload && } + {canDelete && } + {canHardDelete && } + + {StringUtil.humanFileSize(matter.size)} + + {canViewUpdateTime && ( + + {DateUtil.simpleDateHourMinute(matter.updateTime)} + + )} + {canViewDeleteTime && ( + + {DateUtil.simpleDateHourMinute(matter.deleteTime)} + + )} + + ) + } + + const renderMobileOperations = () => { + const Handles = getMatterItemHandles('mobile') + return ( +
+
+ + {canViewUpdateTime && DateUtil.simpleDateHourMinute(matter.updateTime)} + {canViewDeleteTime && DateUtil.simpleDateHourMinute(matter.deleteTime)} + {StringUtil.humanFileSize(matter.size)} + +
+ {/*todo handles*/} +
+ ) + } + + const getIcon = () => { + if (from === EMatterItemFrom.Shared && m.isImage()) + return ImageUtil.handleImageUrl(m.getSharePreviewUrl(props.share.uuid, props.share.code, props.currentShareRootUuid), false, 100, 100) + return m.getIcon() + } + + return ( +
+
+ +
+ +
+ {matter.name} +
+ +
+ {inputMode ? ( + { + if (e.key.toLowerCase() === 'enter') { + e.currentTarget.blur() + } + }} + onBlur={handleBlurInput} + /> + ) : ( +
+ {matter.name} + {!matter.dir && !matter.privacy && ( + + + + )} +
+ )} +
+ + {/*在大屏幕下的操作栏*/} +
{renderPcOperations()}
+ +
+ setExpand(!expand))} /> +
+ {expand ? renderMobileOperations() : null} +
+ ) +} + +export default MatterItem diff --git a/src/pages-hook/matter/components/MatterListBreadcrumb.tsx b/src/pages-hook/matter/components/MatterListBreadcrumb.tsx new file mode 100644 index 0000000..4e222a3 --- /dev/null +++ b/src/pages-hook/matter/components/MatterListBreadcrumb.tsx @@ -0,0 +1,35 @@ +import { Breadcrumb } from 'antd' +import React from 'react' +import { ISpace } from '@/models/Space' +import { Link } from 'react-router-dom' +import { SPACE_PATH } from '@/consts/router.const' +import { Route } from 'antd/lib/breadcrumb/Breadcrumb' + +interface Props { + space?: ISpace + breadcrumbRoutes: Route[] +} + +const MatterListBreadcrumb = ({ space, breadcrumbRoutes }: Props) => { + return ( + + {space && ( + <> + + {space.name} + + : + + )} + + {breadcrumbRoutes.map((route, index) => ( + <> + {route.path ? {route.breadcrumbName} : route.breadcrumbName} + {index !== breadcrumbRoutes.length - 1 && } + + ))} + + ) +} + +export default MatterListBreadcrumb diff --git a/src/pages-hook/matter/components/MatterListHeader.tsx b/src/pages-hook/matter/components/MatterListHeader.tsx new file mode 100644 index 0000000..7fcc1a8 --- /dev/null +++ b/src/pages-hook/matter/components/MatterListHeader.tsx @@ -0,0 +1,5 @@ +const MatterListHeader = () => { + return null +} + +export default MatterListHeader diff --git a/src/pages/Frame.tsx b/src/pages/Frame.tsx index 382a886..5968015 100644 --- a/src/pages/Frame.tsx +++ b/src/pages/Frame.tsx @@ -1,129 +1,116 @@ -import React from 'react'; -import { - Redirect, - Route, - RouteComponentProps, - withRouter, -} from 'react-router-dom'; - -import './Frame.less'; -import TankComponent from '../common/component/TankComponent'; -import UserLogin from './user/Login'; -import UserRegister from './user/Register'; - -import UserList from './user/List'; -import MobileUserList from './user/MobileList'; -import UserDetail from './user/Detail'; -import UserEdit from './user/Edit'; -import UserAuthentication from './user/Authentication'; - -import PreferenceIndex from './preference/Index'; -import PreferenceEdit from './preference/Edit'; -import PreferenceEngineEdit from './preference/PreviewEngineEdit'; -import PreferenceScanEdit from './preference/ScanEdit'; - -import DashboardIndex from './dashboard/Index'; -import InstallIndex from './install/Index'; - -import MatterList from './matter/List'; -import MatterDetail from './matter/Detail'; - -import BinList from './bin/List'; - -import ShareList from './share/List'; -import ShareDetail from './share/Detail'; -import User from '../common/model/user/User'; -import Moon from '../common/model/global/Moon'; -import Sun from '../common/model/global/Sun'; -import FrameLoading from './widget/FrameLoading'; -import Preference from '../common/model/preference/Preference'; -import { WebResultCode } from '../common/model/base/WebResultCode'; -import MessageBoxUtil from '../common/util/MessageBoxUtil'; -import BottomLayout from './layout/BottomLayout'; -import SideLayout from './layout/SideLayout'; -import TopLayout from './layout/TopLayout'; -import ContentLayout from './layout/ContentLayout'; - -import SpaceList from './space/List'; -import SpaceMemberList from './space/member/List'; -import SpaceMatterList from './space/matter/List'; +import React from 'react' +import { Redirect, Route, RouteComponentProps, withRouter } from 'react-router-dom' + +import './Frame.less' +import TankComponent from '../common/component/TankComponent' +import UserLogin from './user/Login' +import UserRegister from './user/Register' + +import UserList from './user/List' +import MobileUserList from './user/MobileList' +import UserDetail from './user/Detail' +import UserEdit from './user/Edit' +import UserAuthentication from './user/Authentication' + +import PreferenceIndex from './preference/Index' +import PreferenceEdit from './preference/Edit' +import PreferenceEngineEdit from './preference/PreviewEngineEdit' +import PreferenceScanEdit from './preference/ScanEdit' + +import DashboardIndex from './dashboard/Index' +import InstallIndex from './install/Index' + +import MatterList from './matter/List' +import MatterDetail from './matter/Detail' + +import BinList from './bin/List' + +import ShareList from './share/List' +import ShareDetail from './share/Detail' +import User from '../common/model/user/User' +import Moon from '../common/model/global/Moon' +import Sun from '../common/model/global/Sun' +import FrameLoading from './widget/FrameLoading' +import Preference from '../common/model/preference/Preference' +import { WebResultCode } from '../common/model/base/WebResultCode' +import MessageBoxUtil from '../common/util/MessageBoxUtil' +import BottomLayout from './layout/BottomLayout' +import SideLayout from './layout/SideLayout' +import TopLayout from './layout/TopLayout' +import ContentLayout from './layout/ContentLayout' + +import SpaceList from './space/List' +import SpaceMemberList from './space/member/List' +import SpaceMatterList from './space/matter/List' + +import NewMatterList from '../pages-hook/matter/List' interface IProps extends RouteComponentProps<{}> {} interface IState {} class RawFrame extends TankComponent { - user: User = Moon.getSingleton().user; + user: User = Moon.getSingleton().user - preference: Preference = Moon.getSingleton().preference; + preference: Preference = Moon.getSingleton().preference //是否已经完成初始化 - initialized: boolean = false; + initialized: boolean = false constructor(props: IProps) { - super(props); + super(props) } componentDidMount() { //装载全局的路由 - Sun.getSingleton().reactRouter = this.props.history; + Sun.getSingleton().reactRouter = this.props.history //装在全局Frame - Sun.getSingleton().frameComponent = this; + Sun.getSingleton().frameComponent = this - this.initialize(); + this.initialize() } //获取当前登录者的信息 initialize() { - let that = this; - let pathname: string = that.props.location.pathname; + let that = this + let pathname: string = that.props.location.pathname this.preference.httpFetch( function () { // 白名单,不要求登录 - let whitePaths = [ - '/user/login', - '/user/register', - '/user/authentication', - ]; + let whitePaths = ['/user/login', '/user/register', '/user/authentication'] // 尝试登录名单,登录失败不做处理 - let tryLoginPaths = ['/share/detail']; + let tryLoginPaths = ['/share/detail'] if (whitePaths.findIndex((path) => pathname.startsWith(path)) >= 0) { - that.initialized = true; - that.updateUI(); - } else if ( - tryLoginPaths.findIndex((path) => pathname.startsWith(path)) >= 0 - ) { + that.initialized = true + that.updateUI() + } else if (tryLoginPaths.findIndex((path) => pathname.startsWith(path)) >= 0) { that.user.httpInfo(false, function () { - that.initialized = true; - that.updateUI(); - }); + that.initialized = true + that.updateUI() + }) } else { that.user.httpInfo(true, function () { - that.initialized = true; - that.updateUI(); - }); + that.initialized = true + that.updateUI() + }) } }, function (errMessage: string, response: any) { - that.initialized = true; - that.updateUI(); - if ( - response && - response.data && - response.data['code'] === WebResultCode.NOT_INSTALLED - ) { - MessageBoxUtil.warning('网站尚未安装,即将引导进入安装页面!'); - that.preference.installed = false; - Sun.navigateTo('/install/index'); + that.initialized = true + that.updateUI() + if (response && response.data && response.data['code'] === WebResultCode.NOT_INSTALLED) { + MessageBoxUtil.warning('网站尚未安装,即将引导进入安装页面!') + that.preference.installed = false + Sun.navigateTo('/install/index') } - } - ); + }, + ) } render() { - let content: React.ReactNode; + let content: React.ReactNode if (this.initialized) { content = ( @@ -135,11 +122,7 @@ class RawFrame extends TankComponent { {this.preference.installed ? (
- } - /> + } /> @@ -147,30 +130,17 @@ class RawFrame extends TankComponent { - + - - + + - } - /> + } /> @@ -178,21 +148,11 @@ class RawFrame extends TankComponent { - - - + + + + +
) : (
@@ -203,14 +163,14 @@ class RawFrame extends TankComponent {
- ); + ) } else { - content = ; + content = } - return
{content}
; + return
{content}
} } -const Frame = withRouter>(RawFrame); -export default Frame; +const Frame = withRouter>(RawFrame) +export default Frame diff --git a/src/setupAxios.ts b/src/setupAxios.ts new file mode 100644 index 0000000..0ea9fe3 --- /dev/null +++ b/src/setupAxios.ts @@ -0,0 +1,72 @@ +import { default as _axios } from 'axios'; +import { WebResultCode } from './common/model/base/WebResultCode'; +import { message } from 'antd'; + +export const axios = _axios.create({ + baseURL: '/api', +}); + +axios.defaults.headers.post['Content-Type'] = + 'application/x-www-form-urlencoded'; + +//专门捕捉没有登录这种错误。return true -> 有错误(已经处理掉了) false -> 没错误 (什么都没干) +const specialErrorHandler = (response: any) => { + if (!response || !response.data) { + return false; + } + + //1.判断是不是登录错误 + if (response.data['code'] === WebResultCode.LOGIN) { + message.error('您尚未登录,请登录后访问!'); + //立即进行登录跳转。 + window.location.href = '/user/login'; + return true; + } + + return false; +}; + +//从一个返回中获取出其错误信息。适配各种错误的类型。 +const getErrorMessage = (response: any) => { + let msg = '服务器出错,请稍后再试!'; + + if (!response) { + msg = '出错啦,请稍后重试!'; + } else if (typeof response === 'string') { + msg = response; + } else if (response['msg']) { + msg = response['msg']; + } else if (response['message']) { + msg = response['message']; + } else { + let temp = response['data']; + if (temp !== null && typeof temp === 'object') { + if (temp['message']) { + msg = temp['message']; + } else if (temp['msg']) { + msg = temp['msg']; + } else { + if (temp['error'] && temp['error']['message']) { + msg = temp['error']['message']; + } + } + } + } + return msg; +}; + +axios.interceptors.response.use( + (response: any) => { + if (specialErrorHandler(response)) return; + return response.data.data; + }, + (err: any) => { + const response = err.response; + if (specialErrorHandler(response)) return; + const msg = getErrorMessage(response); + if (msg) { + message.error(msg); + } + return Promise.reject(err); + }, +); diff --git a/src/typings/index.ts b/src/typings/index.ts new file mode 100644 index 0000000..b4c86fa --- /dev/null +++ b/src/typings/index.ts @@ -0,0 +1 @@ +export type StringifyType = string diff --git a/src/utils/dom.util.ts b/src/utils/dom.util.ts new file mode 100644 index 0000000..89079ff --- /dev/null +++ b/src/utils/dom.util.ts @@ -0,0 +1,14 @@ +export const stopPropagation = (fn: Function) => { + return (e: any) => { + if (e) { + if (e.stopPropagation) { + //系统的点击事件 + e.stopPropagation() + } else if (e.domEvent && e.domEvent.stopPropagation) { + //antd的事件 + e.domEvent.stopPropagation() + } + } + fn.call(null, e) + } +} diff --git a/tsconfig.json b/tsconfig.json index f2850b7..b2b88c4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,7 +17,10 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react" + "jsx": "react", + "paths": { + "@/*": ["src/*"] + } }, "include": [ "src" diff --git a/yarn.lock b/yarn.lock index 72900d6..bf23ebe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,15 @@ # yarn lockfile v1 +"@ahooksjs/use-url-state@^3.5.1": + version "3.5.1" + resolved "https://registry.npmmirror.com/@ahooksjs/use-url-state/-/use-url-state-3.5.1.tgz#c3ad04e98cbcbc8f9eba476bcbd3237e9809aa5b" + integrity sha512-XTrOLZKOAXahDD1Evg+aSN6qNzoh/FuvRKbUtB/0RhYvz57tyXRPbED0KXK4h2C3ZyHUKBJcVCSDcd6EsTyMyQ== + dependencies: + ahooks "^3.4.1" + query-string "^6.9.0" + tslib "^2.4.1" + "@ampproject/remapping@^2.1.0": version "2.1.2" resolved "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.1.2.tgz#4edca94973ded9630d20101cd8559cedb8d8bd34" @@ -1065,6 +1074,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.21.0": + version "7.24.7" + resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.24.7.tgz#f4f0d5530e8dbdf59b3451b9b3e594b6ba082e12" + integrity sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.16.7", "@babel/template@^7.3.3": version "7.16.7" resolved "https://registry.npmmirror.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -2304,6 +2320,21 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ahooks@^3.4.1, ahooks@^3.8.0: + version "3.8.0" + resolved "https://registry.npmmirror.com/ahooks/-/ahooks-3.8.0.tgz#62476bf3459862ff706de2189b87de5e4f49b298" + integrity sha512-M01m+mxLRNNeJ/PCT3Fom26UyreTj6oMqJBetUrJnK4VNI5j6eMA543Xxo53OBXn6XibA2FXKcCCgrT6YCTtKQ== + dependencies: + "@babel/runtime" "^7.21.0" + dayjs "^1.9.1" + intersection-observer "^0.12.0" + js-cookie "^2.x.x" + lodash "^4.17.21" + react-fast-compare "^3.2.2" + resize-observer-polyfill "^1.5.1" + screenfull "^5.0.0" + tslib "^2.4.1" + ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -2584,12 +2615,14 @@ axe-core@^4.3.5: resolved "https://registry.npmmirror.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413" integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw== -axios@^0.26.1: - version "0.26.1" - resolved "https://registry.npmmirror.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" - integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== +axios@^1.7.2: + version "1.7.2" + resolved "https://registry.npmmirror.com/axios/-/axios-1.7.2.tgz#b625db8a7051fbea61c35a3cbb3a1daa7b9c7621" + integrity sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw== dependencies: - follow-redirects "^1.14.8" + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" axobject-query@^2.2.0: version "2.2.0" @@ -3235,6 +3268,13 @@ copy-to-clipboard@^3.2.0: dependencies: toggle-selection "^1.0.6" +copy-to-clipboard@^3.3.3: + version "3.3.3" + resolved "https://registry.npmmirror.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" + integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== + dependencies: + toggle-selection "^1.0.6" + core-js-compat@^3.20.2, core-js-compat@^3.21.0: version "3.21.1" resolved "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.21.1.tgz#cac369f67c8d134ff8f9bd1623e3bc2c42068c82" @@ -3531,6 +3571,11 @@ dayjs@1.x: resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.0.tgz#009bf7ef2e2ea2d5db2e6583d2d39a4b5061e805" integrity sha512-JLC809s6Y948/FuCZPm5IX8rRhQwOiyMb2TfVVQEixG7P8Lm/gt5S7yoQZmC8x1UehI9Pb7sksEt4xx14m+7Ug== +dayjs@^1.9.1: + version "1.11.11" + resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e" + integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg== + debug@2.6.9, debug@^2.6.0, debug@^2.6.9: version "2.6.9" resolved "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -3557,6 +3602,11 @@ decimal.js@^10.2.1: resolved "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== +decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.npmmirror.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + dedent@^0.7.0: version "0.7.0" resolved "https://registry.npmmirror.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" @@ -4433,6 +4483,11 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +filter-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" + integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== + finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -4498,11 +4553,16 @@ flatted@^3.1.0: resolved "https://registry.npmmirror.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== -follow-redirects@^1.0.0, follow-redirects@^1.14.8: +follow-redirects@^1.0.0: version "1.14.9" resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== +follow-redirects@^1.15.6: + version "1.15.6" + resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + fork-ts-checker-webpack-plugin@^6.5.0: version "6.5.0" resolved "https://registry.npmmirror.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.0.tgz#0282b335fa495a97e167f69018f566ea7d2a2b5e" @@ -4531,6 +4591,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -5036,6 +5105,11 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +intersection-observer@^0.12.0: + version "0.12.2" + resolved "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.12.2.tgz#4a45349cc0cd91916682b1f44c28d7ec737dc375" + integrity sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg== + ip@^1.1.0: version "1.1.5" resolved "https://registry.npmmirror.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" @@ -5749,6 +5823,11 @@ jest@^27.4.3: import-local "^3.0.2" jest-cli "^27.5.1" +js-cookie@^2.x.x: + version "2.2.1" + resolved "https://registry.npmmirror.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" + integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -7347,10 +7426,10 @@ prelude-ls@~1.1.2: resolved "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== -prettier@^2.8.8: - version "2.8.8" - resolved "https://registry.npmmirror.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== +prettier@^3.2.5: + version "3.3.2" + resolved "https://registry.npmmirror.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a" + integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA== pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: version "5.6.0" @@ -7411,6 +7490,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.npmmirror.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" @@ -7443,6 +7527,16 @@ qs@^6.10.3: dependencies: side-channel "^1.0.4" +query-string@^6.9.0: + version "6.14.1" + resolved "https://registry.npmmirror.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" + integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== + dependencies: + decode-uri-component "^0.2.0" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -7852,6 +7946,11 @@ rc-virtual-list@^3.2.0, rc-virtual-list@^3.4.2: rc-resize-observer "^1.0.0" rc-util "^5.0.7" +react-app-alias@^2.2.2: + version "2.2.2" + resolved "https://registry.npmmirror.com/react-app-alias/-/react-app-alias-2.2.2.tgz#2b486cd21cdba362df9ef71a06ab73e0a8ea660d" + integrity sha512-mkebUkGLEBA8A8jripu5h1e3cccGl8wWHCUmyJo43/KhaN91DO3qyCLWGWneogqkG4PWhp2JHtlCJ06YSdHVYQ== + react-app-polyfill@^3.0.0: version "3.0.0" resolved "https://registry.npmmirror.com/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz#95221e0a9bd259e5ca6b177c7bb1cb6768f68fd7" @@ -7908,6 +8007,11 @@ react-error-overlay@^6.0.10: resolved "https://registry.npmmirror.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6" integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA== +react-fast-compare@^3.2.2: + version "3.2.2" + resolved "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49" + integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ== + react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: version "16.13.1" resolved "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -8088,6 +8192,11 @@ regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9: resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regenerator-transform@^0.14.2: version "0.14.5" resolved "https://registry.npmmirror.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" @@ -8374,6 +8483,11 @@ schema-utils@^4.0.0: ajv-formats "^2.1.1" ajv-keywords "^5.0.0" +screenfull@^5.0.0: + version "5.2.0" + resolved "https://registry.npmmirror.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba" + integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA== + scroll-into-view-if-needed@^2.2.25: version "2.2.29" resolved "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz#551791a84b7e2287706511f8c68161e4990ab885" @@ -8649,6 +8763,11 @@ spdy@^4.0.2: select-hose "^2.0.0" spdy-transport "^3.0.0" +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -8676,6 +8795,11 @@ stackframe@^1.1.1: resolved "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" + integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== + string-argv@^0.3.1: version "0.3.2" resolved "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz#2b6d0ef24b656274d957d54e0a4bbf6153dc02b6" @@ -9135,6 +9259,11 @@ tslib@^2.1.0: resolved "https://registry.npmmirror.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== +tslib@^2.4.1: + version "2.6.3" + resolved "https://registry.npmmirror.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.npmmirror.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"