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/assets/css/global/responsive.less b/src/assets/css/global/responsive.less index 83685f9..477fa57 100644 --- a/src/assets/css/global/responsive.less +++ b/src/assets/css/global/responsive.less @@ -6,6 +6,10 @@ .visible-pc { display: block !important; } + + .visible-pc-flex { + display: flex !important; + } } @media (max-width: 992px) { @@ -16,6 +20,10 @@ .visible-pc { display: none !important; } + + .visible-pc-flex { + display: none !important; + } } //浏览器滚动条样式 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/global/i18n/LangEn.ts b/src/common/model/global/i18n/LangEn.ts index b9f962e..3b83d04 100644 --- a/src/common/model/global/i18n/LangEn.ts +++ b/src/common/model/global/i18n/LangEn.ts @@ -31,12 +31,9 @@ let LangEn = { mysqlConnectionPass: 'Connect MySQL Ok', testMysqlConnection: 'Tes MySQL Connection', notice: 'Notice', - sqliteNotice1: - 'Sqlite is a tiny file database, you can use it without installation', - mysqlNotice1: - 'If Mysql and EyeblueTank installed on the same server, Host is 127.0.0.1', - mysqlNotice2: - 'Your mysql account must have access to create table, or the second step will fail.', + sqliteNotice1: 'Sqlite is a tiny file database, you can use it without installation', + mysqlNotice1: 'If Mysql and EyeblueTank installed on the same server, Host is 127.0.0.1', + mysqlNotice2: 'Your mysql account must have access to create table, or the second step will fail.', validateMysqlFirst: 'Please test the mysql connection firstly.', preStep: 'Pre Step', nextStep: 'Next Step', @@ -48,27 +45,22 @@ let LangEn = { missingFields: 'Missing fields', tableNotice: "'Create Tables' will trigger the following actions:", tableNotice1: 'If a table not exist, create it.', - tableNotice2: - 'If a table exist and no fields missing, nothing will do on this table.', - tableNotice3: - 'If a table exist but some fields is missing, it will add the missing fields.', - tableNotice4: - 'If a table exist and some fields not necessary, nothing will do on this table.', + tableNotice2: 'If a table exist and no fields missing, nothing will do on this table.', + tableNotice3: 'If a table exist but some fields is missing, it will add the missing fields.', + tableNotice4: 'If a table exist and some fields not necessary, nothing will do on this table.', oneKeyCreate: 'Create Tables', createFinish: 'Finish Creating Tables', createTableSuccess: 'Create tables successfully', crateTableFirst: "Please click 'Create Tables'", setAdministrator: 'Config Administrator', detectAdministrator: 'Detect the following administrators:', - useOrCreateAdministrator: - 'You can validate one of them, or you can create a new one.', + useOrCreateAdministrator: 'You can validate one of them, or you can create a new one.', validateAdministrator: 'Validate administrator', createAdministrator: 'Create administrator', administratorUsername: 'username', administratorPassword: 'password', administratorRePassword: 'Enter administrator password again', - usernameRule: - 'EyeblueTank will use username as directory name, so only lowercase letter and number and _ is permitted.', + usernameRule: 'EyeblueTank will use username as directory name, so only lowercase letter and number and _ is permitted.', congratulationInstall: 'Congratulations, install successfully!', configAdminFirst: 'Please config administrator first.', createAdminSuccess: 'Create administrator successfully!', @@ -109,6 +101,7 @@ let LangEn = { create: 'Create', createTime: 'Create Time', updateTime: 'Update Time', + updateDate: 'Update Date', deleteTime: 'Deleted Time', uploadUserNickname: 'Upload User', root: 'Root', @@ -130,8 +123,7 @@ let LangEn = { uploaded: 'Uploaded', uploadDir: 'Upload dir', uploadInfo: 'Upload Info', - uploadErrorInfo: - 'Some files failed to upload, you can export CSV files for viewing', + uploadErrorInfo: 'Some files failed to upload, you can export CSV files for viewing', exportCSV: 'Export upload error detail', speed: 'Speed', fileInfo: 'File basic info', @@ -144,8 +136,7 @@ let LangEn = { downloadTimes: 'Download times', operations: 'Operation', oneTimeLink: 'One time link', - oneTimeLinkInfo: - 'One time link will expire after downloading, click to copy', + oneTimeLinkInfo: 'One time link will expire after downloading, click to copy', imageCache: 'Image cache', searchFile: 'Search file', noContentYet: 'No content under this directory yet', @@ -156,8 +147,7 @@ let LangEn = { noImageCache: 'No image cache', recycleBin: 'Recycle bin', deleted: 'Deleted', - unCompatibleBrowser: - 'The current browser does not support it. Please try choose another one', + unCompatibleBrowser: 'The current browser does not support it. Please try choose another one', canIUse: 'To see if the current browser supports it', intoRecycleBin: 'Recycle bin', finishingTip: 'Please wait while files are sorted...', @@ -209,10 +199,8 @@ let LangEn = { tankDocLink: 'https://tank-doc.eyeblue.cn', allowRegister: 'Allow register', systemCleanup: 'System Cleanup', - systemCleanupDescription: - "This operation will cleanup everything except administrators' data", - systemCleanupPrompt: - "This operation will cleanup everything except administrators' account data, please input login password.", + systemCleanupDescription: "This operation will cleanup everything except administrators' data", + systemCleanupPrompt: "This operation will cleanup everything except administrators' account data, please input login password.", previewConfig: 'File Preview Config', editPreference: 'Edit Preference', editPreviewEngine: 'Edit Preview engine', @@ -229,13 +217,10 @@ let LangEn = { defaultPreview: 'Default preview engine', previewEngine: 'Number {} preview engine', defaultPreviewDesc: 'Default preview engine, can not be removed', - engineUsageHint: - 'Previewing a file using the first engine matches the extentions.', - engineRegHelper: - 'template syntax, {url} represents the file path, the preview will automatically replace with the corresponding file url', + engineUsageHint: 'Previewing a file using the first engine matches the extentions.', + engineRegHelper: 'template syntax, {url} represents the file path, the preview will automatically replace with the corresponding file url', engineRegPlaceHolder: 'eg:https://xxx.xxx.xxx?url={url}', - engineSuffixPlaceHolder: - 'suffix can not be null, split by comma, look like: doc,ppt,xls', + engineSuffixPlaceHolder: 'suffix can not be null, split by comma, look like: doc,ppt,xls', previewCurrent: 'preview in current page', previewOpen: 'preview in new page', editScan: 'Edit Scan disk', @@ -325,8 +310,7 @@ let LangEn = { sync: 'Sync', activeUser: 'Active this user', deleteUser: 'Delete this user', - deleteHint: - "This action will delete {}'s all records, including files,shares,user infos etc. Continue?", + deleteHint: "This action will delete {}'s all records, including files,shares,user infos etc. Continue?", welcomeLogin: 'Welcome Login', logining: 'Login...', login: 'Login', @@ -348,8 +332,7 @@ let LangEn = { editUser: 'Edit User', editSomebodyPassword: "Edit {}'s Password", transfigurationPromptText: 'Transfiguration Prompt', - transfigurationPrompt: - 'You will login as this user.Please visit this link in other browser, if in current browser, you will logout.', + transfigurationPrompt: 'You will login as this user.Please visit this link in other browser, if in current browser, you will logout.', searchUser: 'Search user', }, space: { @@ -361,8 +344,7 @@ let LangEn = { sizeLimitTip: 'Please enter single file limit', totalSizeLimit: 'Total space limit', totalSizeLimitTip: 'Please enter total space limit', - deleteHint: - 'This operation will permanently delete the space. Please delete all files and members of the space first. Do you want to continue?', + deleteHint: 'This operation will permanently delete the space. Please delete all files and members of the space first. Do you want to continue?', memberManage: 'Space member Manage', member: 'member', memberTip: 'Please enter member', @@ -370,8 +352,7 @@ let LangEn = { memberRoleTip: 'Please select member role', bindMember: 'Bind space member', unBindMember: 'Unbind space member', - unBindMemberHint: - 'This operation will unbind the member from the space. Do you want to continue?', + unBindMemberHint: 'This operation will unbind the member from the space. Do you want to continue?', memberRoleReadonly: 'Readonly', memberRoleReadWrite: 'Read and write', memberRoleAdmin: 'Admin', @@ -429,6 +410,6 @@ let LangEn = { inputRequired: 'Input required', selectRequired: 'Select required', more: 'More', -}; +} -export default LangEn; +export default LangEn diff --git a/src/common/model/global/i18n/LangZh.ts b/src/common/model/global/i18n/LangZh.ts index b085083..01387a7 100644 --- a/src/common/model/global/i18n/LangZh.ts +++ b/src/common/model/global/i18n/LangZh.ts @@ -32,10 +32,8 @@ let LangZh = { testMysqlConnection: '测试MySQL连接', notice: '注意', sqliteNotice1: 'sqlite是一个轻量的文件数据库,无需安装即可使用', - mysqlNotice1: - '如果数据库和蓝眼云盘安装在同一台服务器,Host可以直接填写 127.0.0.1。', - mysqlNotice2: - '数据库账户的权限要求要能够创建表,否则第二步"创建表"操作会出错', + mysqlNotice1: '如果数据库和蓝眼云盘安装在同一台服务器,Host可以直接填写 127.0.0.1。', + mysqlNotice2: '数据库账户的权限要求要能够创建表,否则第二步"创建表"操作会出错', validateMysqlFirst: '请首先验证数据库连接', preStep: '上一步', nextStep: '下一步', @@ -49,16 +47,14 @@ let LangZh = { tableNotice1: '如果某表不存在,则直接创建表。', tableNotice2: '如果某表存在并且字段齐全,那么不会对该表做任何操作。', tableNotice3: '如果某表存在但是部分字段缺失,那么会在该表中增加缺失字段。', - tableNotice4: - '如果表中有多余的字段(多余字段即不是蓝眼云盘需要的字段),不会做删除处理,而会维持原样。', + tableNotice4: '如果表中有多余的字段(多余字段即不是蓝眼云盘需要的字段),不会做删除处理,而会维持原样。', oneKeyCreate: '一键建表', createFinish: '建表完成', createTableSuccess: '建表成功', crateTableFirst: "请首先点击'一键建表'", setAdministrator: '设置管理员', detectAdministrator: '检测到系统中已经存在有以下管理员:', - useOrCreateAdministrator: - '你可以使用其中一位管理员的用户名和密码进行验证,或者创建一位新的管理员账户', + useOrCreateAdministrator: '你可以使用其中一位管理员的用户名和密码进行验证,或者创建一位新的管理员账户', validateAdministrator: '验证管理员账户', createAdministrator: '创建管理员账户', administratorUsername: '管理员用户名', @@ -105,6 +101,7 @@ let LangZh = { create: '新建', createTime: '创建时间', updateTime: '修改时间', + updateDate: '修改日期', deleteTime: '删除时间', uploadUserNickname: '上传者', root: '根目录', @@ -139,8 +136,7 @@ let LangZh = { downloadTimes: '下载次数', operations: '操作', oneTimeLink: '一次性链接', - oneTimeLinkInfo: - '使用一次性链接下载后链接立即失效,可以分享这个链接给朋友,点击复制', + oneTimeLinkInfo: '使用一次性链接下载后链接立即失效,可以分享这个链接给朋友,点击复制', imageCache: '图片缓存', searchFile: '搜索文件', noContentYet: '该目录下暂无任何内容', @@ -162,8 +158,7 @@ let LangZh = { crawlFilenameTip: '请输入文件名', crawlBackground: '后台抓取', crawlSuccessTip: '抓取成功', - crawlDescription: - '抓取文件指后台将从指定资源链接直接下载到蓝眼云盘当前目录下', + crawlDescription: '抓取文件指后台将从指定资源链接直接下载到蓝眼云盘当前目录下', }, router: { allFiles: '全部文件', @@ -204,8 +199,7 @@ let LangZh = { allowRegister: '允许自主注册', systemCleanup: '重置系统', systemCleanupDescription: '重置系统将清空除管理员账号外所有数据', - systemCleanupPrompt: - '重置系统将清空除管理员账号外所有数据,事关重大,请输入登录密码', + systemCleanupPrompt: '重置系统将清空除管理员账号外所有数据,事关重大,请输入登录密码', previewConfig: '文件预览配置', editPreference: '设置网站偏好', editPreviewEngine: '设置预览引擎', @@ -222,13 +216,10 @@ let LangZh = { defaultPreview: '默认引擎', previewEngine: '{}号引擎', defaultPreviewDesc: '默认预览引擎,不可移除', - engineRegHelper: - '此处填写模板语法:{url}表示文件路径,预览时会自动替换成对应的文件url;{b64url}表示base64机密后的路径,适用于kkfileview;', - engineUsageHint: - '对于一个文件的预览,使用第一个匹配到后缀名的引擎,没有配匹到则使用系统默认预览引擎', + engineRegHelper: '此处填写模板语法:{url}表示文件路径,预览时会自动替换成对应的文件url;{b64url}表示base64机密后的路径,适用于kkfileview;', + engineUsageHint: '对于一个文件的预览,使用第一个匹配到后缀名的引擎,没有配匹到则使用系统默认预览引擎', engineRegPlaceHolder: '例如:https://xxx.xxx.xxx?url={url}', - engineSuffixPlaceHolder: - '输入后缀,使用逗号分隔,不用带. 例如:doc,ppt,xls', + engineSuffixPlaceHolder: '输入后缀,使用逗号分隔,不用带. 例如:doc,ppt,xls', previewCurrent: '本站预览', previewOpen: '新标签打开', editScan: '设置磁盘扫描', @@ -315,8 +306,7 @@ let LangZh = { disableUser: '禁用该用户', activeUser: '激活该用户', deleteUser: '删除该用户', - deleteHint: - '此操作将删除用户【{}】的所有记录,包括文件,分享,用户信息等内容,确定继续?', + deleteHint: '此操作将删除用户【{}】的所有记录,包括文件,分享,用户信息等内容,确定继续?', disable: '禁用', sync: '同步', active: '激活', @@ -341,8 +331,7 @@ let LangZh = { editUser: '编辑用户', editSomebodyPassword: '修改{}的密码', transfigurationPromptText: '变身提示', - transfigurationPrompt: - '您将使用该用户的身份登录。请复制以下链接到其他浏览器访问,在当前浏览器访问会导致当前用户登录信息失效。', + transfigurationPrompt: '您将使用该用户的身份登录。请复制以下链接到其他浏览器访问,在当前浏览器访问会导致当前用户登录信息失效。', searchUser: '搜索用户', }, space: { @@ -354,8 +343,7 @@ let LangZh = { sizeLimitTip: '请输入单文件大小限制', totalSizeLimit: '总大小限制', totalSizeLimitTip: '请输入总大小限制', - deleteHint: - '此操作将永久删除该空间, 请先删除空间所有文件和空间成员, 是否继续?', + deleteHint: '此操作将永久删除该空间, 请先删除空间所有文件和空间成员, 是否继续?', memberManage: '空间成员管理', member: '空间成员', memberTip: '请搜索选择用户', @@ -422,6 +410,6 @@ let LangZh = { inputRequired: '该项必填', selectRequired: '该项必选', more: '更多', -}; +} -export default LangZh; +export default LangZh 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..a70f88b --- /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 hasHigherSpacePermission = + from === EMatterItemFrom.Space && !!spaceMember && [SpaceMemberRole.ADMIN, SpaceMemberRole.READ_WRITE].includes(spaceMember.role) + + /* + * 操作项 + * 默认/空间下:设置公私有(空间下校验权限)、文件详情、重命名(空间下校验权限)、复制路径、下载、删除(空间下校验权限)、文件大小、修改时间 + * 分享:下载、文件大小、修改时间 + * 回收站:恢复、文件详情、硬删除、文件大小、软删除时间 + * */ + return { + canSetPrivacy: (from === EMatterItemFrom.Default || hasHigherSpacePermission) && !matter.dir, + canRecover: from === EMatterItemFrom.Bin, + canGoDetail: [EMatterItemFrom.Default, EMatterItemFrom.Space, EMatterItemFrom.Bin].includes(from), + canRename: from === EMatterItemFrom.Default || hasHigherSpacePermission, + canCopyPath: [EMatterItemFrom.Default, EMatterItemFrom.Space].includes(from) && !matter.dir, // 文件夹不支持复制路径 安全问题 + canDownload: [EMatterItemFrom.Default, EMatterItemFrom.Space, EMatterItemFrom.Shared].includes(from), + canDelete: from === EMatterItemFrom.Default || hasHigherSpacePermission, + 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..3ca0243 --- /dev/null +++ b/src/models/Matter.ts @@ -0,0 +1,170 @@ +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 + orderName?: SortDirection + orderSize?: SortDirection + orderUpdateTime?: SortDirection + orderCreateTime?: SortDirection + orderDir?: SortDirection + deleted?: boolean + spaceUuid?: string + name?: string + }): Promise<{ + data: IMatter[] + totalItems: 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..b68a431 --- /dev/null +++ b/src/pages-hook/matter/List.less @@ -0,0 +1,68 @@ +.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; + } + + .widget-matter-list-header { + display: flex; + align-items: center; + background-color: white; + height: 48px; + color: #999; + .sort-part { + line-height: 48px; + display: flex; + align-items: center; + + &:hover { + cursor: pointer; + background-color: aliceblue; + } + + &-name { + flex-grow: 1; + padding-left: 10px; + } + + &-size { + flex-shrink: 0; + width: 90px; + padding-left: 10px; + cursor: pointer; + } + + &-date { + padding-left: 10px; + flex-shrink: 0; + width: 145px; + cursor: pointer; + } + } + } +} diff --git a/src/pages-hook/matter/List.tsx b/src/pages-hook/matter/List.tsx new file mode 100644 index 0000000..f6dc1b1 --- /dev/null +++ b/src/pages-hook/matter/List.tsx @@ -0,0 +1,370 @@ +import React, { useContext, useEffect, useMemo, useRef, useState } from 'react' +import { + ArrowDownOutlined, + ArrowUpOutlined, + DeleteOutlined, + DownloadOutlined, + FolderOutlined, + MinusSquareOutlined, + PlusSquareOutlined, +} from '@ant-design/icons' +import { Button, Col, Input, message, Pagination, Row, Skeleton, Space as AntdSpace } from 'antd' +import { useLockFn, usePagination } from 'ahooks' +import MatterItem, { EMatterItemFrom } from './components/MatterItem' +import Matter, { IMatter, IMatterDetail } from '@/models/Matter' +import { Params } from 'ahooks/es/useAntdTable/types' +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' +import SortDirection from '@/common/model/base/SortDirection' +import EnvUtil from '@/common/util/EnvUtil' +import useModal from '@/hooks/useModal' +import MatterDeleteModal from '@/pages-hook/matter/components/MatterDeleteModal' +import Preference from '@/models/Preference' + +import './List.less' +import { SpaceMemberRole } from '@/common/model/space/member/SpaceMemberRole' + +interface Props { + spaceUuid?: string // 是否在空间模式下 +} + +interface IQuerySearch { + puuid?: string + keyword?: string + orderName?: SortDirection + orderSize?: SortDirection + orderUpdateTime?: SortDirection +} + +/* + * 考虑函数式还是面向对象来解决函数封装问题 + * 答:将二者结合起来进行使用 + * */ + +const List = ({ spaceUuid }: Props) => { + const { user, preference, updateCapacity } = useContext(GlobalContext) + const p = new Preference(preference) + 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 [selectedMatterUuids, setSelectedMatterUuids] = useState([]) + + const { previewFile } = usePreviewer() + const { openModal: openDeleteModal, closeModal: closeDeleteModal } = useModal(MatterDeleteModal) + const [urlQuery, setUrlQuery] = useUrlState({ + puuid: Matter.MATTER_ROOT, + }) + + const fetchData = async ({ + current, + pageSize, + }: Params[0]): Promise<{ + list: IMatter[] + total: number + }> => { + // search query + // fetch data + + const { puuid, orderName, orderSize, orderUpdateTime, keyword } = urlQuery + const res = await Matter.httpList({ + page: current - 1, + pageSize, + puuid, + orderName, + orderSize, + orderUpdateTime, + name: keyword, + }) + console.log('res', res) + + return { + list: res.data, + total: res.totalItems, + } + } + + 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 handleClickCreateDir = () => { + if (isCreateDir) return + setIsCreateDir(true) + } + + const handleCreateDir = async (name: string) => { + try { + await Matter.httpCreateDirectory({ spaceUuid: matterSpaceUuid, puuid: urlQuery.puuid, name }) + refreshAndUpdateCapacity() + } finally { + setIsCreateDir(false) + } + } + + // 如果在空间下,有些操作权限只对管理员和读写成员开放 + const checkHandlePermission = () => { + if (isInSpace) { + if (!spaceMember) return false + return [SpaceMemberRole.ADMIN, SpaceMemberRole.READ_WRITE].includes(spaceMember.role) + } + return true + } + + const handleDeleteBatch = () => {} + + const getBreadcrumbs = useMemo((): BreadcrumbRoute[] => { + const breadcrumbs: BreadcrumbRoute[] = [] + + // 递归遍历父级,直到根目录parent = 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 : '', + }) + + return breadcrumbs + }, [currentDir]) + + const handleMatterClickRow = (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 handleMatterToggleSelect = (matter: IMatter) => { + const index = selectedMatterUuids.findIndex((uuid) => uuid === matter.uuid) + if (index === -1) { + setSelectedMatterUuids(selectedMatterUuids.concat(matter.uuid)) + } else { + selectedMatterUuids.splice(index, 1) + setSelectedMatterUuids([...selectedMatterUuids]) + } + } + + const handleMatterDelete = (matter: IMatter) => { + const m = new Matter(matter) + openDeleteModal({ + onClose: closeDeleteModal, + onSoftDel: async () => { + await m.httpSoftDelete() + closeDeleteModal() + message.success(Lang.t('operationSuccess')) + refreshAndUpdateCapacity() + }, + onHardDel: async () => { + await m.httpHardDelete() + closeDeleteModal() + message.success(Lang.t('operationSuccess')) + refreshAndUpdateCapacity() + }, + allowSoftDel: !isInSpace && p.getRecycleBinStatus(), + }) + } + + const handleChangeSortFilter = (field: keyof Pick) => { + const sortAutomaton = [SortDirection.ASC, SortDirection.DESC, undefined] // 排序状态自动机:ASC -> DESC -> null -> ASC... + const index = sortAutomaton.findIndex((item) => item === urlQuery[field]) + const nextIndex = (index + 1) % sortAutomaton.length + setUrlQuery({ + [field]: sortAutomaton[nextIndex], + }) + } + + 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) + } + + const handleDownloadZip = () => { + window.open(`${EnvUtil.currentHost()}${Matter.URL_MATTER_ZIP}?uuids=${selectedMatterUuids}&spaceUuid=${spaceUuid}`) + } + + useEffect(() => { + refreshCurrentDirDetail(urlQuery.puuid) // 重新获取当前文件夹信息 + setSelectedMatterUuids([]) // 清空选择项 + }, [urlQuery.puuid]) + + if (isInSpace && !space && !spaceMember) return + + // FIXME 用usecallback包裹是否会有性能提升 + const renderSortIcon = (order?: SortDirection) => { + if (!order) return null + return order === SortDirection.ASC ? : + } + + return ( +
+ {/*todo drag file*/} + {/*{dragEnterCount > 0 ? ( +
+ +
+ ) : null}*/} + + + + {/*handle context*/} + + + + {selectedMatterUuids.length !== data?.list.length && ( + + )} + + {!!data?.list.length && selectedMatterUuids.length === data.list.length && ( + + )} + + {selectedMatterUuids.length > 0 && ( + <> + + {checkHandlePermission() && ( + + )} + + )} + + + + + + + + setUrlQuery({ + keyword: e.target.value, + // todo 考虑非第一页的情况会不会自动跳转到第一页 + }) + } + /> + + + + {/*uploader context*/} + +
+
handleChangeSortFilter('orderName')}> + {Lang.t('matter.fileName')} + {renderSortIcon(urlQuery.orderName)} +
+
handleChangeSortFilter('orderSize')}> + {Lang.t('matter.size')} + {renderSortIcon(urlQuery.orderSize)} +
+
handleChangeSortFilter('orderUpdateTime')}> + {Lang.t('matter.updateDate')} + {renderSortIcon(urlQuery.orderUpdateTime)} +
+
+ +
+ {isCreateDir && handleCreateDir(name)} />} + {data?.list.map((matter) => ( + handleMatterToggleSelect(matter)} + onTogglePrivacy={() => handleMatterTogglePrivacy(matter)} + onDelete={() => handleMatterDelete(matter)} + onSaveName={(name) => handleMatterRename(matter, name)} + onClickRow={() => handleMatterClickRow(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..7d0cb47 --- /dev/null +++ b/src/pages-hook/matter/components/MatterItem.less @@ -0,0 +1,103 @@ +.widget-matter-item { + .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; + .pc-operations .operations { + display: inline-flex; + } + } + + .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; + } + + .pc-operations { + display: flex; + .operations { + display: none; + } + .size { + margin-left: 16px; + width: 80px; + text-align: left; + } + .date { + margin-left: 10px; + } + } + } + + .mobile-operations { + display: block; + border-top: 1px solid #eee; + padding-left: 32px; + cursor: pointer; + background: #fff; + &:hover { + cursor: pointer; + background: aliceblue; + } + .operation-item { + line-height: 36px; + + &:not(:first-child) { + border-top: 1px solid #eee; + } + } + } +} diff --git a/src/pages-hook/matter/components/MatterItem.tsx b/src/pages-hook/matter/components/MatterItem.tsx new file mode 100644 index 0000000..8eff9a2 --- /dev/null +++ b/src/pages-hook/matter/components/MatterItem.tsx @@ -0,0 +1,298 @@ +import React, { useCallback, useContext, useMemo, useRef, useState } from 'react' +import './MatterItem.less' +import { Checkbox, Dropdown, Input, InputRef, Menu, 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' +import { useHistory } from 'react-router' +import { getMatterDetailPath } from '@/utils/link.util' + +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 + selected: boolean + onToggleSelect: () => void + onTogglePrivacy: () => void + onDelete: () => void + onSaveName: (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 + selected: boolean + onToggleSelect: () => void + onDeleted: () => void + onRecovered: () => void +} + +type Props = IDefaultProps | ISpaceProps | ISharedProps | IBinProps + +const MatterItem = (props: Props) => { + const { preference } = useContext(GlobalContext) + const history = useHistory() + 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) => { + if (!node) return + node.value = matter.name + node.focus() + const dotIndex = matter.name.lastIndexOf('.') + node.setSelectionRange(0, dotIndex === -1 ? matter.name.length : dotIndex) + }, []) + + // 空间内文件的部分操作需要权限 + const hasHigherSpacePermission = + from === EMatterItemFrom.Space && [SpaceMemberRole.ADMIN, SpaceMemberRole.READ_WRITE].includes(props.spaceMember.role) + + /* + * 操作项 + * 默认/空间下:设置公私有(空间下校验权限)、文件详情、重命名(空间下校验权限)、复制路径、下载、删除(空间下校验权限)、文件大小、修改时间 + * 分享:下载、文件大小、修改时间 + * 回收站:恢复、文件详情、硬删除、文件大小、软删除时间 + * */ + const canSetPrivacy = (from === EMatterItemFrom.Default || hasHigherSpacePermission) && !matter.dir + const canRecover = from === EMatterItemFrom.Bin + const canGoDetail = [EMatterItemFrom.Default, EMatterItemFrom.Space, EMatterItemFrom.Bin].includes(from) + const canRename = from === EMatterItemFrom.Default || hasHigherSpacePermission + 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 || hasHigherSpacePermission + const canHardDelete = from === EMatterItemFrom.Bin + const canViewUpdateTime = [EMatterItemFrom.Default, EMatterItemFrom.Space, EMatterItemFrom.Shared].includes(from) + const canViewDeleteTime = from === EMatterItemFrom.Bin + + const handleGoDetail = () => { + history.push(getMatterDetailPath(matter)) + } + + const handleBlurInput = (e: React.FocusEvent) => { + if (from !== EMatterItemFrom.Default && from !== EMatterItemFrom.Space) return + const name = e.target.value.trim() + props.onSaveName(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)) + } + + // todo 移到外层去 + const handleHardDelete = () => { + Modal.confirm({ + title: Lang.t('actionCanNotRevertConfirm'), + icon: , + onOk: async () => { + await m.httpHardDelete() + message.success(Lang.t('operationSuccess')) + from === EMatterItemFrom.Bin && props.onDeleted() + }, + }) + } + + // todo 移到外层去 + const handleRecover = () => { + Modal.confirm({ + title: Lang.t('actionRecoveryConfirm'), + icon: , + onOk: async () => { + await m.httpRecover() + message.success(Lang.t('operationSuccess')) + from === EMatterItemFrom.Bin && props.onRecovered() + }, + }) + } + + // todo 是否可以用useCallback对其进行优化 + const getOperations = (platform: 'pc' | 'mobile') => { + const Handles = getMatterItemHandles(platform) + const operations: JSX.Element[] = [] + if (canSetPrivacy) { + operations.push( + matter.privacy ? : , + ) + } + + if (canRecover) operations.push() + if (canGoDetail) operations.push() + if (canRename) operations.push( setInputMode(true)} />) + if (canCopyPath) operations.push() + if (canDownload) operations.push() + if (canDelete) operations.push() + if (canHardDelete) operations.push() + return operations + } + + const renderPcOperations = ( +
+ + {getOperations('pc')} + + + +
{StringUtil.humanFileSize(matter.size)}
+
+ {canViewUpdateTime && ( + +
{DateUtil.simpleDateHourMinute(matter.updateTime)}
+
+ )} + {canViewDeleteTime && ( + +
{DateUtil.simpleDateHourMinute(matter.deleteTime)}
+
+ )} +
+ ) + + const renderMobileOperations = ( +
+
+ + {canViewUpdateTime && DateUtil.simpleDateHourMinute(matter.updateTime)} + {canViewDeleteTime && DateUtil.simpleDateHourMinute(matter.deleteTime)} + {StringUtil.humanFileSize(matter.size)} + +
+ {getOperations('mobile')} +
+ ) + + 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() + } + + const dropdownMenu = ( + + {getOperations('mobile').map((item, i) => ( + {item} + ))} + + ) + + return ( + +
+
+ {(from === EMatterItemFrom.Default || from === EMatterItemFrom.Space || from === EMatterItemFrom.Bin) && ( +
+ +
+ )} + +
+ {matter.name} +
+ +
+ {inputMode ? ( + { + if (e.key.toLowerCase() === 'enter') { + e.currentTarget.blur() + } + }} + onClick={stopPropagation(() => {})} + 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/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/pages/widget/previewer/ImagePreviewer.tsx b/src/pages/widget/previewer/ImagePreviewer.tsx index f8b4ed8..1a8c756 100644 --- a/src/pages/widget/previewer/ImagePreviewer.tsx +++ b/src/pages/widget/previewer/ImagePreviewer.tsx @@ -1,16 +1,11 @@ -import React from 'react'; -import { PhotoSlider } from 'react-photo-view'; -import 'react-photo-view/dist/react-photo-view.css'; -import ReactDOM from 'react-dom'; -import { OverlayRenderProps } from 'react-photo-view/dist/types'; -import { - CloseOutlined, - DownloadOutlined, - RotateRightOutlined, -} from '@ant-design/icons'; -import TankComponent from '../../../common/component/TankComponent'; -import { Space } from 'antd'; -import './ImagePreviewer.less'; +import React from 'react' +import { PhotoSlider } from 'react-photo-view' +import 'react-photo-view/dist/react-photo-view.css' +import ReactDOM from 'react-dom' +import { OverlayRenderProps } from 'react-photo-view/dist/types' +import { CloseOutlined, DownloadOutlined, RotateRightOutlined } from '@ant-design/icons' +import { Space } from 'antd' +import './ImagePreviewer.less' /** * 图片预览器 @@ -18,84 +13,77 @@ import './ImagePreviewer.less'; * 参考文档:https://zhuanlan.zhihu.com/p/473554342 * https://gitee.com/MinJieLiu/react-photo-view */ -export default class ImagePreviewer extends TankComponent { - static initialized: boolean = false; +export default class ImagePreviewer extends React.Component { + static initialized: boolean = false - static singletonRef = React.createRef(); + static singletonRef = React.createRef() //组件的私有变量 - visible: boolean = true; - urls: string[] = []; - index: number = 0; + visible: boolean = true + urls: string[] = [] + index: number = 0 //初始化 private static init() { - let div: HTMLElement = document.createElement('div'); - div.id = 'photo-container'; + let div: HTMLElement = document.createElement('div') + div.id = 'photo-container' //添加到body - document.body.appendChild(div); + document.body.appendChild(div) - ReactDOM.render( - , - document.getElementById('photo-container') - ); + ReactDOM.render(, document.getElementById('photo-container')) } //展示一张图片 static showSinglePhoto(url: string) { - ImagePreviewer.showMultiPhoto([url], 0); + ImagePreviewer.showMultiPhoto([url], 0) } //展示一系列图片 static showMultiPhoto(urls: string[], index: number = 0) { if (!ImagePreviewer.initialized) { - ImagePreviewer.initialized = true; - ImagePreviewer.init(); + ImagePreviewer.initialized = true + ImagePreviewer.init() } - ImagePreviewer.singletonRef.current?.show(urls, index); + ImagePreviewer.singletonRef.current?.show(urls, index) } show(urls: string[], index: number = 0) { - this.urls = urls; - this.index = index; - this.visible = true; - this.updateUI(); + this.urls = urls + this.index = index + this.visible = true + this.setState({}) + // this.updateUI(); } render() { - let toolBar: (overlayProps: OverlayRenderProps) => React.ReactNode = ( - overlayProps: OverlayRenderProps - ) => { + let toolBar: (overlayProps: OverlayRenderProps) => React.ReactNode = (overlayProps: OverlayRenderProps) => { return ( { - const a = document.createElement('a'); - a.setAttribute('download', ''); - a.setAttribute('target', '_blank'); - a.setAttribute('rel', 'noopener'); - a.href = overlayProps.images[overlayProps.index].src!; - document.body.appendChild(a); - a.click(); - a.remove(); + const a = document.createElement('a') + a.setAttribute('download', '') + a.setAttribute('target', '_blank') + a.setAttribute('rel', 'noopener') + a.href = overlayProps.images[overlayProps.index].src! + document.body.appendChild(a) + a.click() + a.remove() }} /> { - overlayProps.onRotate(overlayProps.rotate + 90); + overlayProps.onRotate(overlayProps.rotate + 90) }} /> - overlayProps.onClose()} - /> + overlayProps.onClose()} /> - ); - }; + ) + } return ( { images={this.urls.map((item: string) => ({ src: item, key: item }))} visible={this.visible} onClose={() => { - this.visible = false; - this.updateUI(); + this.visible = false + this.setState({}) }} index={this.index} onIndexChange={(index: number) => { - this.index = index; - this.updateUI(); + this.index = index + this.setState({}) }} toolbarRender={toolBar} /> - ); + ) } } 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/src/utils/link.util.ts b/src/utils/link.util.ts new file mode 100644 index 0000000..19858eb --- /dev/null +++ b/src/utils/link.util.ts @@ -0,0 +1,8 @@ +import { IMatter } from '@/models/Matter' + +/** + * 获取空间详情路径,todo check 都传空间uuid是否可行 + * */ +export const getMatterDetailPath = (matter: IMatter) => { + return `/space/${matter.spaceUuid}/matter/detail/${matter.uuid}` +} 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"