pull/979/head
fcurrk 4 years ago
parent 3c55b59a5f
commit f662dc5f1b

@ -0,0 +1,37 @@
extends:
- eslint:recommended
- plugin:react/recommended
- plugin:@typescript-eslint/recommended
parser: '@typescript-eslint/parser'
plugins:
- react
- react-hooks
- '@typescript-eslint'
parserOptions:
sourceType: module
ecmaVersion: 2020
ecmaFeatures:
jsx: true
env:
es6: true
browser: true
node: true
jest: true
settings:
react:
version: detect
ignorePatterns:
- node_modules
rules:
react/prop-types: 0
react-hooks/rules-of-hooks: "error"
# TODO: 修改添加deps后出现的死循环
react-hooks/exhaustive-deps: 0
'@typescript-eslint/explicit-function-return-type': 0
'@typescript-eslint/no-explicit-any': 0
overrides:
- files: ['*.js', '*.jsx']
rules:
'@typescript-eslint/camelcase': 0

25
assets/.gitignore vendored

@ -0,0 +1,25 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
dist/

@ -0,0 +1 @@
export PATH="/usr/local/bin:$PATH"

@ -0,0 +1,3 @@
{
"tabWidth": 4
}

@ -0,0 +1,8 @@
language: node_js
node_js:
- 12.16.3
before_script:
- yarn install
script:
- CI=false yarn run build
- yarn run test

@ -0,0 +1,68 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `yarn build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify

@ -0,0 +1,93 @@
'use strict';
const fs = require('fs');
const path = require('path');
const paths = require('./paths');
// Make sure that including paths.js after env.js will read .env variables.
delete require.cache[require.resolve('./paths')];
const NODE_ENV = process.env.NODE_ENV;
if (!NODE_ENV) {
throw new Error(
'The NODE_ENV environment variable is required but was not specified.'
);
}
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
const dotenvFiles = [
`${paths.dotenv}.${NODE_ENV}.local`,
`${paths.dotenv}.${NODE_ENV}`,
// Don't include `.env.local` for `test` environment
// since normally you expect tests to produce the same
// results for everyone
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
paths.dotenv,
].filter(Boolean);
// Load environment variables from .env* files. Suppress warnings using silent
// if this file is missing. dotenv will never modify any environment variables
// that have already been set. Variable expansion is supported in .env files.
// https://github.com/motdotla/dotenv
// https://github.com/motdotla/dotenv-expand
dotenvFiles.forEach(dotenvFile => {
if (fs.existsSync(dotenvFile)) {
require('dotenv-expand')(
require('dotenv').config({
path: dotenvFile,
})
);
}
});
// We support resolving modules according to `NODE_PATH`.
// This lets you use absolute paths in imports inside large monorepos:
// https://github.com/facebook/create-react-app/issues/253.
// It works similar to `NODE_PATH` in Node itself:
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
// Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims.
// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
// We also resolve them to make sure all tools using them work consistently.
const appDirectory = fs.realpathSync(process.cwd());
process.env.NODE_PATH = (process.env.NODE_PATH || '')
.split(path.delimiter)
.filter(folder => folder && !path.isAbsolute(folder))
.map(folder => path.resolve(appDirectory, folder))
.join(path.delimiter);
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
// injected into the application via DefinePlugin in Webpack configuration.
const REACT_APP = /^REACT_APP_/i;
function getClientEnvironment(publicUrl) {
const raw = Object.keys(process.env)
.filter(key => REACT_APP.test(key))
.reduce(
(env, key) => {
env[key] = process.env[key];
return env;
},
{
// Useful for determining whether were running in production mode.
// Most importantly, it switches React into the correct mode.
NODE_ENV: process.env.NODE_ENV || 'development',
// Useful for resolving the correct path to static assets in `public`.
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
// This should only be used as an escape hatch. Normally you would put
// images into the `src` and `import` them in code to get their paths.
PUBLIC_URL: publicUrl,
}
);
// Stringify all values so we can feed into Webpack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {}),
};
return { raw, stringified };
}
module.exports = getClientEnvironment;

@ -0,0 +1,14 @@
'use strict';
// This is a custom Jest transformer turning style imports into empty objects.
// http://facebook.github.io/jest/docs/en/webpack.html
module.exports = {
process() {
return 'module.exports = {};';
},
getCacheKey() {
// The output is always the same.
return 'cssTransform';
},
};

@ -0,0 +1,40 @@
'use strict';
const path = require('path');
const camelcase = require('camelcase');
// This is a custom Jest transformer turning file imports into filenames.
// http://facebook.github.io/jest/docs/en/webpack.html
module.exports = {
process(src, filename) {
const assetFilename = JSON.stringify(path.basename(filename));
if (filename.match(/\.svg$/)) {
// Based on how SVGR generates a component name:
// https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
const pascalCaseFilename = camelcase(path.parse(filename).name, {
pascalCase: true,
});
const componentName = `Svg${pascalCaseFilename}`;
return `const React = require('react');
module.exports = {
__esModule: true,
default: ${assetFilename},
ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
return {
$$typeof: Symbol.for('react.element'),
type: 'svg',
ref: ref,
key: null,
props: Object.assign({}, props, {
children: ${assetFilename}
})
};
}),
};`;
}
return `module.exports = ${assetFilename};`;
},
};

@ -0,0 +1,141 @@
'use strict';
const fs = require('fs');
const path = require('path');
const paths = require('./paths');
const chalk = require('react-dev-utils/chalk');
const resolve = require('resolve');
/**
* Get additional module paths based on the baseUrl of a compilerOptions object.
*
* @param {Object} options
*/
function getAdditionalModulePaths(options = {}) {
const baseUrl = options.baseUrl;
// We need to explicitly check for null and undefined (and not a falsy value) because
// TypeScript treats an empty string as `.`.
if (baseUrl == null) {
// If there's no baseUrl set we respect NODE_PATH
// Note that NODE_PATH is deprecated and will be removed
// in the next major release of create-react-app.
const nodePath = process.env.NODE_PATH || '';
return nodePath.split(path.delimiter).filter(Boolean);
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
// We don't need to do anything if `baseUrl` is set to `node_modules`. This is
// the default behavior.
if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
return null;
}
// Allow the user set the `baseUrl` to `appSrc`.
if (path.relative(paths.appSrc, baseUrlResolved) === '') {
return [paths.appSrc];
}
// If the path is equal to the root directory we ignore it here.
// We don't want to allow importing from the root directly as source files are
// not transpiled outside of `src`. We do allow importing them with the
// absolute path (e.g. `src/Components/Button.js`) but we set that up with
// an alias.
if (path.relative(paths.appPath, baseUrlResolved) === '') {
return null;
}
// Otherwise, throw an error.
throw new Error(
chalk.red.bold(
"Your project's `baseUrl` can only be set to `src` or `node_modules`." +
' Create React App does not support other values at this time.'
)
);
}
/**
* Get webpack aliases based on the baseUrl of a compilerOptions object.
*
* @param {*} options
*/
function getWebpackAliases(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return {};
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
if (path.relative(paths.appPath, baseUrlResolved) === '') {
return {
src: paths.appSrc,
};
}
}
/**
* Get jest aliases based on the baseUrl of a compilerOptions object.
*
* @param {*} options
*/
function getJestAliases(options = {}) {
const baseUrl = options.baseUrl;
if (!baseUrl) {
return {};
}
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
if (path.relative(paths.appPath, baseUrlResolved) === '') {
return {
'src/(.*)$': '<rootDir>/src/$1',
};
}
}
function getModules() {
// Check if TypeScript is setup
const hasTsConfig = fs.existsSync(paths.appTsConfig);
const hasJsConfig = fs.existsSync(paths.appJsConfig);
if (hasTsConfig && hasJsConfig) {
throw new Error(
'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
);
}
let config;
// If there's a tsconfig.json we assume it's a
// TypeScript project and set up the config
// based on tsconfig.json
if (hasTsConfig) {
const ts = require(resolve.sync('typescript', {
basedir: paths.appNodeModules,
}));
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
// Otherwise we'll check if there is jsconfig.json
// for non TS projects.
} else if (hasJsConfig) {
config = require(paths.appJsConfig);
}
config = config || {};
const options = config.compilerOptions || {};
const additionalModulePaths = getAdditionalModulePaths(options);
return {
additionalModulePaths: additionalModulePaths,
webpackAliases: getWebpackAliases(options),
jestAliases: getJestAliases(options),
hasTsConfig,
};
}
module.exports = getModules();

@ -0,0 +1,90 @@
'use strict';
const path = require('path');
const fs = require('fs');
const url = require('url');
// Make sure any symlinks in the project folder are resolved:
// https://github.com/facebook/create-react-app/issues/637
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
const envPublicUrl = process.env.PUBLIC_URL;
function ensureSlash(inputPath, needsSlash) {
const hasSlash = inputPath.endsWith('/');
if (hasSlash && !needsSlash) {
return inputPath.substr(0, inputPath.length - 1);
} else if (!hasSlash && needsSlash) {
return `${inputPath}/`;
} else {
return inputPath;
}
}
const getPublicUrl = appPackageJson =>
envPublicUrl || require(appPackageJson).homepage;
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
// "public path" at which the app is served.
// Webpack needs to know it to put the right <script> hrefs into HTML even in
// single-page apps that may serve index.html for nested URLs like /todos/42.
// We can't use a relative path in HTML because we don't want to load something
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
function getServedPath(appPackageJson) {
const publicUrl = getPublicUrl(appPackageJson);
const servedUrl =
envPublicUrl || (publicUrl ? url.parse(publicUrl).pathname : '/');
return ensureSlash(servedUrl, true);
}
const moduleFileExtensions = [
'web.mjs',
'mjs',
'web.js',
'js',
'web.ts',
'ts',
'web.tsx',
'tsx',
'json',
'web.jsx',
'jsx',
];
// Resolve file paths in the same order as webpack
const resolveModule = (resolveFn, filePath) => {
const extension = moduleFileExtensions.find(extension =>
fs.existsSync(resolveFn(`${filePath}.${extension}`))
);
if (extension) {
return resolveFn(`${filePath}.${extension}`);
}
return resolveFn(`${filePath}.js`);
};
// config after eject: we're in ./config/
module.exports = {
dotenv: resolveApp('.env'),
appPath: resolveApp('.'),
appBuild: resolveApp('build'),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
appTsConfig: resolveApp('tsconfig.json'),
appJsConfig: resolveApp('jsconfig.json'),
yarnLockFile: resolveApp('yarn.lock'),
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
publicUrl: getPublicUrl(resolveApp('package.json')),
servedPath: getServedPath(resolveApp('package.json')),
};
module.exports.moduleFileExtensions = moduleFileExtensions;

@ -0,0 +1,35 @@
'use strict';
const { resolveModuleName } = require('ts-pnp');
exports.resolveModuleName = (
typescript,
moduleName,
containingFile,
compilerOptions,
resolutionHost
) => {
return resolveModuleName(
moduleName,
containingFile,
compilerOptions,
resolutionHost,
typescript.resolveModuleName
);
};
exports.resolveTypeReferenceDirective = (
typescript,
moduleName,
containingFile,
compilerOptions,
resolutionHost
) => {
return resolveModuleName(
moduleName,
containingFile,
compilerOptions,
resolutionHost,
typescript.resolveTypeReferenceDirective
);
};

@ -0,0 +1,716 @@
'use strict';
const fs = require('fs');
const isWsl = require('is-wsl');
const path = require('path');
const webpack = require('webpack');
const resolve = require('resolve');
const PnpWebpackPlugin = require('pnp-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const TerserPlugin = require('terser-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const safePostCssParser = require('postcss-safe-parser');
const ManifestPlugin = require('webpack-manifest-plugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const paths = require('./paths');
const modules = require('./modules');
const getClientEnvironment = require('./env');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
const eslint = require('eslint');
const CopyPlugin = require('copy-webpack-plugin');
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
const postcssNormalize = require('postcss-normalize');
const appPackageJson = require(paths.appPackageJson);
// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
// Some apps do not need the benefits of saving a web request, so not inlining the chunk
// makes for a smoother build process.
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
const imageInlineSizeLimit = parseInt(
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);
// Check if TypeScript is setup
const useTypeScript = fs.existsSync(paths.appTsConfig);
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
// This is the production and development configuration.
// It is focused on developer experience, fast rebuilds, and a minimal bundle.
module.exports = function(webpackEnv) {
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
// Variable used for enabling profiling in Production
// passed into alias object. Uses a flag if passed into the build command
const isEnvProductionProfile =
isEnvProduction && process.argv.includes('--profile');
// Webpack uses `publicPath` to determine where the app is being served from.
// It requires a trailing slash, or the file assets will get an incorrect path.
// In development, we always serve from the root. This makes config easier.
const publicPath = isEnvProduction
? paths.servedPath
: isEnvDevelopment && '/';
// Some apps do not use client-side routing with pushState.
// For these, "homepage" can be set to "." to enable relative asset paths.
const shouldUseRelativeAssetPaths = publicPath === './';
// `publicUrl` is just like `publicPath`, but we will provide it to our app
// as %PUBLIC_URL% in `index.html` and `process.env.PUBLIC_URL` in JavaScript.
// Omit trailing slash as %PUBLIC_URL%/xyz looks better than %PUBLIC_URL%xyz.
const publicUrl = isEnvProduction
? publicPath.slice(0, -1)
: isEnvDevelopment && '';
// Get environment variables to inject into our app.
const env = getClientEnvironment(publicUrl);
// common function to get style loaders
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
options: shouldUseRelativeAssetPaths ? { publicPath: '../../' } : {},
},
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
{
// Options for PostCSS as we reference these options twice
// Adds vendor prefixing based on your specified browser support in
// package.json
loader: require.resolve('postcss-loader'),
options: {
// Necessary for external CSS imports to work
// https://github.com/facebook/create-react-app/issues/2677
ident: 'postcss',
plugins: () => [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
}),
// Adds PostCSS Normalize as the reset css with default options,
// so that it honors browserslist config in package.json
// which in turn let's users customize the target behavior as per their needs.
postcssNormalize(),
],
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
].filter(Boolean);
if (preProcessor) {
loaders.push(
{
loader: require.resolve('resolve-url-loader'),
options: {
sourceMap: isEnvProduction && shouldUseSourceMap,
},
},
{
loader: require.resolve(preProcessor),
options: {
sourceMap: true,
},
}
);
}
return loaders;
};
return {
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
// Stop compilation early in production
bail: isEnvProduction,
devtool: isEnvProduction
? shouldUseSourceMap
? 'source-map'
: false
: isEnvDevelopment && 'cheap-module-source-map',
// These are the "entry points" to our application.
// This means they will be the "root" imports that are included in JS bundle.
entry: [
// Include an alternative client for WebpackDevServer. A client's job is to
// connect to WebpackDevServer by a socket and get notified about changes.
// When you save a file, the client will either apply hot updates (in case
// of CSS changes), or refresh the page (in case of JS changes). When you
// make a syntax error, this client will display a syntax error overlay.
// Note: instead of the default WebpackDevServer client, we use a custom one
// to bring better experience for Create React App users. You can replace
// the line below with these two lines if you prefer the stock client:
// require.resolve('webpack-dev-server/client') + '?/',
// require.resolve('webpack/hot/dev-server'),
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
// Finally, this is your app's code:
paths.appIndexJs,
// We include the app code last so that if there is a runtime error during
// initialization, it doesn't blow up the WebpackDevServer client, and
// changing JS code would still trigger a refresh.
].filter(Boolean),
output: {
// The build folder.
path: isEnvProduction ? paths.appBuild : undefined,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
// TODO: remove this when upgrading to webpack 5
futureEmitAssets: true,
// There are also additional JS chunk files if you use code splitting.
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
// We inferred the "public path" (such as / or /my-project) from homepage.
// We use "/" in development.
publicPath: publicPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
// Prevents conflicts when multiple Webpack runtimes (from different apps)
// are used on the same page.
jsonpFunction: `webpackJsonp${appPackageJson.name}`,
// this defaults to 'window', but by setting it to 'this' then
// module chunks which are built will work in web workers as well.
globalObject: 'this',
},
optimization: {
minimize: isEnvProduction,
minimizer: [
// This is only used in production mode
new TerserPlugin({
terserOptions: {
parse: {
// We want terser to parse ecma 8 code. However, we don't want it
// to apply any minification steps that turns valid ecma 5 code
// into invalid ecma 5 code. This is why the 'compress' and 'output'
// sections only apply transformations that are ecma 5 safe
// https://github.com/facebook/create-react-app/pull/4234
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
// Disabled because of an issue with Uglify breaking seemingly valid code:
// https://github.com/facebook/create-react-app/issues/2376
// Pending further investigation:
// https://github.com/mishoo/UglifyJS2/issues/2011
comparisons: false,
// Disabled because of an issue with Terser breaking valid code:
// https://github.com/facebook/create-react-app/issues/5250
// Pending further investigation:
// https://github.com/terser-js/terser/issues/120
inline: 2,
},
mangle: {
safari10: true,
},
// Added for profiling in devtools
keep_classnames: isEnvProductionProfile,
keep_fnames: isEnvProductionProfile,
output: {
ecma: 5,
comments: false,
// Turned on because emoji and regex is not minified properly using default
// https://github.com/facebook/create-react-app/issues/2488
ascii_only: true,
},
},
// Use multi-process parallel running to improve the build speed
// Default number of concurrent runs: os.cpus().length - 1
// Disabled on WSL (Windows Subsystem for Linux) due to an issue with Terser
// https://github.com/webpack-contrib/terser-webpack-plugin/issues/21
parallel: !isWsl,
// Enable file caching
cache: true,
sourceMap: shouldUseSourceMap,
}),
// This is only used in production mode
new OptimizeCSSAssetsPlugin({
cssProcessorOptions: {
parser: safePostCssParser,
map: shouldUseSourceMap
? {
// `inline: false` forces the sourcemap to be output into a
// separate file
inline: false,
// `annotation: true` appends the sourceMappingURL to the end of
// the css file, helping the browser find the sourcemap
annotation: true,
}
: false,
},
}),
],
// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
splitChunks: {
chunks: 'all',
name: false,
cacheGroups: {
monacoCommon: {
test: /[\\/]node_modules[\\/]monaco\-editor/,
name: 'monaco-editor-common',
chunks: 'async'
},
pdfCommon: {
test: /[\\/]node_modules[\\/](react\-pdf|pdfjs\-dist)/,
name: 'react-pdf',
chunks: 'async'
}
},
},
// Keep the runtime chunk separated to enable long term caching
// https://twitter.com/wSokra/status/969679223278505985
// https://github.com/facebook/create-react-app/issues/5358
runtimeChunk: {
name: entrypoint => `runtime-${entrypoint.name}`,
},
},
resolve: {
// This allows you to set a fallback for where Webpack should look for modules.
// We placed these paths second because we want `node_modules` to "win"
// if there are any conflicts. This matches Node resolution mechanism.
// https://github.com/facebook/create-react-app/issues/253
modules: ['node_modules', paths.appNodeModules].concat(
modules.additionalModulePaths || []
),
// These are the reasonable defaults supported by the Node ecosystem.
// We also include JSX as a common component filename extension to support
// some tools, although we do not recommend using it, see:
// https://github.com/facebook/create-react-app/issues/290
// `web` extension prefixes have been added for better support
// for React Native Web.
extensions: paths.moduleFileExtensions
.map(ext => `.${ext}`)
.filter(ext => useTypeScript || !ext.includes('ts')),
alias: {
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
'react-native': 'react-native-web',
// Allows for better profiling with ReactDevTools
...(isEnvProductionProfile && {
'react-dom$': 'react-dom/profiling',
'scheduler/tracing': 'scheduler/tracing-profiling',
}),
...(modules.webpackAliases || {}),
},
plugins: [
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
// guards against forgotten dependencies and such.
PnpWebpackPlugin,
// Prevents users from importing files from outside of src/ (or node_modules/).
// This often causes confusion because we only process files within src/ with babel.
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
// please link the files into your node_modules/ and let module-resolution kick in.
// Make sure your source files are compiled, as they will not be processed in any way.
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
],
},
resolveLoader: {
plugins: [
// Also related to Plug'n'Play, but this time it tells Webpack to load its loaders
// from the current package.
PnpWebpackPlugin.moduleLoader(module),
],
},
module: {
strictExportPresence: true,
rules: [
// Disable require.ensure as it's not a standard language feature.
{ parser: { requireEnsure: false } },
// First, run the linter.
// It's important to do this before Babel processes the JS.
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
enforce: 'pre',
use: [
{
options: {
cache: true,
formatter: require.resolve('react-dev-utils/eslintFormatter'),
eslintPath: require.resolve('eslint'),
resolvePluginsRelativeTo: __dirname,
},
loader: require.resolve('eslint-loader'),
},
],
include: paths.appSrc,
},
{
// "oneOf" will traverse all following loaders until one will
// match the requirements. When no loader matches it will fall
// back to the "file" loader at the end of the loader list.
oneOf: [
// "url" loader works like "file" loader except that it embeds assets
// smaller than specified limit in bytes as data URLs to avoid requests.
// A missing `test` is equivalent to a match.
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
limit: imageInlineSizeLimit,
name: 'static/media/[name].[hash:8].[ext]',
},
},
// Process application JS with Babel.
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
customize: require.resolve(
'babel-preset-react-app/webpack-overrides'
),
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent:
'@svgr/webpack?-svgo,+titleProp,+ref![path]',
},
},
},
],
],
// This is a feature of `babel-loader` for webpack (not Babel itself).
// It enables caching results in ./node_modules/.cache/babel-loader/
// directory for faster rebuilds.
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
compact: isEnvProduction,
},
},
// Process any JS outside of the app with Babel.
// Unlike the application JS, we only compile the standard ES features.
{
test: /\.(js|mjs)$/,
exclude: /@babel(?:\/|\\{1,2})runtime/,
loader: require.resolve('babel-loader'),
options: {
babelrc: false,
configFile: false,
compact: false,
presets: [
[
require.resolve('babel-preset-react-app/dependencies'),
{ helpers: true },
],
],
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
// If an error happens in a package, it's possible to be
// because it was compiled. Thus, we don't want the browser
// debugger to show the original code. Instead, the code
// being evaluated would be much more helpful.
sourceMaps: false,
},
},
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader turns CSS into JS modules that inject <style> tags.
// In production, we use MiniCSSExtractPlugin to extract that CSS
// to a file, but in development "style" loader enables hot editing
// of CSS.
// By default we support CSS Modules with the extension .module.css
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
}),
},
// Opt-in support for SASS (using .scss or .sass extensions).
// By default we support SASS Modules with the
// extensions .module.scss or .module.sass
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
},
'sass-loader'
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
},
'sass-loader'
),
},
// "file" loader makes sure those assets get served by WebpackDevServer.
// When you `import` an asset, you get its (virtual) filename.
// In production, they would get copied to the `build` folder.
// This loader doesn't use a "test" so it will catch all modules
// that fall through the other loaders.
{
loader: require.resolve('file-loader'),
// Exclude `js` files to keep "css" loader working as it injects
// its runtime that would otherwise be processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
// ** STOP ** Are you adding a new loader?
// Make sure to add the new loader(s) before the "file" loader.
],
},
],
},
plugins: [
// Monaco 代码编辑器
new MonacoWebpackPlugin({
// available options are documented at https://github.com/Microsoft/monaco-editor-webpack-plugin#options
languages: ['json',"php","bat","cpp","csharp","css","dockerfile","go","html","ini","java","javascript","less","lua","shell","sql","xml","yaml","python"]
}),
// 写入版本文件
new CopyPlugin([
{
from: 'package.json',
to: 'version.json',
transform(content, path) {
let contentJson = JSON.parse(content);
return JSON.stringify({
version:contentJson.version,
name:contentJson.name,
})
},
},
]),
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
// Inlines the webpack runtime script. This script is too small to warrant
// a network request.
// https://github.com/facebook/create-react-app/issues/5358
isEnvProduction &&
shouldInlineRuntimeChunk &&
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
// Makes some environment variables available in index.html.
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
// In production, it will be an empty string unless you specify "homepage"
// in `package.json`, in which case it will be the pathname of that URL.
// In development, this will be an empty string.
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
// This gives some necessary context to module not found errors, such as
// the requesting resource.
new ModuleNotFoundPlugin(paths.appPath),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
// It is absolutely essential that NODE_ENV is set to production
// during a production build.
// Otherwise React will be compiled in the very slow development mode.
new webpack.DefinePlugin(env.stringified),
// This is necessary to emit hot updates (currently CSS only):
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
// Watcher doesn't work well if you mistype casing in a path so we use
// a plugin that prints an error when you attempt to do this.
// See https://github.com/facebook/create-react-app/issues/240
isEnvDevelopment && new CaseSensitivePathsPlugin(),
// If you require a missing module and then `npm install` it, you still have
// to restart the development server for Webpack to discover it. This plugin
// makes the discovery automatic so you don't have to restart.
// See https://github.com/facebook/create-react-app/issues/186
isEnvDevelopment &&
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
isEnvProduction &&
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
}),
// Generate an asset manifest file with the following content:
// - "files" key: Mapping of all asset filenames to their corresponding
// output file so that tools can pick it up without having to parse
// `index.html`
// - "entrypoints" key: Array of files which are included in `index.html`,
// can be used to reconstruct the HTML if necessary
new ManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: publicPath,
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
const entrypointFiles = entrypoints.main.filter(
fileName => !fileName.endsWith('.map')
);
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
// Moment.js is an extremely popular library that bundles large locale files
// by default due to how Webpack interprets its code. This is a practical
// solution that requires the user to opt into importing specific locales.
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
// You can remove this if you don't use Moment.js:
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
// Generate a service worker script that will precache, and keep up to date,
// the HTML & assets that are part of the Webpack build.
isEnvProduction &&
new WorkboxWebpackPlugin.GenerateSW({
clientsClaim: true,
exclude: [/\.map$/, /asset-manifest\.json$/,/\.ttf$/,/version\.json$/,/\.worker\.js$/],
importWorkboxFrom: 'cdn',
excludeChunks:["monaco-editor-common","pdf","react-pdf"],
navigateFallback: publicUrl + '/index.html',
navigateFallbackBlacklist: [
// Exclude URLs starting with /_, as they're likely an API call
new RegExp('^/_'),
// Exclude any URLs whose last part seems to be a file extension
// as they're likely a resource and not a SPA route.
// URLs containing a "?" character won't be blacklisted as they're likely
// a route with query params (e.g. auth callbacks).
new RegExp('/[^/?]+\\.[^/]+$'),
new RegExp(/^\/api/),
new RegExp(/^\/custom/),
],
}),
// TypeScript type checking
useTypeScript &&
new ForkTsCheckerWebpackPlugin({
typescript: resolve.sync('typescript', {
basedir: paths.appNodeModules,
}),
async: isEnvDevelopment,
useTypescriptIncrementalApi: true,
checkSyntacticErrors: true,
resolveModuleNameModule: process.versions.pnp
? `${__dirname}/pnpTs.js`
: undefined,
resolveTypeReferenceDirectiveModule: process.versions.pnp
? `${__dirname}/pnpTs.js`
: undefined,
tsconfig: paths.appTsConfig,
reportFiles: [
'**',
'!**/__tests__/**',
'!**/?(*.)(spec|test).*',
'!**/src/setupProxy.*',
'!**/src/setupTests.*',
],
silent: true,
// The formatter is invoked directly in WebpackDevServerUtils during development
formatter: isEnvProduction ? typescriptFormatter : undefined,
}),
].filter(Boolean),
// Some libraries import Node modules but don't use them in the browser.
// Tell Webpack to provide empty mocks for them so importing them works.
node: {
module: 'empty',
dgram: 'empty',
dns: 'mock',
fs: 'empty',
http2: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
// Turn off performance processing because we utilize
// our own hints via the FileSizeReporter
performance: false,
};
};

@ -0,0 +1,104 @@
"use strict";
const errorOverlayMiddleware = require("react-dev-utils/errorOverlayMiddleware");
const evalSourceMapMiddleware = require("react-dev-utils/evalSourceMapMiddleware");
const noopServiceWorkerMiddleware = require("react-dev-utils/noopServiceWorkerMiddleware");
const ignoredFiles = require("react-dev-utils/ignoredFiles");
const paths = require("./paths");
const fs = require("fs");
const protocol = process.env.HTTPS === "true" ? "https" : "http";
const host = process.env.HOST || "0.0.0.0";
module.exports = function (proxy, allowedHost) {
return {
// WebpackDevServer 2.4.3 introduced a security fix that prevents remote
// websites from potentially accessing local content through DNS rebinding:
// https://github.com/webpack/webpack-dev-server/issues/887
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
// However, it made several existing use cases such as development in cloud
// environment or subdomains in development significantly more complicated:
// https://github.com/facebook/create-react-app/issues/2271
// https://github.com/facebook/create-react-app/issues/2233
// While we're investigating better solutions, for now we will take a
// compromise. Since our WDS configuration only serves files in the `public`
// folder we won't consider accessing them a vulnerability. However, if you
// use the `proxy` feature, it gets more dangerous because it can expose
// remote code execution vulnerabilities in backends like Django and Rails.
// So we will disable the host check normally, but enable it if you have
// specified the `proxy` setting. Finally, we let you override it if you
// really know what you're doing with a special environment variable.
disableHostCheck:
!proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === "true",
// Enable gzip compression of generated files.
compress: true,
// Silence WebpackDevServer's own logs since they're generally not useful.
// It will still show compile warnings and errors with this setting.
clientLogLevel: "none",
// By default WebpackDevServer serves physical files from current directory
// in addition to all the virtual build products that it serves from memory.
// This is confusing because those files wont automatically be available in
// production build folder unless we copy them. However, copying the whole
// project directory is dangerous because we may expose sensitive files.
// Instead, we establish a convention that only files in `public` directory
// get served. Our build script will copy `public` into the `build` folder.
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
// Note that we only recommend to use `public` folder as an escape hatch
// for files like `favicon.ico`, `manifest.json`, and libraries that are
// for some reason broken when imported through Webpack. If you just want to
// use an image, put it in `src` and `import` it from JavaScript instead.
contentBase: paths.appPublic,
// By default files from `contentBase` will not trigger a page reload.
watchContentBase: true,
// Enable hot reloading server. It will provide /sockjs-node/ endpoint
// for the WebpackDevServer client so it can learn when the files were
// updated. The WebpackDevServer client is included as an entry point
// in the Webpack development configuration. Note that only changes
// to CSS are currently hot reloaded. JS changes will refresh the browser.
hot: true,
// It is important to tell WebpackDevServer to use the same "root" path
// as we specified in the config. In development, we always serve from /.
publicPath: "/",
// WebpackDevServer is noisy by default so we emit custom message instead
// by listening to the compiler events with `compiler.hooks[...].tap` calls above.
quiet: true,
// Reportedly, this avoids CPU overload on some systems.
// https://github.com/facebook/create-react-app/issues/293
// src/node_modules is not ignored to support absolute imports
// https://github.com/facebook/create-react-app/issues/1065
watchOptions: {
ignored: ignoredFiles(paths.appSrc),
},
// Enable HTTPS if the HTTPS environment variable is set to 'true'
https: protocol === "https",
host,
overlay: false,
historyApiFallback: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebook/create-react-app/issues/387.
disableDotRule: true,
},
public: allowedHost,
proxy,
before(app, server) {
if (fs.existsSync(paths.proxySetup)) {
// This registers user provided middleware for proxy reasons
require(paths.proxySetup)(app);
}
// This lets us fetch source contents from webpack for the error overlay
app.use(evalSourceMapMiddleware(server));
// This lets us open files from the runtime error overlay.
app.use(errorOverlayMiddleware());
// This service worker file is effectively a 'no-op' that will reset any
// previous service worker registered for the same host:port combination.
// We do this in development to avoid hitting the production cache if
// it used the same host and port.
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
app.use(noopServiceWorkerMiddleware("/"));
},
};
};

@ -0,0 +1,188 @@
{
"name": "cloudreve-frontend",
"version": "3.3.2",
"private": true,
"dependencies": {
"@babel/core": "7.6.0",
"@material-ui/core": "^4.9.0",
"@material-ui/icons": "^4.5.1",
"@material-ui/lab": "^4.0.0-alpha.42",
"@svgr/webpack": "4.3.2",
"@types/invariant": "^2.2.32",
"@types/jest": "^25.2.2",
"@types/node": "^14.0.1",
"@types/react": "^16.9.35",
"@types/react-dom": "^16.9.8",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"axios": "^0.21.1",
"babel-eslint": "10.0.3",
"babel-jest": "^24.9.0",
"babel-loader": "8.0.6",
"babel-plugin-named-asset-import": "^0.3.4",
"babel-preset-react-app": "^9.0.2",
"camelcase": "^5.2.0",
"case-sensitive-paths-webpack-plugin": "2.2.0",
"classnames": "^2.2.6",
"clsx": "latest",
"connected-react-router": "^6.9.1",
"css-loader": "2.1.1",
"dayjs": "^1.10.4",
"dotenv": "6.2.0",
"dotenv-expand": "5.1.0",
"eslint": "^6.8.0",
"eslint-config-react-app": "^5.0.2",
"eslint-loader": "3.0.2",
"eslint-plugin-flowtype": "3.13.0",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "^7.19.0",
"file-loader": "3.0.1",
"for-editor": "^0.3.5",
"fs-extra": "7.0.1",
"html-webpack-plugin": "4.0.0-beta.5",
"http-proxy-middleware": "^0.20.0",
"husky": "^4.2.5",
"identity-obj-proxy": "3.0.0",
"invariant": "^2.2.4",
"is-wsl": "^1.1.0",
"jest": "24.9.0",
"jest-environment-jsdom-fourteen": "0.1.0",
"jest-resolve": "24.9.0",
"jest-watch-typeahead": "0.4.0",
"material-ui-toggle-icon": "^1.1.1",
"mdi-material-ui": "^6.9.0",
"mini-css-extract-plugin": "0.8.0",
"monaco-editor-webpack-plugin": "^3.0.0",
"optimize-css-assets-webpack-plugin": "5.0.3",
"pnp-webpack-plugin": "1.5.0",
"postcss-flexbugs-fixes": "4.1.0",
"postcss-loader": "3.0.0",
"postcss-normalize": "7.0.1",
"postcss-preset-env": "6.7.0",
"postcss-safe-parser": "4.0.1",
"qrcode-react": "^0.1.16",
"react": "^16.12.0",
"react-addons-update": "^15.6.2",
"react-app-polyfill": "^1.0.4",
"react-async-script": "^1.1.1",
"react-color": "^2.18.0",
"react-content-loader": "^5.0.2",
"react-dev-utils": "^11.0.4",
"react-dnd": "^9.5.1",
"react-dnd-html5-backend": "^9.5.1",
"react-dom": "^16.12.0",
"react-dplayer": "^0.4.1",
"react-hotkeys": "^2.0.0",
"react-lazy-load-image-component": "^1.3.2",
"react-load-script": "^0.0.6",
"react-monaco-editor": "^0.36.0",
"react-pdf": "^4.1.0",
"react-photo-view": "^0.4.0",
"react-redux": "^7.1.3",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"recharts": "^2.0.6",
"redux": "^4.0.4",
"redux-mock-store": "^1.5.4",
"redux-thunk": "^2.3.0",
"resolve": "1.12.0",
"resolve-url-loader": "3.1.0",
"sass-loader": "7.2.0",
"semver": "6.3.0",
"style-loader": "1.0.0",
"terser-webpack-plugin": "1.4.1",
"timeago-react": "^3.0.0",
"ts-pnp": "1.1.4",
"typescript": "^3.9.2",
"url-loader": "2.1.0",
"webpack": "4.41.0",
"webpack-dev-server": "3.2.1",
"webpack-manifest-plugin": "2.1.1",
"workbox-webpack-plugin": "4.3.1"
},
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js",
"eslint": "eslint src --fix",
"postinstall": "node node_modules/husky/lib/installer/bin install"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"jest": {
"roots": [
"<rootDir>/src"
],
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}",
"!src/**/*.d.ts"
],
"setupFiles": [
"react-app-polyfill/jsdom"
],
"setupFilesAfterEnv": [],
"testMatch": [
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
],
"testEnvironment": "jest-environment-jsdom-fourteen",
"transform": {
"^.+\\.(js|jsx|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
"^(?!.*\\.(js|jsx|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
},
"transformIgnorePatterns": [
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|ts|tsx)$",
"^.+\\.module\\.(css|sass|scss)$"
],
"modulePaths": [],
"moduleNameMapper": {
"^react-native$": "react-native-web",
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy"
},
"moduleFileExtensions": [
"web.js",
"js",
"web.ts",
"ts",
"web.tsx",
"tsx",
"json",
"web.jsx",
"jsx",
"node"
],
"watchPlugins": [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname"
]
},
"babel": {
"presets": [
"react-app"
]
},
"devDependencies": {
"copy-webpack-plugin": "^5.1.1",
"eslint-plugin-react-hooks": "^4.0.0"
},
"husky": {
"hooks": {
"pre-commit": "yarn run eslint"
}
},
"resolutions": {
"@types/react": "^16.9.35"
}
}

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="{pwa_small_icon}" sizes="64x64"/>
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,shrink-to-fit=no"
/>
<meta name="theme-color" content="" />
<link rel="manifest" href="/manifest.json" />
<meta name="description" content="{siteDes}">
<title>{siteName}</title>
<script>
window.subTitle = "{siteName}";
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
{siteScript}
</html>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@ -0,0 +1,40 @@
// Chinese (China) (zh_CN)
plupload.addI18n({
"Stop Upload": "停止上传",
"Upload URL might be wrong or doesn't exist.":
"上传的URL可能是错误的或不存在。",
tb: "tb",
Size: "大小",
Close: "关闭",
"Init error.": "初始化错误。",
"Add files to the upload queue and click the start button.":
"将文件添加到上传队列,然后点击”开始上传“按钮。",
Filename: "文件名",
"Image format either wrong or not supported.": "图片格式错误或者不支持。",
Status: "状态",
"HTTP Error.": "HTTP 错误。",
"Start Upload": "开始上传",
mb: "mb",
kb: "kb",
"Duplicate file error.": "重复文件错误。",
"File size error.": "文件大小错误。",
"N/A": "N/A",
gb: "gb",
"Error: Invalid file extension:": "错误:无效的文件扩展名:",
"Select files": "选择文件",
"%s already present in the queue.": "%s 已经在当前队列里。",
"File: %s": "文件: %s",
b: "b",
"Uploaded %d/%d files": "已上传 %d/%d 个文件",
"Upload element accepts only %d file(s) at a time. Extra files were stripped.":
"每次只接受同时上传 %d 个文件,多余的文件将会被删除。",
"%d files queued": "%d 个文件加入到队列",
"File: %s, size: %d, max file size: %d":
"文件: %s, 大小: %d, 最大文件大小: %d",
"Drag files here.": "把文件拖到这里。",
"Runtime ran out of available memory.": "运行时已消耗所有可用内存。",
"File count error.": "文件数量错误。",
"File extension error.": "文件扩展名错误。",
"Error: File too large:": "错误: 文件太大:",
"Add Files": "增加文件",
});

@ -0,0 +1,268 @@
// /*global Qiniu */
// /*global plupload */
// /*global FileProgress */
// /*global hljs */
// function getCookieByString(cookieName){
// var start = document.cookie.indexOf(cookieName+'=');
// if (start == -1) return false;
// start = start+cookieName.length+1;
// var end = document.cookie.indexOf(';', start);
// if (end == -1) end=document.cookie.length;
// return document.cookie.substring(start, end);
// }
// if(uploadConfig.saveType == "oss" || uploadConfig.saveType == "upyun" || uploadConfig.saveType == "s3"){
// ChunkSize = "0";
// }else{
// ChunkSize = "4mb";
// }
// uploader = Qiniu.uploader({
// runtimes: 'html5,flash,html4',
// browse_button: 'pickfiles',
// container: 'container',
// drop_element: 'container',
// max_file_size: uploadConfig.maxSize,
// flash_swf_url: '/bower_components/plupload/js/Moxie.swf',
// dragdrop: true,
// chunk_size: ChunkSize,
// filters: {
// mime_types :uploadConfig.allowedType,
// },
// multi_selection: !(moxie.core.utils.Env.OS.toLowerCase() === "ios"),
// uptoken_url: "/Upload/Token",
// // uptoken_func: function(){
// // var ajax = new XMLHttpRequest();
// // ajax.open('GET', $('#uptoken_url').val(), false);
// // ajax.setRequestHeader("If-Modified-Since", "0");
// // ajax.send();
// // if (ajax.status === 200) {
// // var res = JSON.parse(ajax.responseText);
// // console.log('custom uptoken_func:' + res.uptoken);
// // return res.uptoken;
// // } else {
// // console.log('custom uptoken_func err');
// // return '';
// // }
// // },
// domain: $('#domain').val(),
// get_new_uptoken: true,
// // downtoken_url: '/downtoken',
// // unique_names: true,
// // save_key: true,
// // x_vars: {
// // 'id': '1234',
// // 'time': function(up, file) {
// // var time = (new Date()).getTime();
// // // do something with 'time'
// // return time;
// // },
// // },
// auto_start: true,
// log_level: 5,
// init: {
// 'FilesAdded': function(up, files) {
// $('table').show();
// $('#upload_box').show();
// $('#success').hide();
// $('#info_box').hide();
// $.cookie('path', decodeURI(getCookieByString("path_tmp")));
// plupload.each(files, function(file) {
// var progress = new FileProgress(file, 'fsUploadProgress');
// progress.setStatus("等待...");
// progress.bindUploadCancel(up);
// });
// },
// 'BeforeUpload': function(up, file) {
// var progress = new FileProgress(file, 'fsUploadProgress');
// var chunk_size = plupload.parseSize(this.getOption('chunk_size'));
// if (up.runtime === 'html5' && chunk_size) {
// progress.setChunkProgess(chunk_size);
// }
// },
// 'UploadProgress': function(up, file) {
// var progress = new FileProgress(file, 'fsUploadProgress');
// var chunk_size = plupload.parseSize(this.getOption('chunk_size'));
// progress.setProgress(file.percent + "%", file.speed, chunk_size);
// },
// 'UploadComplete': function(up, file) {
// $('#success').show();
// toastr["success"]("队列全部文件处理完毕");
// getMemory();
// },
// 'FileUploaded': function(up, file, info) {
// var progress = new FileProgress(file, 'fsUploadProgress');
// progress.setComplete(up, info);
// },
// 'Error': function(up, err, errTip) {
// $('#upload_box').show();
// $('table').show();
// $('#info_box').hide();
// var progress = new FileProgress(err.file, 'fsUploadProgress');
// progress.setError();
// progress.setStatus(errTip);
// toastr["error"]("上传时遇到错误");
// }
// // ,
// // 'Key': function(up, file) {
// // var key = "";
// // // do something with key
// // return key
// // }
// }
// });
// uploader.bind('FileUploaded', function(up,file) {
// console.log('a file is uploaded');
// });
// $('#container').on(
// 'dragenter',
// function(e) {
// e.preventDefault();
// $('#container').addClass('draging');
// e.stopPropagation();
// }
// ).on('drop', function(e) {
// e.preventDefault();
// $('#container').removeClass('draging');
// e.stopPropagation();
// }).on('dragleave', function(e) {
// e.preventDefault();
// $('#container').removeClass('draging');
// e.stopPropagation();
// }).on('dragover', function(e) {
// e.preventDefault();
// $('#container').addClass('draging');
// e.stopPropagation();
// });
// $('#show_code').on('click', function() {
// $('#myModal-code').modal();
// $('pre code').each(function(i, e) {
// hljs.highlightBlock(e);
// });
// });
// $('body').on('click', 'table button.btn', function() {
// $(this).parents('tr').next().toggle();
// });
// var getRotate = function(url) {
// if (!url) {
// return 0;
// }
// var arr = url.split('/');
// for (var i = 0, len = arr.length; i < len; i++) {
// if (arr[i] === 'rotate') {
// return parseInt(arr[i + 1], 10);
// }
// }
// return 0;
// };
// $('#myModal-img .modal-body-footer').find('a').on('click', function() {
// var img = $('#myModal-img').find('.modal-body img');
// var key = img.data('key');
// var oldUrl = img.attr('src');
// var originHeight = parseInt(img.data('h'), 10);
// var fopArr = [];
// var rotate = getRotate(oldUrl);
// if (!$(this).hasClass('no-disable-click')) {
// $(this).addClass('disabled').siblings().removeClass('disabled');
// if ($(this).data('imagemogr') !== 'no-rotate') {
// fopArr.push({
// 'fop': 'imageMogr2',
// 'auto-orient': true,
// 'strip': true,
// 'rotate': rotate,
// 'format': 'png'
// });
// }
// } else {
// $(this).siblings().removeClass('disabled');
// var imageMogr = $(this).data('imagemogr');
// if (imageMogr === 'left') {
// rotate = rotate - 90 < 0 ? rotate + 270 : rotate - 90;
// } else if (imageMogr === 'right') {
// rotate = rotate + 90 > 360 ? rotate - 270 : rotate + 90;
// }
// fopArr.push({
// 'fop': 'imageMogr2',
// 'auto-orient': true,
// 'strip': true,
// 'rotate': rotate,
// 'format': 'png'
// });
// }
// $('#myModal-img .modal-body-footer').find('a.disabled').each(function() {
// var watermark = $(this).data('watermark');
// var imageView = $(this).data('imageview');
// var imageMogr = $(this).data('imagemogr');
// if (watermark) {
// fopArr.push({
// fop: 'watermark',
// mode: 1,
// image: 'http://www.b1.qiniudn.com/images/logo-2.png',
// dissolve: 100,
// gravity: watermark,
// dx: 100,
// dy: 100
// });
// }
// if (imageView) {
// var height;
// switch (imageView) {
// case 'large':
// height = originHeight;
// break;
// case 'middle':
// height = originHeight * 0.5;
// break;
// case 'small':
// height = originHeight * 0.1;
// break;
// default:
// height = originHeight;
// break;
// }
// fopArr.push({
// fop: 'imageView2',
// mode: 3,
// h: parseInt(height, 10),
// q: 100,
// format: 'png'
// });
// }
// if (imageMogr === 'no-rotate') {
// fopArr.push({
// 'fop': 'imageMogr2',
// 'auto-orient': true,
// 'strip': true,
// 'rotate': 0,
// 'format': 'png'
// });
// }
// });
// var newUrl = Qiniu.pipeline(fopArr, key);
// var newImg = new Image();
// img.attr('src', 'images/loading.gif');
// newImg.onload = function() {
// img.attr('src', newUrl);
// img.parent('a').attr('href', newUrl);
// };
// newImg.src = newUrl;
// return false;
// });
// function t(){
// uploader.getNewUpToken();
// }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,323 @@
/*global plupload */
/*global qiniu */
function FileProgress(file, targetID) {
this.fileProgressID = file.id;
this.file = file;
this.opacity = 100;
this.height = 0;
this.fileProgressWrapper = $("#" + this.fileProgressID);
if (!this.fileProgressWrapper.length) {
// <div class="progress">
// <div class="progress-bar progress-bar-info" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100" style="width: 20%">
// <span class="sr-only">20% Complete</span>
// </div>
// </div>
this.fileProgressWrapper = $("<tr></tr>");
var Wrappeer = this.fileProgressWrapper;
Wrappeer.attr("id", this.fileProgressID).addClass("progressContainer");
var progressText = $("<td/>");
progressText.addClass("progressName").text(file.name);
var fileSize = plupload.formatSize(file.size).toUpperCase();
var progressSize = $("<td/>");
progressSize.addClass("progressFileSize").text(fileSize);
var progressBarTd = $("<td/>");
var progressBarBox = $("<div/>");
progressBarBox.addClass("info");
var progressBarWrapper = $("<div/>");
progressBarWrapper.addClass("progress progress-striped");
var progressBar = $("<div/>");
progressBar
.addClass("progress-bar progress-bar-info")
.attr("role", "progressbar")
.attr("aria-valuemax", 100)
.attr("aria-valuenow", 0)
.attr("aria-valuein", 0)
.width("0%");
var progressBarPercent = $("<span class=sr-only />");
progressBarPercent.text(fileSize);
var progressCancel = $("<a href=javascript:; />");
progressCancel.show().addClass("progressCancel").text("×");
progressBar.append(progressBarPercent);
progressBarWrapper.append(progressBar);
progressBarBox.append(progressBarWrapper);
progressText.append(progressCancel);
var progressBarStatus = $('<div class="status text-center"/>');
progressBarBox.append(progressBarStatus);
progressBarTd.append(progressBarBox);
Wrappeer.append(progressText);
Wrappeer.append(progressSize);
Wrappeer.append(progressBarTd);
$("#" + targetID).append(Wrappeer);
} else {
this.reset();
}
this.height = this.fileProgressWrapper.offset().top;
this.setTimer(null);
}
FileProgress.prototype.setTimer = function (timer) {
this.fileProgressWrapper.FP_TIMER = timer;
};
FileProgress.prototype.getTimer = function (timer) {
return this.fileProgressWrapper.FP_TIMER || null;
};
FileProgress.prototype.reset = function () {
this.fileProgressWrapper.attr("class", "progressContainer");
this.fileProgressWrapper
.find("td .progress .progress-bar-info")
.attr("aria-valuenow", 0)
.width("0%")
.find("span")
.text("");
this.appear();
};
FileProgress.prototype.setChunkProgess = function (chunk_size) {
var chunk_amount = Math.ceil(this.file.size / chunk_size);
if (chunk_amount === 1) {
return false;
}
var viewProgess = $(
'<button class="btn btn-default">查看分块上传进度</button>'
);
var progressBarChunkTr = $(
'<tr class="chunk-status-tr"><td colspan=3></td></tr>'
);
var progressBarChunk = $("<div/>");
for (var i = 1; i <= chunk_amount; i++) {
var col = $('<div class="col-md-2"/>');
var progressBarWrapper = $(
'<div class="progress progress-striped"></div'
);
var progressBar = $("<div/>");
progressBar
.addClass("progress-bar progress-bar-info text-left")
.attr("role", "progressbar")
.attr("aria-valuemax", 100)
.attr("aria-valuenow", 0)
.attr("aria-valuein", 0)
.width("0%")
.attr("id", this.file.id + "_" + i)
.text("");
var progressBarStatus = $("<span/>");
progressBarStatus.addClass("chunk-status").text();
progressBarWrapper.append(progressBar);
progressBarWrapper.append(progressBarStatus);
col.append(progressBarWrapper);
progressBarChunk.append(col);
}
if (!this.fileProgressWrapper.find("td:eq(2) .btn-default").length) {
this.fileProgressWrapper.find("td>div").append(viewProgess);
}
progressBarChunkTr.hide().find("td").append(progressBarChunk);
progressBarChunkTr.insertAfter(this.fileProgressWrapper);
};
FileProgress.prototype.setProgress = function (percentage, speed, chunk_size) {
this.fileProgressWrapper.attr("class", "progressContainer green");
var file = this.file;
var uploaded = file.loaded;
var size = plupload.formatSize(uploaded).toUpperCase();
var formatSpeed = plupload.formatSize(speed).toUpperCase();
var progressbar = this.fileProgressWrapper
.find("td .progress")
.find(".progress-bar-info");
if (this.fileProgressWrapper.find(".status").text() === "取消上传") {
return;
}
this.fileProgressWrapper
.find(".status")
.text("已上传: " + size + " 上传速度: " + formatSpeed + "/s");
percentage = parseInt(percentage, 10);
if (file.status !== plupload.DONE && percentage === 100) {
percentage = 99;
}
progressbar
.attr("aria-valuenow", percentage)
.css("width", percentage + "%");
if (chunk_size) {
var chunk_amount = Math.ceil(file.size / chunk_size);
if (chunk_amount === 1) {
return false;
}
var current_uploading_chunk = Math.ceil(uploaded / chunk_size);
var pre_chunk, text;
for (var index = 0; index < current_uploading_chunk; index++) {
pre_chunk = $("#" + file.id + "_" + index);
pre_chunk
.width("100%")
.removeClass()
.addClass("alert alert-success")
.attr("aria-valuenow", 100);
text = "块" + index + "上传进度100%";
pre_chunk.next().html(text);
}
var currentProgessBar = $(
"#" + file.id + "_" + current_uploading_chunk
);
var current_chunk_percent;
if (current_uploading_chunk < chunk_amount) {
if (uploaded % chunk_size) {
current_chunk_percent = (
((uploaded % chunk_size) / chunk_size) *
100
).toFixed(2);
} else {
current_chunk_percent = 100;
currentProgessBar.removeClass().addClass("alert alert-success");
}
} else {
var last_chunk_size = file.size - chunk_size * (chunk_amount - 1);
var left_file_size = file.size - uploaded;
if (left_file_size % last_chunk_size) {
current_chunk_percent = (
((uploaded % chunk_size) / last_chunk_size) *
100
).toFixed(2);
} else {
current_chunk_percent = 100;
currentProgessBar.removeClass().addClass("alert alert-success");
}
}
currentProgessBar.width(current_chunk_percent + "%");
currentProgessBar.attr("aria-valuenow", current_chunk_percent);
text =
"块" +
current_uploading_chunk +
"上传进度" +
current_chunk_percent +
"%";
currentProgessBar.next().html(text);
}
this.appear();
};
FileProgress.prototype.setComplete = function (up, info) {
var td = this.fileProgressWrapper.find("td:eq(2)"),
tdProgress = td.find(".progress");
var res;
var url;
if (uploadConfig.saveType == "oss") {
url = "oss";
str = "<div class='success_text'>上传成功</div>";
} else {
res = $.parseJSON(info);
if (res.url) {
url = res.url;
str = "<div class='success_text'>上传成功</div>";
} else {
var domain = up.getOption("domain");
url = domain + encodeURI(res.key);
var link = domain + res.key;
str = "<div class='success_text'>上传成功</div>";
}
}
tdProgress.html(str).removeClass().next().next(".status").hide();
this.fileProgressWrapper.find("td:eq(0) .progressCancel").hide();
td.find(".status").hide();
angular
.element(document.querySelector("angular-filemanager > div"))
.scope()
.fileNavigator.refresh();
};
FileProgress.prototype.setError = function () {
this.fileProgressWrapper.find("td:eq(2)").attr("class", "text-warning");
this.fileProgressWrapper.find("td:eq(2) .progress").css("width", 0).hide();
this.fileProgressWrapper.find("button").hide();
this.fileProgressWrapper.next(".chunk-status-tr").hide();
};
FileProgress.prototype.setCancelled = function (manual) {
var progressContainer = "progressContainer";
if (!manual) {
progressContainer += " red";
}
this.fileProgressWrapper.attr("class", progressContainer);
this.fileProgressWrapper.find("td .progress").remove();
this.fileProgressWrapper.find("td:eq(2) .btn-default").hide();
this.fileProgressWrapper.find("td:eq(0) .progressCancel").hide();
};
FileProgress.prototype.setStatus = function (status, isUploading) {
if (!isUploading) {
this.fileProgressWrapper
.find(".status")
.text(status)
.attr("class", "status text-left");
}
};
// 绑定取消上传事件
FileProgress.prototype.bindUploadCancel = function (up) {
var self = this;
if (up) {
self.fileProgressWrapper
.find("td:eq(0) .progressCancel")
.on("click", function () {
self.setCancelled(false);
self.setStatus("取消上传");
self.fileProgressWrapper.find(".status").css("left", "0");
up.removeFile(self.file);
});
}
};
FileProgress.prototype.appear = function () {
if (this.getTimer() !== null) {
clearTimeout(this.getTimer());
this.setTimer(null);
}
if (this.fileProgressWrapper[0].filters) {
try {
this.fileProgressWrapper[0].filters.item(
"DXImageTransform.Microsoft.Alpha"
).opacity = 100;
} catch (e) {
// If it is not set initially, the browser will throw an error. This will set it if it is not set yet.
this.fileProgressWrapper.css(
"filter",
"progid:DXImageTransform.Microsoft.Alpha(opacity=100)"
);
}
} else {
this.fileProgressWrapper.css("opacity", 1);
}
this.fileProgressWrapper.css("height", "");
this.height = this.fileProgressWrapper.offset().top;
this.opacity = 100;
this.fileProgressWrapper.show();
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,199 @@
'use strict';
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'production';
process.env.NODE_ENV = 'production';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');
const path = require('path');
const chalk = require('react-dev-utils/chalk');
const fs = require('fs-extra');
const webpack = require('webpack');
const configFactory = require('../config/webpack.config');
const paths = require('../config/paths');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
const printBuildError = require('react-dev-utils/printBuildError');
const measureFileSizesBeforeBuild =
FileSizeReporter.measureFileSizesBeforeBuild;
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
const useYarn = fs.existsSync(paths.yarnLockFile);
// These sizes are pretty large. We'll warn for bundles exceeding them.
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
const isInteractive = process.stdout.isTTY;
// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
// Generate configuration
const config = configFactory('production');
// We require that you explicitly set browsers and do not fall back to
// browserslist defaults.
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
// First, read the current file sizes in build directory.
// This lets us display how much they changed later.
return measureFileSizesBeforeBuild(paths.appBuild);
})
.then(previousFileSizes => {
// Remove all content but keep the directory so that
// if you're in it, you don't end up in Trash
fs.emptyDirSync(paths.appBuild);
// Merge with the public folder
copyPublicFolder();
// Start the webpack build
return build(previousFileSizes);
})
.then(
({ stats, previousFileSizes, warnings }) => {
if (warnings.length) {
console.log(chalk.yellow('Compiled with warnings.\n'));
console.log(warnings.join('\n\n'));
console.log(
'\nSearch for the ' +
chalk.underline(chalk.yellow('keywords')) +
' to learn more about each warning.'
);
console.log(
'To ignore, add ' +
chalk.cyan('// eslint-disable-next-line') +
' to the line before.\n'
);
} else {
console.log(chalk.green('Compiled successfully.\n'));
}
console.log('File sizes after gzip:\n');
printFileSizesAfterBuild(
stats,
previousFileSizes,
paths.appBuild,
WARN_AFTER_BUNDLE_GZIP_SIZE,
WARN_AFTER_CHUNK_GZIP_SIZE
);
console.log();
const appPackage = require(paths.appPackageJson);
const publicUrl = paths.publicUrl;
const publicPath = config.output.publicPath;
const buildFolder = path.relative(process.cwd(), paths.appBuild);
printHostingInstructions(
appPackage,
publicUrl,
publicPath,
buildFolder,
useYarn
);
},
err => {
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
if (tscCompileOnError) {
console.log(chalk.yellow(
'Compiled with the following type errors (you may want to check these before deploying your app):\n'
));
printBuildError(err);
} else {
console.log(chalk.red('Failed to compile.\n'));
printBuildError(err);
process.exit(1);
}
}
)
.catch(err => {
if (err && err.message) {
console.log(err.message);
}
process.exit(1);
});
// Create the production build and print the deployment instructions.
function build(previousFileSizes) {
// We used to support resolving modules according to `NODE_PATH`.
// This now has been deprecated in favor of jsconfig/tsconfig.json
// This lets you use absolute paths in imports inside large monorepos:
if (process.env.NODE_PATH) {
console.log(
chalk.yellow(
'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
)
);
console.log();
}
console.log('Creating an optimized production build...');
const compiler = webpack(config);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
let messages;
if (err) {
if (!err.message) {
return reject(err);
}
messages = formatWebpackMessages({
errors: [err.message],
warnings: [],
});
} else {
messages = formatWebpackMessages(
stats.toJson({ all: false, warnings: true, errors: true })
);
}
if (messages.errors.length) {
// Only keep the first error. Others are often indicative
// of the same problem, but confuse the reader with noise.
if (messages.errors.length > 1) {
messages.errors.length = 1;
}
return reject(new Error(messages.errors.join('\n\n')));
}
if (
process.env.CI &&
(typeof process.env.CI !== 'string' ||
process.env.CI.toLowerCase() !== 'false') &&
messages.warnings.length
) {
console.log(
chalk.yellow(
'\nTreating warnings as errors because process.env.CI = true.\n' +
'Most CI servers set it automatically.\n'
)
);
return reject(new Error(messages.warnings.join('\n\n')));
}
return resolve({
stats,
previousFileSizes,
warnings: messages.warnings,
});
});
});
}
function copyPublicFolder() {
fs.copySync(paths.appPublic, paths.appBuild, {
dereference: true,
filter: file => file !== paths.appHtml,
});
}

@ -0,0 +1,147 @@
'use strict';
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');
const fs = require('fs');
const chalk = require('react-dev-utils/chalk');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const clearConsole = require('react-dev-utils/clearConsole');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const {
choosePort,
createCompiler,
prepareProxy,
prepareUrls,
} = require('react-dev-utils/WebpackDevServerUtils');
const openBrowser = require('react-dev-utils/openBrowser');
const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
const createDevServerConfig = require('../config/webpackDevServer.config');
const useYarn = fs.existsSync(paths.yarnLockFile);
const isInteractive = process.stdout.isTTY;
// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
// Tools like Cloud9 rely on this.
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0';
if (process.env.HOST) {
console.log(
chalk.cyan(
`Attempting to bind to HOST environment variable: ${chalk.yellow(
chalk.bold(process.env.HOST)
)}`
)
);
console.log(
`If this was unintentional, check that you haven't mistakenly set it in your shell.`
);
console.log(
`Learn more here: ${chalk.yellow('https://bit.ly/CRA-advanced-config')}`
);
console.log();
}
// We require that you explicitly set browsers and do not fall back to
// browserslist defaults.
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
// We attempt to use the default port but if it is busy, we offer the user to
// run on a different port. `choosePort()` Promise resolves to the next free port.
return choosePort(HOST, DEFAULT_PORT);
})
.then(port => {
if (port == null) {
// We have not found a port.
return;
}
const config = configFactory('development');
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
const appName = require(paths.appPackageJson).name;
const useTypeScript = fs.existsSync(paths.appTsConfig);
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
const urls = prepareUrls(protocol, HOST, port);
const devSocket = {
warnings: warnings =>
devServer.sockWrite(devServer.sockets, 'warnings', warnings),
errors: errors =>
devServer.sockWrite(devServer.sockets, 'errors', errors),
};
// Create a webpack compiler that is configured with custom messages.
const compiler = createCompiler({
appName,
config,
devSocket,
urls,
useYarn,
useTypeScript,
tscCompileOnError,
webpack,
});
// Load proxy config
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(proxySetting, paths.appPublic);
// Serve webpack assets generated by the compiler over a web server.
const serverConfig = createDevServerConfig(
proxyConfig,
urls.lanUrlForConfig
);
const devServer = new WebpackDevServer(compiler, serverConfig);
// Launch WebpackDevServer.
devServer.listen(port, HOST, err => {
if (err) {
return console.log(err);
}
if (isInteractive) {
clearConsole();
}
// We used to support resolving modules according to `NODE_PATH`.
// This now has been deprecated in favor of jsconfig/tsconfig.json
// This lets you use absolute paths in imports inside large monorepos:
if (process.env.NODE_PATH) {
console.log(
chalk.yellow(
'Setting NODE_PATH to resolve modules absolutely has been deprecated in favor of setting baseUrl in jsconfig.json (or tsconfig.json if you are using TypeScript) and will be removed in a future major release of create-react-app.'
)
);
console.log();
}
console.log(chalk.cyan('Starting the development server...\n'));
openBrowser(urls.localUrlForBrowser);
});
['SIGINT', 'SIGTERM'].forEach(function(sig) {
process.on(sig, function() {
devServer.close();
process.exit();
});
});
})
.catch(err => {
if (err && err.message) {
console.log(err.message);
}
process.exit(1);
});

@ -0,0 +1,53 @@
'use strict';
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'test';
process.env.NODE_ENV = 'test';
process.env.PUBLIC_URL = '';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', err => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');
const jest = require('jest');
const execSync = require('child_process').execSync;
let argv = process.argv.slice(2);
function isInGitRepository() {
try {
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
function isInMercurialRepository() {
try {
execSync('hg --cwd . root', { stdio: 'ignore' });
return true;
} catch (e) {
return false;
}
}
// Watch unless on CI or explicitly running all tests
if (
!process.env.CI &&
argv.indexOf('--watchAll') === -1 &&
argv.indexOf('--watchAll=false') === -1
) {
// https://github.com/facebook/create-react-app/issues/5210
const hasSourceControl = isInGitRepository() || isInMercurialRepository();
argv.push(hasSourceControl ? '--watch' : '--watchAll');
}
jest.run(argv);

@ -0,0 +1,194 @@
import React, { useEffect, useState } from "react";
import { CssBaseline, makeStyles } from "@material-ui/core";
import AlertBar from "./component/Common/Snackbar";
import Dashboard from "./component/Admin/Dashboard";
import { useHistory } from "react-router";
import Auth from "./middleware/Auth";
import { Route, Switch } from "react-router-dom";
import { ThemeProvider } from "@material-ui/styles";
import createMuiTheme from "@material-ui/core/styles/createMuiTheme";
import { zhCN } from "@material-ui/core/locale";
import Index from "./component/Admin/Index";
import SiteInformation from "./component/Admin/Setting/SiteInformation";
import Access from "./component/Admin/Setting/Access";
import Mail from "./component/Admin/Setting/Mail";
import UploadDownload from "./component/Admin/Setting/UploadDownload";
import Theme from "./component/Admin/Setting/Theme";
import Aria2 from "./component/Admin/Setting/Aria2";
import ImageSetting from "./component/Admin/Setting/Image";
import Policy from "./component/Admin/Policy/Policy";
import AddPolicy from "./component/Admin/Policy/AddPolicy";
import EditPolicyPreload from "./component/Admin/Policy/EditPolicy";
import Group from "./component/Admin/Group/Group";
import GroupForm from "./component/Admin/Group/GroupForm";
import EditGroupPreload from "./component/Admin/Group/EditGroup";
import User from "./component/Admin/User/User";
import UserForm from "./component/Admin/User/UserForm";
import EditUserPreload from "./component/Admin/User/EditUser";
import File from "./component/Admin/File/File";
import Share from "./component/Admin/Share/Share";
import Download from "./component/Admin/Task/Download";
import Task from "./component/Admin/Task/Task";
import Import from "./component/Admin/File/Import";
import Captcha from "./component/Admin/Setting/Captcha";
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
},
content: {
flexGrow: 1,
padding: 0,
minWidth: 0,
},
toolbar: theme.mixins.toolbar,
}));
const theme = createMuiTheme(
{
palette: {
background: {},
},
},
zhCN
);
export default function Admin() {
const classes = useStyles();
const history = useHistory();
const [show, setShow] = useState(false);
useEffect(() => {
const user = Auth.GetUser();
if (user && user.group) {
if (user.group.id !== 1) {
history.push("/home");
return;
}
setShow(true);
return;
}
history.push("/login");
// eslint-disable-next-line
}, []);
return (
<React.Fragment>
<ThemeProvider theme={theme}>
<div className={classes.root}>
<CssBaseline />
<AlertBar />
{show && (
<Dashboard
content={(path) => (
<Switch>
<Route path={`${path}/home`} exact>
<Index />
</Route>
<Route path={`${path}/basic`}>
<SiteInformation />
</Route>
<Route path={`${path}/access`}>
<Access />
</Route>
<Route path={`${path}/mail`}>
<Mail />
</Route>
<Route path={`${path}/upload`}>
<UploadDownload />
</Route>
<Route path={`${path}/theme`}>
<Theme />
</Route>
<Route path={`${path}/aria2`}>
<Aria2 />
</Route>
<Route path={`${path}/image`}>
<ImageSetting />
</Route>
<Route path={`${path}/captcha`}>
<Captcha />
</Route>
<Route path={`${path}/policy`} exact>
<Policy />
</Route>
<Route
path={`${path}/policy/add/:type`}
exact
>
<AddPolicy />
</Route>
<Route
path={`${path}/policy/edit/:mode/:id`}
exact
>
<EditPolicyPreload />
</Route>
<Route path={`${path}/group`} exact>
<Group />
</Route>
<Route path={`${path}/group/add`} exact>
<GroupForm />
</Route>
<Route
path={`${path}/group/edit/:id`}
exact
>
<EditGroupPreload />
</Route>
<Route path={`${path}/user`} exact>
<User />
</Route>
<Route path={`${path}/user/add`} exact>
<UserForm />
</Route>
<Route path={`${path}/user/edit/:id`} exact>
<EditUserPreload />
</Route>
<Route path={`${path}/file`} exact>
<File />
</Route>
<Route path={`${path}/file/import`} exact>
<Import />
</Route>
<Route path={`${path}/share`} exact>
<Share />
</Route>
<Route path={`${path}/download`} exact>
<Download />
</Route>
<Route path={`${path}/task`} exact>
<Task />
</Route>
</Switch>
)}
/>
)}
</div>
</ThemeProvider>
</React.Fragment>
);
}

@ -0,0 +1,214 @@
import React, { Suspense } from "react";
import AuthRoute from "./middleware/AuthRoute";
import Navbar from "./component/Navbar/Navbar.js";
import useMediaQuery from "@material-ui/core/useMediaQuery";
import AlertBar from "./component/Common/Snackbar";
import { createMuiTheme, lighten } from "@material-ui/core/styles";
import { useSelector } from "react-redux";
import { Redirect, Route, Switch, useRouteMatch } from "react-router-dom";
import Auth from "./middleware/Auth";
import { CssBaseline, makeStyles, ThemeProvider } from "@material-ui/core";
import { changeThemeColor } from "./utils";
import NotFound from "./component/Share/NotFound";
// Lazy loads
import LoginForm from "./component/Login/LoginForm";
import FileManager from "./component/FileManager/FileManager.js";
import VideoPreview from "./component/Viewer/Video.js";
import SearchResult from "./component/Share/SearchResult";
import MyShare from "./component/Share/MyShare";
import Download from "./component/Download/Download";
import SharePreload from "./component/Share/SharePreload";
import DocViewer from "./component/Viewer/Doc";
import TextViewer from "./component/Viewer/Text";
import WebDAV from "./component/Setting/WebDAV";
import Tasks from "./component/Setting/Tasks";
import Profile from "./component/Setting/Profile";
import UserSetting from "./component/Setting/UserSetting";
import Register from "./component/Login/Register";
import Activation from "./component/Login/Activication";
import ResetForm from "./component/Login/ResetForm";
import Reset from "./component/Login/Reset";
import PageLoading from "./component/Placeholder/PageLoading";
import CodeViewer from "./component/Viewer/Code";
const PDFViewer = React.lazy(() =>
import(/* webpackChunkName: "pdf" */ "./component/Viewer/PDF")
);
export default function App() {
const themeConfig = useSelector((state) => state.siteConfig.theme);
const isLogin = useSelector((state) => state.viewUpdate.isLogin);
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
const theme = React.useMemo(() => {
themeConfig.palette.type = prefersDarkMode ? "dark" : "light";
const prefer = Auth.GetPreference("theme_mode");
if (prefer) {
themeConfig.palette.type = prefer;
}
const theme = createMuiTheme({
...themeConfig,
palette: {
...themeConfig.palette,
primary: {
...themeConfig.palette.primary,
main:
themeConfig.palette.type === "dark"
? lighten(themeConfig.palette.primary.main, 0.3)
: themeConfig.palette.primary.main,
},
},
});
changeThemeColor(
themeConfig.palette.type === "dark"
? theme.palette.background.default
: theme.palette.primary.main
);
return theme;
}, [prefersDarkMode, themeConfig]);
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
},
content: {
flexGrow: 1,
padding: theme.spacing(0),
minWidth: 0,
},
toolbar: theme.mixins.toolbar,
}));
const classes = useStyles();
const { path } = useRouteMatch();
return (
<React.Fragment>
<ThemeProvider theme={theme}>
<div className={classes.root} id="container">
<CssBaseline />
<AlertBar />
<Navbar />
<main className={classes.content}>
<div className={classes.toolbar} />
<Switch>
<AuthRoute exact path={path} isLogin={isLogin}>
<Redirect
to={{
pathname: "/home",
}}
/>
</AuthRoute>
<AuthRoute path={`${path}home`} isLogin={isLogin}>
<FileManager />
</AuthRoute>
<AuthRoute path={`${path}video`} isLogin={isLogin}>
<VideoPreview />
</AuthRoute>
<AuthRoute path={`${path}text`} isLogin={isLogin}>
<TextViewer />
</AuthRoute>
<AuthRoute path={`${path}doc`} isLogin={isLogin}>
<DocViewer />
</AuthRoute>
<AuthRoute path={`${path}pdf`} isLogin={isLogin}>
<Suspense fallback={<PageLoading />}>
<PDFViewer />
</Suspense>
</AuthRoute>
<AuthRoute path={`${path}code`} isLogin={isLogin}>
<CodeViewer />
</AuthRoute>
<AuthRoute path={`${path}aria2`} isLogin={isLogin}>
<Download />
</AuthRoute>
<AuthRoute path={`${path}shares`} isLogin={isLogin}>
<MyShare />
</AuthRoute>
<Route path={`${path}search`} isLogin={isLogin}>
<SearchResult />
</Route>
<Route path={`${path}setting`} isLogin={isLogin}>
<UserSetting />
</Route>
<AuthRoute
path={`${path}profile/:id`}
isLogin={isLogin}
>
<Profile />
</AuthRoute>
<AuthRoute path={`${path}webdav`} isLogin={isLogin}>
<WebDAV />
</AuthRoute>
<AuthRoute path={`${path}tasks`} isLogin={isLogin}>
<Tasks />
</AuthRoute>
<Route path={`${path}login`} exact>
<LoginForm />
</Route>
<Route path={`${path}signup`} exact>
<Register />
</Route>
<Route path={`${path}activate`} exact>
<Activation />
</Route>
<Route path={`${path}reset`} exact>
<ResetForm />
</Route>
<Route path={`${path}forget`} exact>
<Reset />
</Route>
<Route exact path={`${path}s/:id`}>
<SharePreload />
</Route>
<Route path={`${path}s/:id/video(/)*`}>
<VideoPreview />
</Route>
<Route path={`${path}s/:id/doc(/)*`}>
<DocViewer />
</Route>
<Route path={`${path}s/:id/text(/)*`}>
<TextViewer />
</Route>
<Route path={`${path}s/:id/pdf(/)*`}>
<Suspense fallback={<PageLoading />}>
<PDFViewer />
</Suspense>
</Route>
<Route path={`${path}s/:id/code(/)*`}>
<CodeViewer />
</Route>
<Route path="*">
<NotFound msg={"页面不存在"} />
</Route>
</Switch>
</main>
</div>
</ThemeProvider>
</React.Fragment>
);
}

@ -0,0 +1,240 @@
import { isMac } from "../utils";
import pathHelper from "../utils/page";
import Auth from "../middleware/Auth";
import {
changeContextMenu,
openLoadingDialog,
openMusicDialog,
showImgPreivew,
toggleSnackbar,
} from "./index";
import { isPreviewable } from "../config";
import { push } from "connected-react-router";
export const removeSelectedTargets = (fileIds) => {
return {
type: "RMOVE_SELECTED_TARGETS",
fileIds,
};
};
export const addSelectedTargets = (targets) => {
return {
type: "ADD_SELECTED_TARGETS",
targets,
};
};
export const setSelectedTarget = (targets) => {
return {
type: "SET_SELECTED_TARGET",
targets,
};
};
export const setLastSelect = (file, index) => {
return {
type: "SET_LAST_SELECT",
file,
index,
};
};
export const setShiftSelectedIds = (shiftSelectedIds) => {
return {
type: "SET_SHIFT_SELECTED_IDS",
shiftSelectedIds,
};
};
export const openPreview = () => {
return (dispatch, getState) => {
const {
explorer: { selected },
router: {
location: { pathname },
},
} = getState();
const isShare = pathHelper.isSharePage(pathname);
if (isShare) {
const user = Auth.GetUser();
if (!Auth.Check() && user && !user.group.shareDownload) {
dispatch(toggleSnackbar("top", "right", "请先登录", "warning"));
dispatch(changeContextMenu("file", false));
return;
}
}
dispatch(changeContextMenu("file", false));
const previewPath =
selected[0].path === "/"
? selected[0].path + selected[0].name
: selected[0].path + "/" + selected[0].name;
switch (isPreviewable(selected[0].name)) {
case "img":
dispatch(showImgPreivew(selected[0]));
return;
case "msDoc":
if (isShare) {
dispatch(
push(
selected[0].key +
"/doc?name=" +
encodeURIComponent(selected[0].name) +
"&share_path=" +
encodeURIComponent(previewPath)
)
);
return;
}
dispatch(
push(
"/doc?p=" +
encodeURIComponent(previewPath) +
"&id=" +
selected[0].id
)
);
return;
case "audio":
dispatch(openMusicDialog());
return;
case "video":
if (isShare) {
dispatch(
push(
selected[0].key +
"/video?name=" +
encodeURIComponent(selected[0].name) +
"&share_path=" +
encodeURIComponent(previewPath)
)
);
return;
}
dispatch(
push(
"/video?p=" +
encodeURIComponent(previewPath) +
"&id=" +
selected[0].id
)
);
return;
case "pdf":
if (isShare) {
dispatch(
push(
selected[0].key +
"/pdf?name=" +
encodeURIComponent(selected[0].name) +
"&share_path=" +
encodeURIComponent(previewPath)
)
);
return;
}
dispatch(
push(
"/pdf?p=" +
encodeURIComponent(previewPath) +
"&id=" +
selected[0].id
)
);
return;
case "edit":
if (isShare) {
dispatch(
push(
selected[0].key +
"/text?name=" +
encodeURIComponent(selected[0].name) +
"&share_path=" +
encodeURIComponent(previewPath)
)
);
return;
}
dispatch(
push(
"/text?p=" +
encodeURIComponent(previewPath) +
"&id=" +
selected[0].id
)
);
return;
case "code":
if (isShare) {
dispatch(
push(
selected[0].key +
"/code?name=" +
encodeURIComponent(selected[0].name) +
"&share_path=" +
encodeURIComponent(previewPath)
)
);
return;
}
dispatch(
push(
"/code?p=" +
encodeURIComponent(previewPath) +
"&id=" +
selected[0].id
)
);
return;
default:
dispatch(openLoadingDialog("获取下载地址..."));
return;
}
};
};
export const selectFile = (file, event, fileIndex) => {
const { ctrlKey, metaKey, shiftKey } = event;
return (dispatch, getState) => {
// 多种组合操作忽略
if ([ctrlKey, metaKey, shiftKey].filter(Boolean).length > 1) {
return;
}
const isMacbook = isMac();
const { explorer } = getState();
const { selected, lastSelect, dirList, fileList } = explorer;
if (
shiftKey &&
!ctrlKey &&
!metaKey &&
selected.length !== 0 &&
// 点击类型一样
file.type === lastSelect.file.type
) {
// shift 多选
// 取消原有选择
dispatch(removeSelectedTargets(selected.map((v) => v.id)));
// 添加新选择
const begin = Math.min(lastSelect.index, fileIndex);
const end = Math.max(lastSelect.index, fileIndex);
const list = file.type === "dir" ? dirList : fileList;
const newShiftSelected = list.slice(begin, end + 1);
return dispatch(addSelectedTargets(newShiftSelected));
}
dispatch(setLastSelect(file, fileIndex));
dispatch(setShiftSelectedIds([]));
if ((ctrlKey && !isMacbook) || (metaKey && isMacbook)) {
// Ctrl/Command 单击添加/删除
const presentIndex = selected.findIndex((value) => {
return value.id === file.id;
});
if (presentIndex !== -1) {
return dispatch(removeSelectedTargets([file.id]));
}
return dispatch(addSelectedTargets([file]));
}
// 单选
return dispatch(setSelectedTarget([file]));
};
};

@ -0,0 +1,267 @@
export * from "./explorer";
export const setNavigator = (path, navigatorLoading) => {
return {
type: "SET_NAVIGATOR",
path,
navigatorLoading,
};
};
export const navigateTo = (path) => {
return (dispatch, getState) => {
const state = getState();
const navigatorLoading = path !== state.navigator.path;
dispatch(setNavigator(path, navigatorLoading));
};
};
export const navigateUp = () => {
return (dispatch, getState) => {
const state = getState();
const pathSplit = state.navigator.path.split("/");
pathSplit.pop();
const newPath = pathSplit.length === 1 ? "/" : pathSplit.join("/");
const navigatorLoading = newPath !== state.navigator.path;
dispatch(setNavigator(newPath, navigatorLoading));
};
};
export const drawerToggleAction = (open) => {
return {
type: "DRAWER_TOGGLE",
open: open,
};
};
export const dragAndDrop = (source, target) => {
return {
type: "DRAG_AND_DROP",
source: source,
target: target,
};
};
export const changeViewMethod = (method) => {
return {
type: "CHANGE_VIEW_METHOD",
method: method,
};
};
export const toggleDaylightMode = () => {
return {
type: "TOGGLE_DAYLIGHT_MODE",
};
};
export const changeContextMenu = (type, open) => {
return {
type: "CHANGE_CONTEXT_MENU",
menuType: type,
open: open,
};
};
export const setNavigatorLoadingStatus = (status) => {
return {
type: "SET_NAVIGATOR_LOADING_STATUE",
status: status,
};
};
export const setNavigatorError = (status, msg) => {
return {
type: "SET_NAVIGATOR_ERROR",
status: status,
msg: msg,
};
};
export const openCreateFolderDialog = () => {
return {
type: "OPEN_CREATE_FOLDER_DIALOG",
};
};
export const openCreateFileDialog = () => {
return {
type: "OPEN_CREATE_FILE_DIALOG",
};
};
export const setUserPopover = (anchor) => {
return {
type: "SET_USER_POPOVER",
anchor: anchor,
};
};
export const setShareUserPopover = (anchor) => {
return {
type: "SET_SHARE_USER_POPOVER",
anchor: anchor,
};
};
export const openRenameDialog = () => {
return {
type: "OPEN_RENAME_DIALOG",
};
};
export const openResaveDialog = (key) => {
return {
type: "OPEN_RESAVE_DIALOG",
key: key,
};
};
export const openMoveDialog = () => {
return {
type: "OPEN_MOVE_DIALOG",
};
};
export const openRemoveDialog = () => {
return {
type: "OPEN_REMOVE_DIALOG",
};
};
export const openShareDialog = () => {
return {
type: "OPEN_SHARE_DIALOG",
};
};
export const applyThemes = (theme) => {
return {
type: "APPLY_THEME",
theme: theme,
};
};
export const setSessionStatus = (status) => {
return {
type: "SET_SESSION_STATUS",
status: status,
};
};
export const openMusicDialog = () => {
return {
type: "OPEN_MUSIC_DIALOG",
};
};
export const openRemoteDownloadDialog = () => {
return {
type: "OPEN_REMOTE_DOWNLOAD_DIALOG",
};
};
export const openTorrentDownloadDialog = () => {
return {
type: "OPEN_TORRENT_DOWNLOAD_DIALOG",
};
};
export const openDecompressDialog = () => {
return {
type: "OPEN_DECOMPRESS_DIALOG",
};
};
export const openCompressDialog = () => {
return {
type: "OPEN_COMPRESS_DIALOG",
};
};
export const openGetSourceDialog = () => {
return {
type: "OPEN_GET_SOURCE_DIALOG",
};
};
export const openCopyDialog = () => {
return {
type: "OPEN_COPY_DIALOG",
};
};
export const openLoadingDialog = (text) => {
return {
type: "OPEN_LOADING_DIALOG",
text: text,
};
};
export const closeAllModals = () => {
return {
type: "CLOSE_ALL_MODALS",
};
};
export const toggleSnackbar = (vertical, horizontal, msg, color) => {
return {
type: "TOGGLE_SNACKBAR",
vertical: vertical,
horizontal: horizontal,
msg: msg,
color: color,
};
};
export const enableLoadUploader = () => {
return {
type: "ENABLE_LOAD_UPLOADER",
};
};
export const setModalsLoading = (status) => {
return {
type: "SET_MODALS_LOADING",
status: status,
};
};
export const refreshFileList = () => {
return {
type: "REFRESH_FILE_LIST",
};
};
export const searchMyFile = (keywords) => {
return {
type: "SEARCH_MY_FILE",
keywords: keywords,
};
};
export const showImgPreivew = (first) => {
return {
type: "SHOW_IMG_PREIVEW",
first: first,
};
};
export const refreshStorage = () => {
return {
type: "REFRESH_STORAGE",
};
};
export const saveFile = () => {
return {
type: "SAVE_FILE",
};
};
export const setSiteConfig = (config) => {
return {
type: "SET_SITE_CONFIG",
config: config,
};
};

@ -0,0 +1,77 @@
import React, { useEffect, useState } from "react";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import InputAdornment from "@material-ui/core/InputAdornment";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import FormHelperText from "@material-ui/core/FormHelperText";
export default function DomainInput({ onChange, value, required, label }) {
const [domain, setDomain] = useState("");
const [protocol, setProtocol] = useState("https://");
const [error, setError] = useState();
useState(() => {
value = value ? value : "";
if (value.startsWith("https://")) {
setDomain(value.replace("https://", ""));
setProtocol("https://");
} else {
if (value !== "") {
setDomain(value.replace("http://", ""));
setProtocol("http://");
}
}
}, [value]);
useEffect(() => {
if (protocol === "http://" && window.location.protocol === "https:") {
setError(
"您当前站点启用了 HTTPS ,此处选择 HTTP 可能会导致无法连接。"
);
} else {
setError("");
}
}, [protocol]);
return (
<FormControl>
<InputLabel htmlFor="component-helper">{label}</InputLabel>
<Input
error={error !== ""}
value={domain}
onChange={(e) => {
setDomain(e.target.value);
onChange({
target: {
value: protocol + e.target.value,
},
});
}}
required={required}
startAdornment={
<InputAdornment position="start">
<Select
value={protocol}
onChange={(e) => {
setProtocol(e.target.value);
onChange({
target: {
value: e.target.value + domain,
},
});
}}
>
<MenuItem value={"http://"}>http://</MenuItem>
<MenuItem value={"https://"}>https://</MenuItem>
</Select>
</InputAdornment>
}
/>
{error !== "" && (
<FormHelperText error={error !== ""}>{error}</FormHelperText>
)}
</FormControl>
);
}

@ -0,0 +1,128 @@
import FormControl from "@material-ui/core/FormControl";
import Input from "@material-ui/core/Input";
import InputAdornment from "@material-ui/core/InputAdornment";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import React, { useCallback, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
const unitTransform = (v) => {
if (v < 1024) {
return [Math.round(v), 1];
}
if (v < 1024 * 1024) {
return [Math.round(v / 1024), 1024];
}
if (v < 1024 * 1024 * 1024) {
return [Math.round(v / (1024 * 1024)), 1024 * 1024];
}
if (v < 1024 * 1024 * 1024 * 1024) {
return [Math.round(v / (1024 * 1024 * 1024)), 1024 * 1024 * 1024];
}
return [
Math.round(v / (1024 * 1024 * 1024 * 1024)),
1024 * 1024 * 1024 * 1024,
];
};
export default function SizeInput({
onChange,
min,
value,
required,
label,
max,
suffix,
}) {
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const [unit, setUnit] = useState(1);
let first = true;
const transform = useCallback(() => {
const res = unitTransform(value);
if (first && value !== 0) {
setUnit(res[1]);
first = false;
}
return res;
}, [value]);
return (
<FormControl>
<InputLabel htmlFor="component-helper">{label}</InputLabel>
<Input
style={{ width: 200 }}
value={transform()[0]}
type={"number"}
inputProps={{ min: min, step: 1 }}
onChange={(e) => {
if (e.target.value * unit < max) {
onChange({
target: {
value: (e.target.value * unit).toString(),
},
});
} else {
ToggleSnackbar(
"top",
"right",
"超出最大尺寸限制",
"warning"
);
}
}}
required={required}
endAdornment={
<InputAdornment position="end">
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={unit}
onChange={(e) => {
if (transform()[0] * e.target.value < max) {
onChange({
target: {
value: (
transform()[0] * e.target.value
).toString(),
},
});
setUnit(e.target.value);
} else {
ToggleSnackbar(
"top",
"right",
"超出最大尺寸限制",
"warning"
);
}
}}
>
<MenuItem value={1}>B{suffix && suffix}</MenuItem>
<MenuItem value={1024}>
KB{suffix && suffix}
</MenuItem>
<MenuItem value={1024 * 1024}>
MB{suffix && suffix}
</MenuItem>
<MenuItem value={1024 * 1024 * 1024}>
GB{suffix && suffix}
</MenuItem>
<MenuItem value={1024 * 1024 * 1024 * 1024}>
TB{suffix && suffix}
</MenuItem>
</Select>
</InputAdornment>
}
/>
</FormControl>
);
}

@ -0,0 +1,455 @@
import { withStyles } from "@material-ui/core";
import AppBar from "@material-ui/core/AppBar";
import Divider from "@material-ui/core/Divider";
import Drawer from "@material-ui/core/Drawer";
import MuiExpansionPanel from "@material-ui/core/ExpansionPanel";
import MuiExpansionPanelDetails from "@material-ui/core/ExpansionPanelDetails";
import MuiExpansionPanelSummary from "@material-ui/core/ExpansionPanelSummary";
import IconButton from "@material-ui/core/IconButton";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import { lighten, makeStyles, useTheme } from "@material-ui/core/styles";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import {
Assignment,
Category,
CloudDownload,
Contacts,
Group,
Home,
Image,
InsertDriveFile,
Language,
ListAlt,
Mail,
Palette,
Person,
Settings,
SettingsEthernet,
Share,
Storage,
} from "@material-ui/icons";
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
import MenuIcon from "@material-ui/icons/Menu";
import clsx from "clsx";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory, useLocation } from "react-router";
import { useRouteMatch } from "react-router-dom";
import { changeSubTitle } from "../../redux/viewUpdate/action";
import pathHelper from "../../utils/page";
import UserAvatar from "../Navbar/UserAvatar";
const ExpansionPanel = withStyles({
root: {
maxWidth: "100%",
boxShadow: "none",
"&:not(:last-child)": {
borderBottom: 0,
},
"&:before": {
display: "none",
},
"&$expanded": { margin: 0 },
},
expanded: {},
})(MuiExpansionPanel);
const ExpansionPanelSummary = withStyles({
root: {
minHeight: 0,
padding: 0,
"&$expanded": {
minHeight: 0,
},
},
content: {
maxWidth: "100%",
margin: 0,
display: "block",
"&$expanded": {
margin: "0",
},
},
expanded: {},
})(MuiExpansionPanelSummary);
const ExpansionPanelDetails = withStyles((theme) => ({
root: {
display: "block",
padding: theme.spacing(0),
},
}))(MuiExpansionPanelDetails);
const drawerWidth = 240;
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
width: "100%",
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
appBarShift: {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
menuButton: {
marginRight: 36,
},
hide: {
display: "none",
},
drawer: {
width: drawerWidth,
flexShrink: 0,
whiteSpace: "nowrap",
},
drawerOpen: {
width: drawerWidth,
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
drawerClose: {
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: "hidden",
width: theme.spacing(7) + 1,
[theme.breakpoints.up("sm")]: {
width: theme.spacing(9) + 1,
},
},
title: {
flexGrow: 1,
},
toolbar: {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: theme.spacing(0, 1),
...theme.mixins.toolbar,
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
sub: {
paddingLeft: 36,
color: theme.palette.text.secondary,
},
subMenu: {
backgroundColor: theme.palette.background.default,
paddingTop: 0,
paddingBottom: 0,
},
active: {
backgroundColor: lighten(theme.palette.primary.main, 0.8),
color: theme.palette.primary.main,
"&:hover": {
backgroundColor: lighten(theme.palette.primary.main, 0.7),
},
},
activeText: {
fontWeight: 500,
},
activeIcon: {
color: theme.palette.primary.main,
},
}));
const items = [
{
title: "面板首页",
icon: <Home />,
path: "home",
},
{
title: "参数设置",
icon: <Settings />,
sub: [
{
title: "站点信息",
path: "basic",
icon: <Language />,
},
{
title: "注册与登录",
path: "access",
icon: <Contacts />,
},
{
title: "邮件",
path: "mail",
icon: <Mail />,
},
{
title: "上传与下载",
path: "upload",
icon: <SettingsEthernet />,
},
{
title: "外观",
path: "theme",
icon: <Palette />,
},
{
title: "离线下载",
path: "aria2",
icon: <CloudDownload />,
},
{
title: "图像处理",
path: "image",
icon: <Image />,
},
{
title: "验证码",
path: "captcha",
icon: <Category />,
},
],
},
{
title: "存储策略",
icon: <Storage />,
path: "policy",
},
{
title: "用户组",
icon: <Group />,
path: "group",
},
{
title: "用户",
icon: <Person />,
path: "user",
},
{
title: "文件",
icon: <InsertDriveFile />,
path: "file",
},
{
title: "分享",
icon: <Share />,
path: "share",
},
{
title: "持久任务",
icon: <Assignment />,
sub: [
{
title: "离线下载",
path: "download",
icon: <CloudDownload />,
},
{
title: "常规任务",
path: "task",
icon: <ListAlt />,
},
],
},
];
export default function Dashboard({ content }) {
const classes = useStyles();
const theme = useTheme();
const [open, setOpen] = useState(!pathHelper.isMobile());
const [menuOpen, setMenuOpen] = useState(null);
const history = useHistory();
const location = useLocation();
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
const dispatch = useDispatch();
const SetSubTitle = useCallback(
(title) => dispatch(changeSubTitle(title)),
[dispatch]
);
useEffect(() => {
SetSubTitle("仪表盘");
}, []);
useEffect(() => {
return () => {
SetSubTitle();
};
}, []);
const { path } = useRouteMatch();
return (
<div className={classes.root}>
<AppBar
position="fixed"
className={clsx(classes.appBar, {
[classes.appBarShift]: open,
})}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
className={clsx(classes.menuButton, {
[classes.hide]: open,
})}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title} noWrap>
管理后台 仪表盘
</Typography>
<UserAvatar />
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
}),
}}
>
<div className={classes.toolbar}>
<IconButton onClick={handleDrawerClose}>
{theme.direction === "rtl" ? (
<ChevronRightIcon />
) : (
<ChevronLeftIcon />
)}
</IconButton>
</div>
<Divider />
<List className={classes.noPadding}>
{items.map((item) => {
if (item.path !== undefined) {
return (
<ListItem
onClick={() =>
history.push("/admin/" + item.path)
}
button
className={clsx({
[classes.active]: location.pathname.startsWith(
"/admin/" + item.path
),
})}
key={item.title}
>
<ListItemIcon
className={clsx({
[classes.activeIcon]: location.pathname.startsWith(
"/admin/" + item.path
),
})}
>
{item.icon}
</ListItemIcon>
<ListItemText
className={clsx({
[classes.activeText]: location.pathname.startsWith(
"/admin/" + item.path
),
})}
primary={item.title}
/>
</ListItem>
);
}
return (
<ExpansionPanel
key={item.title}
square
expanded={menuOpen === item.title}
onChange={(event, isExpanded) => {
setMenuOpen(isExpanded ? item.title : null);
}}
>
<ExpansionPanelSummary
aria-controls="panel1d-content"
id="panel1d-header"
>
<ListItem button key={item.title}>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.title} />
</ListItem>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<List className={classes.subMenu}>
{item.sub.map((sub) => (
<ListItem
onClick={() =>
history.push(
"/admin/" + sub.path
)
}
className={clsx({
[classes.sub]: open,
[classes.active]: location.pathname.startsWith(
"/admin/" + sub.path
),
})}
button
key={sub.title}
>
<ListItemIcon
className={clsx({
[classes.activeIcon]: location.pathname.startsWith(
"/admin/" + sub.path
),
})}
>
{sub.icon}
</ListItemIcon>
<ListItemText
primary={sub.title}
/>
</ListItem>
))}
</List>
</ExpansionPanelDetails>
</ExpansionPanel>
);
})}
</List>
</Drawer>
<main className={classes.content}>
<div className={classes.toolbar} />
{content(path)}
</main>
</div>
);
}

@ -0,0 +1,246 @@
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import FormControl from "@material-ui/core/FormControl";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import FormHelperText from "@material-ui/core/FormHelperText";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import { makeStyles } from "@material-ui/core/styles";
import Switch from "@material-ui/core/Switch";
import React, { useEffect, useState } from "react";
import API from "../../../middleware/Api";
const useStyles = makeStyles(() => ({
formContainer: {
margin: "8px 0 8px 0",
},
}));
export default function AddGroup({ open, onClose, onSubmit }) {
const classes = useStyles();
const [groups, setGroups] = useState([]);
const [group, setGroup] = useState({
name: "",
group_id: 2,
time: "",
price: "",
score: "",
des: "",
highlight: false,
});
useEffect(() => {
if (open && groups.length === 0) {
API.get("/admin/groups")
.then((response) => {
setGroups(response.data);
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
.catch(() => {});
}
// eslint-disable-next-line
}, [open]);
const handleChange = (name) => (event) => {
setGroup({
...group,
[name]: event.target.value,
});
};
const handleCheckChange = (name) => (event) => {
setGroup({
...group,
[name]: event.target.checked,
});
};
const submit = (e) => {
e.preventDefault();
const groupCopy = { ...group };
groupCopy.time = parseInt(groupCopy.time) * 86400;
groupCopy.price = parseInt(groupCopy.price) * 100;
groupCopy.score = parseInt(groupCopy.score);
groupCopy.id = new Date().valueOf();
groupCopy.des = groupCopy.des.split("\n");
onSubmit(groupCopy);
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth={"xs"}
scroll={"paper"}
>
<form onSubmit={submit}>
<DialogTitle id="alert-dialog-title">
添加可购用户组
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
名称
</InputLabel>
<Input
value={group.name}
onChange={handleChange("name")}
required
/>
<FormHelperText id="component-helper-text">
商品展示名称
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
用户组
</InputLabel>
<Select
value={group.group_id}
onChange={handleChange("group_id")}
required
>
{groups.map((v) => {
if (v.ID !== 3) {
return (
<MenuItem value={v.ID}>
{v.Name}
</MenuItem>
);
}
return null;
})}
</Select>
<FormHelperText id="component-helper-text">
购买后升级的用户组
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
有效期 ()
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={group.time}
onChange={handleChange("time")}
required
/>
<FormHelperText id="component-helper-text">
单位购买时间的有效期
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
单价 ()
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0.01,
step: 0.01,
}}
value={group.price}
onChange={handleChange("price")}
required
/>
<FormHelperText id="component-helper-text">
用户组的单价
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
单价 (积分)
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={group.score}
onChange={handleChange("score")}
required
/>
<FormHelperText id="component-helper-text">
使用积分购买时的价格填写为 0
表示不能使用积分购买
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
商品描述 (一行一个)
</InputLabel>
<Input
value={group.des}
onChange={handleChange("des")}
multiline
rowsMax={10}
required
/>
<FormHelperText id="component-helper-text">
购买页面展示的商品描述
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={group.highlight}
onChange={handleCheckChange(
"highlight"
)}
/>
}
label="突出展示"
/>
<FormHelperText id="component-helper-text">
开启后在商品选择页面会被突出展示
</FormHelperText>
</FormControl>
</div>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
取消
</Button>
<Button type={"submit"} color="primary">
确定
</Button>
</DialogActions>
</form>
</Dialog>
);
}

@ -0,0 +1,169 @@
import React, { useState } from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import InputLabel from "@material-ui/core/InputLabel";
import Input from "@material-ui/core/Input";
import FormHelperText from "@material-ui/core/FormHelperText";
import FormControl from "@material-ui/core/FormControl";
import SizeInput from "../Common/SizeInput";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(() => ({
formContainer: {
margin: "8px 0 8px 0",
},
}));
export default function AddPack({ open, onClose, onSubmit }) {
const classes = useStyles();
const [pack, setPack] = useState({
name: "",
size: "1073741824",
time: "",
price: "",
score: "",
});
const handleChange = (name) => (event) => {
setPack({
...pack,
[name]: event.target.value,
});
};
const submit = (e) => {
e.preventDefault();
const packCopy = { ...pack };
packCopy.size = parseInt(packCopy.size);
packCopy.time = parseInt(packCopy.time) * 86400;
packCopy.price = parseInt(packCopy.price) * 100;
packCopy.score = parseInt(packCopy.score);
packCopy.id = new Date().valueOf();
onSubmit(packCopy);
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth={"xs"}
>
<form onSubmit={submit}>
<DialogTitle id="alert-dialog-title">添加容量包</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
名称
</InputLabel>
<Input
value={pack.name}
onChange={handleChange("name")}
required
/>
<FormHelperText id="component-helper-text">
商品展示名称
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<SizeInput
value={pack.size}
onChange={handleChange("size")}
min={1}
label={"大小"}
max={9223372036854775807}
required
/>
<FormHelperText id="component-helper-text">
容量包的大小
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
有效期 ()
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={pack.time}
onChange={handleChange("time")}
required
/>
<FormHelperText id="component-helper-text">
每个容量包的有效期
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
单价 ()
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0.01,
step: 0.01,
}}
value={pack.price}
onChange={handleChange("price")}
required
/>
<FormHelperText id="component-helper-text">
容量包的单价
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
单价 (积分)
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={pack.score}
onChange={handleChange("score")}
required
/>
<FormHelperText id="component-helper-text">
使用积分购买时的价格填写为 0
表示不能使用积分购买
</FormHelperText>
</FormControl>
</div>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
取消
</Button>
<Button type={"submit"} color="primary">
确定
</Button>
</DialogActions>
</form>
</Dialog>
);
}

@ -0,0 +1,142 @@
import Button from "@material-ui/core/Button";
import Card from "@material-ui/core/Card";
import CardActionArea from "@material-ui/core/CardActionArea";
import CardContent from "@material-ui/core/CardContent";
import CardMedia from "@material-ui/core/CardMedia";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import Grid from "@material-ui/core/Grid";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import React from "react";
import { useHistory } from "react-router";
const useStyles = makeStyles((theme) => ({
cardContainer: {
display: "flex",
},
cover: {
width: 100,
height: 60,
},
card: {},
content: {
flex: "1 0 auto",
},
bg: {
backgroundColor: theme.palette.background.default,
padding: "24px 24px",
},
dialogFooter: {
justifyContent: "space-between",
},
}));
const policies = [
{
name: "本机存储",
img: "local.png",
path: "/admin/policy/add/local",
},
{
name: "从机存储",
img: "remote.png",
path: "/admin/policy/add/remote",
},
{
name: "七牛",
img: "qiniu.png",
path: "/admin/policy/add/qiniu",
},
{
name: "阿里云 OSS",
img: "oss.png",
path: "/admin/policy/add/oss",
},
{
name: "又拍云",
img: "upyun.png",
path: "/admin/policy/add/upyun",
},
{
name: "腾讯云 COS",
img: "cos.png",
path: "/admin/policy/add/cos",
},
{
name: "OneDrive",
img: "onedrive.png",
path: "/admin/policy/add/onedrive",
},
{
name: "Amazon S3",
img: "s3.png",
path: "/admin/policy/add/s3",
},
];
export default function AddPolicy({ open, onClose }) {
const classes = useStyles();
const location = useHistory();
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth={"sm"}
fullWidth
>
<DialogTitle id="alert-dialog-title">选择存储方式</DialogTitle>
<DialogContent dividers className={classes.bg}>
<Grid container spacing={2}>
{policies.map((v) => (
<Grid item sm={12} md={6} key={v.path}>
<Card className={classes.card}>
<CardActionArea
onClick={() => {
location.push(v.path);
onClose();
}}
className={classes.cardContainer}
>
<CardMedia
className={classes.cover}
image={"/static/img/" + v.img}
/>
<CardContent className={classes.content}>
<Typography
variant="subtitle1"
color="textSecondary"
>
{v.name}
</Typography>
</CardContent>
</CardActionArea>
</Card>
</Grid>
))}
</Grid>
</DialogContent>
<DialogActions className={classes.dialogFooter}>
<Button
onClick={() =>
window.open(
"https://docs.cloudreve.org/use/policy/compare"
)
}
color="primary"
>
存储策略对比
</Button>
<Button onClick={onClose} color="primary">
取消
</Button>
</DialogActions>
</Dialog>
);
}

@ -0,0 +1,175 @@
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import FormControl from "@material-ui/core/FormControl";
import FormHelperText from "@material-ui/core/FormHelperText";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import { makeStyles } from "@material-ui/core/styles";
import React, { useCallback, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
const useStyles = makeStyles(() => ({
formContainer: {
margin: "8px 0 8px 0",
},
}));
export default function AddRedeem({ open, onClose, products, onSuccess }) {
const classes = useStyles();
const [input, setInput] = useState({
num: 1,
id: 0,
time: 1,
});
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const handleChange = (name) => (event) => {
setInput({
...input,
[name]: event.target.value,
});
};
const submit = (e) => {
e.preventDefault();
setLoading(true);
input.num = parseInt(input.num);
input.id = parseInt(input.id);
input.time = parseInt(input.time);
input.type = 2;
for (let i = 0; i < products.length; i++) {
if (products[i].id === input.id) {
if (products[i].group_id !== undefined) {
input.type = 1;
} else {
input.type = 0;
}
break;
}
}
API.post("/admin/redeem", input)
.then((response) => {
onSuccess(response.data);
onClose();
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
maxWidth={"xs"}
>
<form onSubmit={submit}>
<DialogTitle id="alert-dialog-title">生成兑换码</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
生成数量
</InputLabel>
<Input
type={"number"}
inputProps={{
step: 1,
min: 1,
max: 100,
}}
value={input.num}
onChange={handleChange("num")}
required
/>
<FormHelperText id="component-helper-text">
激活码批量生成数量
</FormHelperText>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
对应商品
</InputLabel>
<Select
value={input.id}
onChange={(e) => {
handleChange("id")(e);
}}
>
{products.map((v) => (
<MenuItem
key={v.id}
value={v.id}
data-type={"1"}
>
{v.name}
</MenuItem>
))}
<MenuItem value={0}>积分</MenuItem>
</Select>
</FormControl>
</div>
<div className={classes.formContainer}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
商品数量
</InputLabel>
<Input
type={"number"}
inputProps={{
step: 1,
min: 1,
}}
value={input.time}
onChange={handleChange("time")}
required
/>
<FormHelperText id="component-helper-text">
对于积分类商品此处为积分数量其他商品为时长倍数
</FormHelperText>
</FormControl>
</div>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
disabled={loading}
onClick={onClose}
color="default"
>
取消
</Button>
<Button disabled={loading} type={"submit"} color="primary">
确定
</Button>
</DialogActions>
</form>
</Dialog>
);
}

@ -0,0 +1,31 @@
import React from "react";
import DialogTitle from "@material-ui/core/DialogTitle";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import Typography from "@material-ui/core/Typography";
import DialogActions from "@material-ui/core/DialogActions";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
export default function AlertDialog({ title, msg, open, onClose }) {
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{title}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<Typography>{msg}</Typography>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
知道了
</Button>
</DialogActions>
</Dialog>
);
}

@ -0,0 +1,348 @@
import AppBar from "@material-ui/core/AppBar";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import Fab from "@material-ui/core/Fab";
import Grid from "@material-ui/core/Grid";
import IconButton from "@material-ui/core/IconButton";
import { createMuiTheme, makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import Toolbar from "@material-ui/core/Toolbar";
import Typography from "@material-ui/core/Typography";
import { Add, Menu } from "@material-ui/icons";
import { ThemeProvider } from "@material-ui/styles";
import React, { useCallback, useState } from "react";
import { CompactPicker } from "react-color";
const useStyles = makeStyles((theme) => ({
picker: {
"& div": {
boxShadow: "none !important",
},
marginTop: theme.spacing(1),
},
"@global": {
".compact-picker:parent ": {
boxShadow: "none !important",
},
},
statusBar: {
height: 24,
width: "100%",
},
fab: {
textAlign: "right",
},
}));
export default function CreateTheme({ open, onClose, onSubmit }) {
const classes = useStyles();
const [theme, setTheme] = useState({
palette: {
primary: {
main: "#3f51b5",
contrastText: "#fff",
},
secondary: {
main: "#d81b60",
contrastText: "#fff",
},
},
});
const subTheme = useCallback(() => {
return createMuiTheme(theme);
}, [theme]);
return (
<Dialog open={open} onClose={onClose} fullWidth maxWidth={"md"}>
<DialogContent>
<Grid container>
<Grid spacing={2} md={8} xs={12} container>
<Grid md={6} xs={12} item>
<Typography variant="h6" gutterBottom>
主色调
</Typography>
<TextField
value={theme.palette.primary.main}
onChange={(e) => {
setTheme({
...theme,
palette: {
...theme.palette,
primary: {
...theme.palette.primary,
main: e.target.value,
},
},
});
}}
fullWidth
/>
<div className={classes.picker}>
<CompactPicker
colors={[
"#4D4D4D",
"#999999",
"#FFFFFF",
"#f44336",
"#ff9800",
"#ffeb3b",
"#cddc39",
"#A4DD00",
"#00bcd4",
"#03a9f4",
"#AEA1FF",
"#FDA1FF",
"#333333",
"#808080",
"#cccccc",
"#ff5722",
"#ffc107",
"#FCC400",
"#8bc34a",
"#4caf50",
"#009688",
"#2196f3",
"#3f51b5",
"#e91e63",
"#000000",
"#666666",
"#B3B3B3",
"#9F0500",
"#C45100",
"#FB9E00",
"#808900",
"#194D33",
"#0C797D",
"#0062B1",
"#673ab7",
"#9c27b0",
]}
color={theme.palette.primary.main}
onChangeComplete={(c) => {
setTheme({
...theme,
palette: {
...theme.palette,
primary: {
...theme.palette.primary,
main: c.hex,
},
},
});
}}
/>
</div>
</Grid>
<Grid md={6} xs={12} item>
<Typography variant="h6" gutterBottom>
辅色调
</Typography>
<TextField
value={theme.palette.secondary.main}
onChange={(e) => {
setTheme({
...theme,
palette: {
...theme.palette,
secondary: {
...theme.palette.secondary,
main: e.target.value,
},
},
});
}}
fullWidth
/>
<div className={classes.picker}>
<CompactPicker
colors={[
"#4D4D4D",
"#999999",
"#FFFFFF",
"#ff1744",
"#ff3d00",
"#ffeb3b",
"#cddc39",
"#A4DD00",
"#00bcd4",
"#00e5ff",
"#AEA1FF",
"#FDA1FF",
"#333333",
"#808080",
"#cccccc",
"#ff5722",
"#ffea00",
"#ffc400",
"#c6ff00",
"#00e676",
"#76ff03",
"#00b0ff",
"#2979ff",
"#f50057",
"#000000",
"#666666",
"#B3B3B3",
"#9F0500",
"#C45100",
"#FB9E00",
"#808900",
"#1de9b6",
"#0C797D",
"#3d5afe",
"#651fff",
"#d500f9",
]}
color={theme.palette.secondary.main}
onChangeComplete={(c) => {
setTheme({
...theme,
palette: {
...theme.palette,
secondary: {
...theme.palette.secondary,
main: c.hex,
},
},
});
}}
/>
</div>
</Grid>
<Grid md={6} xs={12} item>
<Typography variant="h6" gutterBottom>
主色调文字
</Typography>
<TextField
value={theme.palette.primary.contrastText}
onChange={(e) => {
setTheme({
...theme,
palette: {
...theme.palette,
primary: {
...theme.palette.primary,
contrastText: e.target.value,
},
},
});
}}
fullWidth
/>
<div className={classes.picker}>
<CompactPicker
color={theme.palette.primary.contrastText}
onChangeComplete={(c) => {
setTheme({
...theme,
palette: {
...theme.palette,
primary: {
...theme.palette.primary,
contrastText: c.hex,
},
},
});
}}
/>
</div>
</Grid>
<Grid md={6} xs={12} item>
<Typography variant="h6" gutterBottom>
辅色调文字
</Typography>
<TextField
value={theme.palette.secondary.contrastText}
onChange={(e) => {
setTheme({
...theme,
palette: {
...theme.palette,
secondary: {
...theme.palette.secondary,
contrastText: e.target.value,
},
},
});
}}
fullWidth
/>
<div className={classes.picker}>
<CompactPicker
color={theme.palette.secondary.contrastText}
onChangeComplete={(c) => {
setTheme({
...theme,
palette: {
...theme.palette,
secondary: {
...theme.palette.secondary,
contrastText: c.hex,
},
},
});
}}
/>
</div>
</Grid>
</Grid>
<Grid spacing={2} md={4} xs={12}>
<ThemeProvider theme={subTheme()}>
<div
className={classes.statusBar}
style={{
backgroundColor: subTheme().palette.primary
.dark,
}}
/>
<AppBar position="static">
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="menu"
>
<Menu />
</IconButton>
<Typography
variant="h6"
className={classes.title}
>
Color
</Typography>
</Toolbar>
</AppBar>
<div style={{ padding: 16 }}>
<TextField
fullWidth
color={"secondary"}
label={"文字输入"}
/>
<div
className={classes.fab}
style={{ paddingTop: 64 }}
>
<Fab color="secondary" aria-label="add">
<Add />
</Fab>
</div>
</div>
</ThemeProvider>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
取消
</Button>
<Button onClick={() => onSubmit(theme)} color="primary">
创建
</Button>
</DialogActions>
</Dialog>
);
}

@ -0,0 +1,129 @@
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import TextField from "@material-ui/core/TextField";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
export default function FileFilter({ setFilter, setSearch, open, onClose }) {
const [input, setInput] = useState({
policy_id: "all",
user_id: "",
});
const [policies, setPolicies] = useState([]);
const [keywords, setKeywords] = useState("");
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const handleChange = (name) => (event) => {
setInput({ ...input, [name]: event.target.value });
};
useEffect(() => {
API.post("/admin/policy/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
conditions: {},
})
.then((response) => {
setPolicies(response.data.items);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
const submit = () => {
const res = {};
Object.keys(input).forEach((v) => {
if (input[v] !== "all" && input[v] !== "") {
res[v] = input[v];
}
});
setFilter(res);
if (keywords !== "") {
setSearch({
name: keywords,
});
} else {
setSearch({});
}
onClose();
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
fullWidth
maxWidth={"xs"}
>
<DialogTitle id="alert-dialog-title">过滤条件</DialogTitle>
<DialogContent>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">
存储策略
</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={input.policy_id}
onChange={handleChange("policy_id")}
>
<MenuItem value={"all"}>全部</MenuItem>
{policies.map((v) => {
if (v.ID === 3) {
return null;
}
return (
<MenuItem key={v.ID} value={v.ID.toString()}>
{v.Name}
</MenuItem>
);
})}
</Select>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={input.user_id}
onChange={handleChange("user_id")}
id="standard-basic"
label="上传者ID"
/>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
id="standard-basic"
label="搜索 文件名"
/>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
取消
</Button>
<Button onClick={submit} color="primary">
应用
</Button>
</DialogActions>
</Dialog>
);
}

@ -0,0 +1,158 @@
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import React from "react";
export default function MagicVar({ isFile, open, onClose, isSlave }) {
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{isFile ? "文件名魔法变量" : "路径魔法变量"}
</DialogTitle>
<DialogContent>
<TableContainer>
<Table size="small" aria-label="a dense table">
<TableHead>
<TableRow>
<TableCell>魔法变量</TableCell>
<TableCell>描述</TableCell>
<TableCell>示例</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell component="th" scope="row">
{"{randomkey16}"}
</TableCell>
<TableCell>16位随机字符</TableCell>
<TableCell>N6IimT5XZP324ACK</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{"{randomkey8}"}
</TableCell>
<TableCell>8位随机字符</TableCell>
<TableCell>gWz78q30</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{"{timestamp}"}
</TableCell>
<TableCell>秒级时间戳</TableCell>
<TableCell>1582692933</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{"{timestamp_nano}"}
</TableCell>
<TableCell>纳秒级时间戳</TableCell>
<TableCell>1582692933231834600</TableCell>
</TableRow>
{!isSlave && (
<TableRow>
<TableCell component="th" scope="row">
{"{uid}"}
</TableCell>
<TableCell>用户ID</TableCell>
<TableCell>1</TableCell>
</TableRow>
)}
{isFile && (
<TableRow>
<TableCell component="th" scope="row">
{"{originname}"}
</TableCell>
<TableCell>原始文件名</TableCell>
<TableCell>MyPico.mp4</TableCell>
</TableRow>
)}
{!isFile && !isSlave && (
<TableRow>
<TableCell component="th" scope="row">
{"{path}"}
</TableCell>
<TableCell>用户上传路径</TableCell>
<TableCell>///</TableCell>
</TableRow>
)}
<TableRow>
<TableCell component="th" scope="row">
{"{date}"}
</TableCell>
<TableCell>日期</TableCell>
<TableCell>20060102</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{"{datetime}"}
</TableCell>
<TableCell>日期时间</TableCell>
<TableCell>20060102150405</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{"{year}"}
</TableCell>
<TableCell>年份</TableCell>
<TableCell>2006</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{"{month}"}
</TableCell>
<TableCell>月份</TableCell>
<TableCell>01</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{"{day}"}
</TableCell>
<TableCell></TableCell>
<TableCell>02</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{"{hour}"}
</TableCell>
<TableCell>小时</TableCell>
<TableCell>15</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{"{minute}"}
</TableCell>
<TableCell>分钟</TableCell>
<TableCell>04</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
{"{second}"}
</TableCell>
<TableCell></TableCell>
<TableCell>05</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
关闭
</Button>
</DialogActions>
</Dialog>
);
}

@ -0,0 +1,95 @@
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import TextField from "@material-ui/core/TextField";
import React, { useState } from "react";
export default function ShareFilter({ setFilter, setSearch, open, onClose }) {
const [input, setInput] = useState({
is_dir: "all",
user_id: "",
});
const [keywords, setKeywords] = useState("");
const handleChange = (name) => (event) => {
setInput({ ...input, [name]: event.target.value });
};
const submit = () => {
const res = {};
Object.keys(input).forEach((v) => {
if (input[v] !== "all" && input[v] !== "") {
res[v] = input[v];
}
});
setFilter(res);
if (keywords !== "") {
setSearch({
source_name: keywords,
});
} else {
setSearch({});
}
onClose();
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
fullWidth
maxWidth={"xs"}
>
<DialogTitle id="alert-dialog-title">过滤条件</DialogTitle>
<DialogContent>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">
源文件类型
</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={input.is_dir}
onChange={handleChange("is_dir")}
>
<MenuItem value={"all"}>全部</MenuItem>
<MenuItem value={"1"}>目录</MenuItem>
<MenuItem value={"0"}>文件</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={input.user_id}
onChange={handleChange("user_id")}
id="standard-basic"
label="上传者ID"
/>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
id="standard-basic"
label="搜索 文件名"
/>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
取消
</Button>
<Button onClick={submit} color="primary">
应用
</Button>
</DialogActions>
</Dialog>
);
}

@ -0,0 +1,134 @@
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogTitle from "@material-ui/core/DialogTitle";
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import TextField from "@material-ui/core/TextField";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
export default function UserFilter({ setFilter, setSearch, open, onClose }) {
const [input, setInput] = useState({
group_id: "all",
status: "all",
});
const [groups, setGroups] = useState([]);
const [keywords, setKeywords] = useState("");
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const handleChange = (name) => (event) => {
setInput({ ...input, [name]: event.target.value });
};
useEffect(() => {
API.get("/admin/groups")
.then((response) => {
setGroups(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
const submit = () => {
const res = {};
Object.keys(input).forEach((v) => {
if (input[v] !== "all") {
res[v] = input[v];
}
});
setFilter(res);
if (keywords !== "") {
setSearch({
nick: keywords,
email: keywords,
});
} else {
setSearch({});
}
onClose();
};
return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
fullWidth
maxWidth={"xs"}
>
<DialogTitle id="alert-dialog-title">过滤条件</DialogTitle>
<DialogContent>
<FormControl fullWidth>
<InputLabel id="demo-simple-select-label">
用户组
</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={input.group_id}
onChange={handleChange("group_id")}
>
<MenuItem value={"all"}>全部</MenuItem>
{groups.map((v) => {
if (v.ID === 3) {
return null;
}
return (
<MenuItem key={v.ID} value={v.ID.toString()}>
{v.Name}
</MenuItem>
);
})}
</Select>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<InputLabel id="demo-simple-select-label">
用户状态
</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={input.status}
onChange={handleChange("status")}
>
<MenuItem value={"all"}>全部</MenuItem>
<MenuItem value={"0"}>正常</MenuItem>
<MenuItem value={"1"}>未激活</MenuItem>
<MenuItem value={"2"}>被封禁</MenuItem>
<MenuItem value={"3"}>超额使用被封禁</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth style={{ marginTop: 16 }}>
<TextField
value={keywords}
onChange={(e) => setKeywords(e.target.value)}
id="standard-basic"
label="搜索 昵称 / 用户名"
/>
</FormControl>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="default">
取消
</Button>
<Button onClick={submit} color="primary">
应用
</Button>
</DialogActions>
</Dialog>
);
}

@ -0,0 +1,466 @@
import { lighten } from "@material-ui/core";
import Badge from "@material-ui/core/Badge";
import Button from "@material-ui/core/Button";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import Link from "@material-ui/core/Link";
import Paper from "@material-ui/core/Paper";
import { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import Toolbar from "@material-ui/core/Toolbar";
import Tooltip from "@material-ui/core/Tooltip";
import Typography from "@material-ui/core/Typography";
import { Delete, DeleteForever, FilterList } from "@material-ui/icons";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
import { sizeToString } from "../../../utils";
import FileFilter from "../Dialogs/FileFilter";
import { formatLocalTime } from "../../../utils/datetime";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function File() {
const classes = useStyles();
const [files, setFiles] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [users, setUsers] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const history = useHistory();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/file/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setFiles(response.data.items);
setTotal(response.data.total);
setSelected([]);
setUsers(response.data.users);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/file/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar(
"top",
"right",
"删除任务将在后台执行",
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = (force) => () => {
setLoading(true);
API.post("/admin/file/delete", { id: selected, force: force })
.then(() => {
loadList();
ToggleSnackbar(
"top",
"right",
"删除任务将在后台执行",
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = files.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<FileFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<Button
color={"primary"}
onClick={() => history.push("/admin/file/import")}
variant={"contained"}
style={{
alignSelf: "center",
}}
>
从外部导入
</Button>
<div className={classes.headerRight}>
<Tooltip title="过滤">
<IconButton
style={{ marginRight: 8 }}
onClick={() => setFilterDialog(true)}
>
<Badge
color="secondary"
variant="dot"
invisible={
Object.keys(search).length === 0 &&
Object.keys(filter).length === 0
}
>
<FilterList />
</Badge>
</IconButton>
</Tooltip>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
刷新
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
已选择 {selected.length} 个对象
</Typography>
<Tooltip title="删除">
<IconButton
onClick={deleteBatch(false)}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
<Tooltip title="强制删除">
<IconButton
onClick={deleteBatch(true)}
disabled={loading}
aria-label="delete"
>
<DeleteForever />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < files.length
}
checked={
files.length > 0 &&
selected.length === files.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 59 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 250 }}>
<TableSortLabel
active={orderBy[0] === "name"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"name",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
文件名
{orderBy[0] === "name" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell
align={"right"}
style={{ minWidth: 70 }}
>
<TableSortLabel
active={orderBy[0] === "size"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"size",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
大小
{orderBy[0] === "size" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 120 }}>
上传者
</TableCell>
<TableCell style={{ minWidth: 150 }}>
上传于
</TableCell>
<TableCell style={{ minWidth: 100 }}>
操作
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{files.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell>
<Link
target={"_blank"}
color="inherit"
href={
"/api/v3/admin/file/preview/" +
row.ID
}
>
{row.Name}
</Link>
</TableCell>
<TableCell align={"right"}>
{sizeToString(row.Size)}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" + row.UserID
}
>
{users[row.UserID]
? users[row.UserID].Nick
: "未知"}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(
row.CreatedAt,
"YYYY-MM-DD H:mm:ss"
)}
</TableCell>
<TableCell>
<Tooltip title={"删除"}>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

@ -0,0 +1,471 @@
import { Dialog } from "@material-ui/core";
import Button from "@material-ui/core/Button";
import Chip from "@material-ui/core/Chip";
import DialogActions from "@material-ui/core/DialogActions";
import DialogTitle from "@material-ui/core/DialogTitle";
import Fade from "@material-ui/core/Fade";
import FormControl from "@material-ui/core/FormControl";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import FormHelperText from "@material-ui/core/FormHelperText";
import Input from "@material-ui/core/Input";
import InputAdornment from "@material-ui/core/InputAdornment";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import Paper from "@material-ui/core/Paper";
import Popper from "@material-ui/core/Popper";
import Select from "@material-ui/core/Select";
import { makeStyles } from "@material-ui/core/styles";
import Switch from "@material-ui/core/Switch";
import Typography from "@material-ui/core/Typography";
import Alert from "@material-ui/lab/Alert";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
import PathSelector from "../../FileManager/PathSelector";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
userSelect: {
width: 400,
borderRadius: 0,
},
}));
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value]);
return debouncedValue;
}
export default function Import() {
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
policy: 1,
userInput: "",
src: "",
dst: "",
recursive: true,
});
const [anchorEl, setAnchorEl] = useState(null);
const [policies, setPolicies] = useState({});
const [users, setUsers] = useState([]);
const [user, setUser] = useState(null);
const [selectRemote, setSelectRemote] = useState(false);
const [selectLocal, setSelectLocal] = useState(false);
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const handleCheckChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.checked,
});
};
const history = useHistory();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const submit = (e) => {
e.preventDefault();
if (user === null) {
ToggleSnackbar("top", "right", "请先选择目标用户", "warning");
return;
}
setLoading(true);
API.post("/admin/task/import", {
uid: user.ID,
policy_id: parseInt(options.policy),
src: options.src,
dst: options.dst,
recursive: options.recursive,
})
.then(() => {
setLoading(false);
history.push("/admin/file");
ToggleSnackbar(
"top",
"right",
"导入任务已创建,您可以在“持久任务”中查看执行情况",
"success"
);
})
.catch((error) => {
setLoading(false);
ToggleSnackbar("top", "right", error.message, "error");
});
};
const debouncedSearchTerm = useDebounce(options.userInput, 500);
useEffect(() => {
if (debouncedSearchTerm !== "") {
API.post("/admin/user/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
searches: {
nick: debouncedSearchTerm,
email: debouncedSearchTerm,
},
})
.then((response) => {
setUsers(response.data.items);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}
}, [debouncedSearchTerm]);
useEffect(() => {
API.post("/admin/policy/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
conditions: {},
})
.then((response) => {
const res = {};
response.data.items.forEach((v) => {
res[v.ID] = v;
});
setPolicies(res);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const selectUser = (u) => {
setOptions({
...options,
userInput: "",
});
setUser(u);
};
const setMoveTarget = (setter) => (folder) => {
const path =
folder.path === "/"
? folder.path + folder.name
: folder.path + "/" + folder.name;
setter(path === "//" ? "/" : path);
};
const openPathSelector = (isSrcSelect) => {
if (isSrcSelect) {
if (
!policies[options.policy] ||
policies[options.policy].Type === "local" ||
policies[options.policy].Type === "remote"
) {
ToggleSnackbar(
"top",
"right",
"选择的存储策略只支持手动输入路径",
"warning"
);
return;
}
setSelectRemote(true);
} else {
if (user === null) {
ToggleSnackbar("top", "right", "请先选择目标用户", "warning");
return;
}
setSelectLocal(true);
}
};
return (
<div>
<Dialog
open={selectRemote}
onClose={() => setSelectRemote(false)}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">选择目录</DialogTitle>
<PathSelector
presentPath="/"
api={"/admin/file/folders/policy/" + options.policy}
selected={[]}
onSelect={setMoveTarget((p) =>
setOptions({
...options,
src: p,
})
)}
/>
<DialogActions>
<Button
onClick={() => setSelectRemote(false)}
color="primary"
>
确定
</Button>
</DialogActions>
</Dialog>
<Dialog
open={selectLocal}
onClose={() => setSelectLocal(false)}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">选择目录</DialogTitle>
<PathSelector
presentPath="/"
api={
"/admin/file/folders/user/" +
(user === null ? 0 : user.ID)
}
selected={[]}
onSelect={setMoveTarget((p) =>
setOptions({
...options,
dst: p,
})
)}
/>
<DialogActions>
<Button
onClick={() => setSelectLocal(false)}
color="primary"
>
确定
</Button>
</DialogActions>
</Dialog>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
导入外部目录
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Alert severity="info">
您可以将存储策略中已有文件目录结构导入到
Cloudreve
导入操作不会额外占用物理存储空间但仍会正常扣除用户已用容量空间空间不足时将停止导入
</Alert>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
存储策略
</InputLabel>
<Select
labelId="demo-mutiple-chip-label"
id="demo-mutiple-chip"
value={options.policy}
onChange={handleChange("policy")}
input={<Input id="select-multiple-chip" />}
>
{Object.keys(policies).map((pid) => (
<MenuItem key={pid} value={pid}>
{policies[pid].Name}
</MenuItem>
))}
</Select>
<FormHelperText id="component-helper-text">
选择要导入文件目前存储所在的存储策略
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
目标用户
</InputLabel>
<Input
value={options.userInput}
onChange={(e) => {
handleChange("userInput")(e);
setAnchorEl(e.currentTarget);
}}
startAdornment={
user !== null && (
<InputAdornment position="start">
<Chip
size="small"
onDelete={() => {
setUser(null);
}}
label={user.Nick}
/>
</InputAdornment>
)
}
disabled={user !== null}
/>
<Popper
open={
options.userInput !== "" &&
users.length > 0
}
anchorEl={anchorEl}
placement={"bottom"}
transition
>
{({ TransitionProps }) => (
<Fade
{...TransitionProps}
timeout={350}
>
<Paper
className={classes.userSelect}
>
{users.map((u) => (
<MenuItem
key={u.Email}
onClick={() =>
selectUser(u)
}
>
{u.Nick}{" "}
{"<" + u.Email + ">"}
</MenuItem>
))}
</Paper>
</Fade>
)}
</Popper>
<FormHelperText id="component-helper-text">
选择要将文件导入到哪个用户的文件系统中可通过昵称邮箱搜索用户
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
原始目录路径
</InputLabel>
<Input
value={options.src}
onChange={(e) => {
handleChange("src")(e);
setAnchorEl(e.currentTarget);
}}
required
endAdornment={
<Button
onClick={() =>
openPathSelector(true)
}
>
选择
</Button>
}
/>
<FormHelperText id="component-helper-text">
要导入的目录在存储端的路径
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
目的目录路径
</InputLabel>
<Input
value={options.dst}
onChange={(e) => {
handleChange("dst")(e);
setAnchorEl(e.currentTarget);
}}
required
endAdornment={
<Button
onClick={() =>
openPathSelector(false)
}
>
选择
</Button>
}
/>
<FormHelperText id="component-helper-text">
要将目录导入到用户文件系统中的路径
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={options.recursive}
onChange={handleCheckChange(
"recursive"
)}
/>
}
label="递归导入子目录"
/>
<FormHelperText id="component-helper-text">
是否将目录下的所有子目录递归导入
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
创建导入任务
</Button>
</div>
</form>
</div>
);
}

@ -0,0 +1,84 @@
import React, { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import GroupForm from "./GroupForm";
export default function EditGroupPreload() {
const [group, setGroup] = useState({});
const { id } = useParams();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
setGroup({});
API.get("/admin/group/" + id)
.then((response) => {
// 布尔值转换
["ShareEnabled", "WebDAVEnabled"].forEach((v) => {
response.data[v] = response.data[v] ? "true" : "false";
});
[
"archive_download",
"archive_task",
"one_time_download",
"share_download",
"aria2",
].forEach((v) => {
if (response.data.OptionsSerialized[v] !== undefined) {
response.data.OptionsSerialized[v] = response.data
.OptionsSerialized[v]
? "true"
: "false";
}
});
// 整型转换
["MaxStorage", "SpeedLimit"].forEach((v) => {
response.data[v] = response.data[v].toString();
});
["compress_size", "decompress_size"].forEach((v) => {
if (response.data.OptionsSerialized[v] !== undefined) {
response.data.OptionsSerialized[
v
] = response.data.OptionsSerialized[v].toString();
}
});
response.data.PolicyList = response.data.PolicyList[0];
// JSON转换
if (
response.data.OptionsSerialized.aria2_options === undefined
) {
response.data.OptionsSerialized.aria2_options = "{}";
} else {
try {
response.data.OptionsSerialized.aria2_options = JSON.stringify(
response.data.OptionsSerialized.aria2_options
);
} catch (e) {
ToggleSnackbar(
"top",
"right",
"Aria2 设置项格式错误",
"warning"
);
return;
}
}
setGroup(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, [id]);
return <div>{group.ID !== undefined && <GroupForm group={group} />}</div>;
}

@ -0,0 +1,248 @@
import Button from "@material-ui/core/Button";
import IconButton from "@material-ui/core/IconButton";
import Paper from "@material-ui/core/Paper";
import { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import Tooltip from "@material-ui/core/Tooltip";
import { Delete, Edit } from "@material-ui/icons";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory, useLocation } from "react-router";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
import { sizeToString } from "../../../utils";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
}));
const columns = [
{ id: "#", label: "#", minWidth: 50 },
{ id: "name", label: "名称", minWidth: 170 },
{ id: "type", label: "存储策略", minWidth: 170 },
{
id: "count",
label: "下属用户数",
minWidth: 50,
align: "right",
},
{
id: "size",
label: "最大容量",
minWidth: 100,
align: "right",
},
{
id: "action",
label: "操作",
minWidth: 170,
align: "right",
},
];
function useQuery() {
return new URLSearchParams(useLocation().search);
}
export default function Group() {
const classes = useStyles();
const [groups, setGroups] = useState([]);
const [statics, setStatics] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [policies, setPolicies] = React.useState({});
const location = useLocation();
const history = useHistory();
const query = useQuery();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/group/list", {
page: page,
page_size: pageSize,
order_by: "id desc",
})
.then((response) => {
setGroups(response.data.items);
setStatics(response.data.statics);
setTotal(response.data.total);
setPolicies(response.data.policies);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
if (query.get("code") === "0") {
ToggleSnackbar("top", "right", "授权成功", "success");
} else if (query.get("msg") && query.get("msg") !== "") {
ToggleSnackbar(
"top",
"right",
query.get("msg") + ", " + query.get("err"),
"warning"
);
}
}, [location]);
useEffect(() => {
loadList();
}, [page, pageSize]);
const deletePolicy = (id) => {
API.delete("/admin/group/" + id)
.then(() => {
loadList();
ToggleSnackbar("top", "right", "用户组已删除", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
return (
<div>
<div className={classes.header}>
<Button
color={"primary"}
onClick={() => history.push("/admin/group/add")}
variant={"contained"}
>
新建用户组
</Button>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
刷新
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
{columns.map((column) => (
<TableCell
key={column.id}
align={column.align}
style={{ minWidth: column.minWidth }}
>
{column.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{groups.map((row) => (
<TableRow hover key={row.ID}>
<TableCell>{row.ID}</TableCell>
<TableCell>{row.Name}</TableCell>
<TableCell>
{row.PolicyList !== null &&
row.PolicyList.map((pid, key) => {
let res = "";
if (policies[pid]) {
res += policies[pid].Name;
}
if (
key !==
row.PolicyList.length - 1
) {
res += " / ";
}
return res;
})}
</TableCell>
<TableCell align={"right"}>
{statics[row.ID] !== undefined &&
statics[row.ID].toLocaleString()}
</TableCell>
<TableCell align={"right"}>
{statics[row.ID] !== undefined &&
sizeToString(row.MaxStorage)}
</TableCell>
<TableCell align={"right"}>
<Tooltip title={"编辑"}>
<IconButton
onClick={() =>
history.push(
"/admin/group/edit/" +
row.ID
)
}
size={"small"}
>
<Edit />
</IconButton>
</Tooltip>
<Tooltip title={"删除"}>
<IconButton
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

@ -0,0 +1,553 @@
import Button from "@material-ui/core/Button";
import Collapse from "@material-ui/core/Collapse";
import FormControl from "@material-ui/core/FormControl";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import FormHelperText from "@material-ui/core/FormHelperText";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import { makeStyles } from "@material-ui/core/styles";
import Switch from "@material-ui/core/Switch";
import Typography from "@material-ui/core/Typography";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
import SizeInput from "../Common/SizeInput";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
// function getStyles(name, personName, theme) {
// return {
// fontWeight:
// personName.indexOf(name) === -1
// ? theme.typography.fontWeightRegular
// : theme.typography.fontWeightMedium
// };
// }
export default function GroupForm(props) {
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [group, setGroup] = useState(
props.group
? props.group
: {
ID: 0,
Name: "",
MaxStorage: "1073741824", // 转换类型
ShareEnabled: "true", // 转换类型
WebDAVEnabled: "true", // 转换类型
SpeedLimit: "0", // 转换类型
PolicyList: 1, // 转换类型,至少选择一个
OptionsSerialized: {
// 批量转换类型
share_download: "true",
aria2_options: "{}", // json decode
compress_size: "0",
decompress_size: "0",
},
}
);
const [policies, setPolicies] = useState({});
const history = useHistory();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/policy/list", {
page: 1,
page_size: 10000,
order_by: "id asc",
conditions: {},
})
.then((response) => {
const res = {};
response.data.items.forEach((v) => {
res[v.ID] = v.Name;
});
setPolicies(res);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
const handleChange = (name) => (event) => {
setGroup({
...group,
[name]: event.target.value,
});
};
const handleCheckChange = (name) => (event) => {
const value = event.target.checked ? "true" : "false";
setGroup({
...group,
[name]: value,
});
};
const handleOptionCheckChange = (name) => (event) => {
const value = event.target.checked ? "true" : "false";
setGroup({
...group,
OptionsSerialized: {
...group.OptionsSerialized,
[name]: value,
},
});
};
const handleOptionChange = (name) => (event) => {
setGroup({
...group,
OptionsSerialized: {
...group.OptionsSerialized,
[name]: event.target.value,
},
});
};
const submit = (e) => {
e.preventDefault();
const groupCopy = {
...group,
OptionsSerialized: { ...group.OptionsSerialized },
};
// 布尔值转换
["ShareEnabled", "WebDAVEnabled"].forEach((v) => {
groupCopy[v] = groupCopy[v] === "true";
});
[
"archive_download",
"archive_task",
"one_time_download",
"share_download",
"aria2",
].forEach((v) => {
if (groupCopy.OptionsSerialized[v] !== undefined) {
groupCopy.OptionsSerialized[v] =
groupCopy.OptionsSerialized[v] === "true";
}
});
// 整型转换
["MaxStorage", "SpeedLimit"].forEach((v) => {
groupCopy[v] = parseInt(groupCopy[v]);
});
["compress_size", "decompress_size"].forEach((v) => {
if (groupCopy.OptionsSerialized[v] !== undefined) {
groupCopy.OptionsSerialized[v] = parseInt(
groupCopy.OptionsSerialized[v]
);
}
});
groupCopy.PolicyList = [parseInt(groupCopy.PolicyList)];
// JSON转换
try {
groupCopy.OptionsSerialized.aria2_options = JSON.parse(
groupCopy.OptionsSerialized.aria2_options
);
} catch (e) {
ToggleSnackbar("top", "right", "Aria2 设置项格式错误", "warning");
return;
}
setLoading(true);
API.post("/admin/group", {
group: groupCopy,
})
.then(() => {
history.push("/admin/group");
ToggleSnackbar(
"top",
"right",
"用户组已" + (props.group ? "保存" : "添加"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{group.ID === 0 && "新建用户组"}
{group.ID !== 0 && "编辑 " + group.Name}
</Typography>
<div className={classes.formContainer}>
{group.ID !== 3 && (
<>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
用户组名
</InputLabel>
<Input
value={group.Name}
onChange={handleChange("Name")}
required
/>
<FormHelperText id="component-helper-text">
用户组的名称
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
存储策略
</InputLabel>
<Select
labelId="demo-mutiple-chip-label"
id="demo-mutiple-chip"
value={group.PolicyList}
onChange={handleChange(
"PolicyList"
)}
input={
<Input id="select-multiple-chip" />
}
>
{Object.keys(policies).map(
(pid) => (
<MenuItem
key={pid}
value={pid}
>
{policies[pid]}
</MenuItem>
)
)}
</Select>
<FormHelperText id="component-helper-text">
指定用户组的存储策略
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<SizeInput
value={group.MaxStorage}
onChange={handleChange(
"MaxStorage"
)}
min={0}
max={9223372036854775807}
label={"初始容量"}
required
/>
</FormControl>
<FormHelperText id="component-helper-text">
用户组下的用户初始可用最大容量
</FormHelperText>
</div>
</>
)}
<div className={classes.form}>
<FormControl fullWidth>
<SizeInput
value={group.SpeedLimit}
onChange={handleChange("SpeedLimit")}
min={0}
max={9223372036854775807}
label={"下载限速"}
suffix={"/s"}
required
/>
</FormControl>
<FormHelperText id="component-helper-text">
填写为 0 表示不限制开启限制后
此用户组下的用户下载所有支持限速的存储策略下的文件时下载最大速度会被限制
</FormHelperText>
</div>
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.ShareEnabled ===
"true"
}
onChange={handleCheckChange(
"ShareEnabled"
)}
/>
}
label="允许创建分享"
/>
<FormHelperText id="component-helper-text">
关闭后用户无法创建分享链接
</FormHelperText>
</FormControl>
</div>
)}
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.share_download === "true"
}
onChange={handleOptionCheckChange(
"share_download"
)}
/>
}
label="允许下载分享"
/>
<FormHelperText id="component-helper-text">
关闭后用户无法下载别人创建的文件分享
</FormHelperText>
</FormControl>
</div>
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.WebDAVEnabled ===
"true"
}
onChange={handleCheckChange(
"WebDAVEnabled"
)}
/>
}
label="WebDAV"
/>
<FormHelperText id="component-helper-text">
关闭后用户无法通过 WebDAV
协议连接至网盘
</FormHelperText>
</FormControl>
</div>
)}
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.one_time_download ===
"true"
}
onChange={handleOptionCheckChange(
"one_time_download"
)}
/>
}
label="禁止多次下载请求"
/>
<FormHelperText id="component-helper-text">
只针对本机存储策略有效开启后用户无法使用多线程下载工具
</FormHelperText>
</FormControl>
</div>
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.aria2 === "true"
}
onChange={handleOptionCheckChange(
"aria2"
)}
/>
}
label="离线下载"
/>
<FormHelperText id="component-helper-text">
是否允许用户创建离线下载任务
</FormHelperText>
</FormControl>
</div>
)}
<Collapse in={group.OptionsSerialized.aria2 === "true"}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
Aria2 任务参数
</InputLabel>
<Input
multiline
value={
group.OptionsSerialized
.aria2_options
}
onChange={handleOptionChange(
"aria2_options"
)}
/>
<FormHelperText id="component-helper-text">
此用户组创建离线下载任务时额外携带的参数
JSON
编码后的格式书写您可也可以将这些设置写在
Aria2 配置文件里可用参数请查阅官方文档
</FormHelperText>
</FormControl>
</div>
</Collapse>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.archive_download === "true"
}
onChange={handleOptionCheckChange(
"archive_download"
)}
/>
}
label="打包下载"
/>
<FormHelperText id="component-helper-text">
是否允许用户多选文件打包下载
</FormHelperText>
</FormControl>
</div>
{group.ID !== 3 && (
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
group.OptionsSerialized
.archive_task === "true"
}
onChange={handleOptionCheckChange(
"archive_task"
)}
/>
}
label="压缩/解压缩 任务"
/>
<FormHelperText id="component-helper-text">
是否用户创建 压缩/解压缩 任务
</FormHelperText>
</FormControl>
</div>
)}
<Collapse
in={group.OptionsSerialized.archive_task === "true"}
>
<div className={classes.form}>
<FormControl fullWidth>
<SizeInput
value={
group.OptionsSerialized
.compress_size
}
onChange={handleOptionChange(
"compress_size"
)}
min={0}
max={9223372036854775807}
label={"待压缩文件最大大小"}
/>
</FormControl>
<FormHelperText id="component-helper-text">
用户可创建的压缩任务的文件最大总大小填写为
0 表示不限制
</FormHelperText>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<SizeInput
value={
group.OptionsSerialized
.decompress_size
}
onChange={handleOptionChange(
"decompress_size"
)}
min={0}
max={9223372036854775807}
label={"待解压文件最大大小"}
/>
</FormControl>
<FormHelperText id="component-helper-text">
用户可创建的解压缩任务的文件最大总大小填写为
0 表示不限制
</FormHelperText>
</div>
</Collapse>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
保存
</Button>
</div>
</form>
</div>
);
}

@ -0,0 +1,509 @@
import Avatar from "@material-ui/core/Avatar";
import Button from "@material-ui/core/Button";
import Chip from "@material-ui/core/Chip";
import { blue, green, red, yellow } from "@material-ui/core/colors";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import Divider from "@material-ui/core/Divider";
import Grid from "@material-ui/core/Grid";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemAvatar from "@material-ui/core/ListItemAvatar";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import Paper from "@material-ui/core/Paper";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import {
Description,
Favorite,
FileCopy,
Forum,
GitHub,
Home,
Launch,
Lock,
People,
Public,
Telegram,
} from "@material-ui/icons";
import axios from "axios";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import {
CartesianGrid,
Legend,
Line,
LineChart,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { ResponsiveContainer } from "recharts/lib/component/ResponsiveContainer";
import TimeAgo from "timeago-react";
import { toggleSnackbar } from "../../actions";
import API from "../../middleware/Api";
import pathHelper from "../../utils/page";
const useStyles = makeStyles((theme) => ({
paper: {
padding: theme.spacing(3),
height: "100%",
},
logo: {
width: 70,
},
logoContainer: {
padding: theme.spacing(3),
display: "flex",
},
title: {
marginLeft: 16,
},
cloudreve: {
fontSize: 25,
color: theme.palette.text.secondary,
},
version: {
color: theme.palette.text.hint,
},
links: {
padding: theme.spacing(3),
},
iconRight: {
minWidth: 0,
},
userIcon: {
backgroundColor: blue[100],
color: blue[600],
},
fileIcon: {
backgroundColor: yellow[100],
color: yellow[800],
},
publicIcon: {
backgroundColor: green[100],
color: green[800],
},
secretIcon: {
backgroundColor: red[100],
color: red[800],
},
}));
export default function Index() {
const classes = useStyles();
const [lineData, setLineData] = useState([]);
const [news, setNews] = useState([]);
const [newsUsers, setNewsUsers] = useState({});
const [open, setOpen] = React.useState(false);
const [siteURL, setSiteURL] = React.useState("");
const [statistics, setStatistics] = useState({
fileTotal: 0,
userTotal: 0,
publicShareTotal: 0,
secretShareTotal: 0,
});
const [version, setVersion] = useState({
backend: "-",
});
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const ResetSiteURL = () => {
setOpen(false);
API.patch("/admin/setting", {
options: [
{
key: "siteURL",
value: window.location.origin,
},
],
})
.then(() => {
setSiteURL(window.location.origin);
ToggleSnackbar("top", "right", "设置已更改", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
API.get("/admin/summary")
.then((response) => {
const data = [];
response.data.date.forEach((v, k) => {
data.push({
name: v,
file: response.data.files[k],
user: response.data.users[k],
share: response.data.shares[k],
});
});
setLineData(data);
setStatistics({
fileTotal: response.data.fileTotal,
userTotal: response.data.userTotal,
publicShareTotal: response.data.publicShareTotal,
secretShareTotal: response.data.secretShareTotal,
});
setVersion(response.data.version);
setSiteURL(response.data.siteURL);
if (
response.data.siteURL === "" ||
response.data.siteURL !== window.location.origin
) {
setOpen(true);
}
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
axios
.get("/api/v3/admin/news")
.then((response) => {
setNews(response.data.data);
const res = {};
response.data.included.forEach((v) => {
if (v.type === "users") {
res[v.id] = v.attributes;
}
});
setNewsUsers(res);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
return (
<Grid container spacing={3}>
<Dialog
open={open}
onClose={() => setOpen(false)}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{"确定站点URL设置"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
<Typography>
{siteURL === "" &&
"您尚未设定站点URL是否要将其设定为当前的 " +
window.location.origin +
" ?"}
{siteURL !== "" &&
"您设置的站点URL与当前实际不一致是否要将其设定为当前的 " +
window.location.origin +
" ?"}
</Typography>
<Typography>
此设置非常重要请确保其与您站点的实际地址一致你可以在
参数设置 - 站点信息 中更改此设置
</Typography>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)} color="default">
忽略
</Button>
<Button onClick={() => ResetSiteURL()} color="primary">
更改
</Button>
</DialogActions>
</Dialog>
<Grid alignContent={"stretch"} item xs={12} md={8} lg={9}>
<Paper className={classes.paper}>
<Typography variant="button" display="block" gutterBottom>
趋势
</Typography>
<ResponsiveContainer
width="100%"
aspect={pathHelper.isMobile() ? 4.0 / 3.0 : 3.0 / 1.0}
>
<LineChart width={1200} height={300} data={lineData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Legend />
<Line
name={"文件"}
type="monotone"
dataKey="file"
stroke="#3f51b5"
/>
<Line
name={"用户"}
type="monotone"
dataKey="user"
stroke="#82ca9d"
/>
<Line
name={"分享"}
type="monotone"
dataKey="share"
stroke="#e91e63"
/>
</LineChart>
</ResponsiveContainer>
</Paper>
</Grid>
<Grid item xs={12} md={4} lg={3}>
<Paper className={classes.paper}>
<Typography variant="button" display="block" gutterBottom>
总计
</Typography>
<Divider />
<List className={classes.root}>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.userIcon}>
<People />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={statistics.userTotal}
secondary="注册用户"
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.fileIcon}>
<FileCopy />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={statistics.fileTotal}
secondary="文件总数"
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.publicIcon}>
<Public />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={statistics.publicShareTotal}
secondary="公开分享总数"
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.secretIcon}>
<Lock />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={statistics.secretShareTotal}
secondary="私密分享总数"
/>
</ListItem>
</List>
</Paper>
</Grid>
<Grid item xs={12} md={4} lg={3}>
<Paper>
<div className={classes.logoContainer}>
<img
alt="网盘"
className={classes.logo}
src={"/static/img/cloudreve.svg"}
/>
<div className={classes.title}>
<Typography className={classes.cloudreve}>
Cloudreve
</Typography>
<Typography className={classes.version}>
{version.backend}{" "}
{version.is_pro === "true" && (
<Chip size="small" label="Pro" />
)}
</Typography>
</div>
</div>
<Divider />
<div>
<List component="nav" aria-label="main mailbox folders">
<ListItem
button
onClick={() =>
window.open("https://cloudreve.org")
}
>
<ListItemIcon>
<Home />
</ListItemIcon>
<ListItemText primary="主页" />
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open(
"https://github.com/cloudreve/cloudreve"
)
}
>
<ListItemIcon>
<GitHub />
</ListItemIcon>
<ListItemText primary="GitHub" />
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open("https://docs.cloudreve.org/")
}
>
<ListItemIcon>
<Description />
</ListItemIcon>
<ListItemText primary="文档" />
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open("https://forum.cloudreve.org")
}
>
<ListItemIcon>
<Forum />
</ListItemIcon>
<ListItemText primary="讨论社区" />
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open(
"https://t.me/cloudreve_official"
)
}
>
<ListItemIcon>
<Telegram />
</ListItemIcon>
<ListItemText primary="Telegram 群组" />
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
<ListItem
button
onClick={() =>
window.open(
"https://docs.cloudreve.org/use/pro/jie-shao"
)
}
>
<ListItemIcon style={{ color: "#ff789d" }}>
<Favorite />
</ListItemIcon>
<ListItemText primary="升级到捐助版" />
<ListItemIcon className={classes.iconRight}>
<Launch />
</ListItemIcon>
</ListItem>
</List>
</div>
</Paper>
</Grid>
<Grid item xs={12} md={8} lg={9}>
<Paper className={classes.paper}>
<List>
{news &&
news.map((v) => (
<>
<ListItem
button
alignItems="flex-start"
onClick={() =>
window.open(
"https://forum.cloudreve.org/d/" +
v.id
)
}
>
<ListItemAvatar>
<Avatar
alt="Travis Howard"
src={
newsUsers[
v.relationships
.startUser.data.id
] &&
newsUsers[
v.relationships
.startUser.data.id
].avatarUrl
}
/>
</ListItemAvatar>
<ListItemText
primary={v.attributes.title}
secondary={
<React.Fragment>
<Typography
component="span"
variant="body2"
className={
classes.inline
}
color="textPrimary"
>
{newsUsers[
v.relationships
.startUser.data
.id
] &&
newsUsers[
v.relationships
.startUser
.data.id
].username}{" "}
</Typography>
发表于{" "}
<TimeAgo
datetime={
v.attributes
.startTime
}
locale="zh_CN"
/>
</React.Fragment>
}
/>
</ListItem>
<Divider />
</>
))}
</List>
</Paper>
</Grid>
</Grid>
);
}

@ -0,0 +1,45 @@
import Paper from "@material-ui/core/Paper";
import { makeStyles } from "@material-ui/core/styles";
import React from "react";
import { useParams } from "react-router";
import COSGuide from "./Guid/COSGuide";
import LocalGuide from "./Guid/LocalGuide";
import OneDriveGuide from "./Guid/OneDriveGuide";
import OSSGuide from "./Guid/OSSGuide";
import QiniuGuide from "./Guid/QiniuGuide";
import RemoteGuide from "./Guid/RemoteGuide";
import UpyunGuide from "./Guid/UpyunGuide";
import S3Guide from "./Guid/S3Guide";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
}));
export default function AddPolicyParent() {
const classes = useStyles();
const { type } = useParams();
return (
<div>
<Paper square className={classes.content}>
{type === "local" && <LocalGuide />}
{type === "remote" && <RemoteGuide />}
{type === "qiniu" && <QiniuGuide />}
{type === "oss" && <OSSGuide />}
{type === "upyun" && <UpyunGuide />}
{type === "cos" && <COSGuide />}
{type === "onedrive" && <OneDriveGuide />}
{type === "s3" && <S3Guide />}
</Paper>
</div>
);
}

@ -0,0 +1,93 @@
import Paper from "@material-ui/core/Paper";
import { makeStyles } from "@material-ui/core/styles";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useParams } from "react-router";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
import COSGuide from "./Guid/COSGuide";
import EditPro from "./Guid/EditPro";
import LocalGuide from "./Guid/LocalGuide";
import OneDriveGuide from "./Guid/OneDriveGuide";
import OSSGuide from "./Guid/OSSGuide";
import QiniuGuide from "./Guid/QiniuGuide";
import RemoteGuide from "./Guid/RemoteGuide";
import UpyunGuide from "./Guid/UpyunGuide";
import S3Guide from "./Guid/S3Guide";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
}));
export default function EditPolicyPreload() {
const classes = useStyles();
const [type, setType] = useState("");
const [policy, setPolicy] = useState({});
const { mode, id } = useParams();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
setType("");
API.get("/admin/policy/" + id)
.then((response) => {
response.data.IsOriginLinkEnable = response.data
.IsOriginLinkEnable
? "true"
: "false";
response.data.AutoRename = response.data.AutoRename
? "true"
: "false";
response.data.MaxSize = response.data.MaxSize.toString();
response.data.IsPrivate = response.data.IsPrivate
? "true"
: "false";
response.data.OptionsSerialized.file_type = response.data
.OptionsSerialized.file_type
? response.data.OptionsSerialized.file_type.join(",")
: "";
setPolicy(response.data);
setType(response.data.Type);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, [id]);
return (
<div>
<Paper square className={classes.content}>
{mode === "guide" && (
<>
{type === "local" && <LocalGuide policy={policy} />}
{type === "remote" && <RemoteGuide policy={policy} />}
{type === "qiniu" && <QiniuGuide policy={policy} />}
{type === "oss" && <OSSGuide policy={policy} />}
{type === "upyun" && <UpyunGuide policy={policy} />}
{type === "cos" && <COSGuide policy={policy} />}
{type === "onedrive" && (
<OneDriveGuide policy={policy} />
)}
{type === "s3" && <S3Guide policy={policy} />}
</>
)}
{mode === "pro" && type !== "" && <EditPro policy={policy} />}
</Paper>
</div>
);
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,548 @@
import Button from "@material-ui/core/Button";
import FormControl from "@material-ui/core/FormControl";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Input from "@material-ui/core/Input";
import Radio from "@material-ui/core/Radio";
import RadioGroup from "@material-ui/core/RadioGroup";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Typography from "@material-ui/core/Typography";
import React, { useCallback, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../../actions";
import API from "../../../../middleware/Api";
export default function EditPro(props) {
const [, setLoading] = useState(false);
const [policy, setPolicy] = useState(props.policy);
const handleChange = (name) => (event) => {
setPolicy({
...policy,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
[name]: event.target.value,
},
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const submitPolicy = (e) => {
e.preventDefault();
setLoading(true);
const policyCopy = { ...policy };
policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized };
// 类型转换
policyCopy.AutoRename = policyCopy.AutoRename === "true";
policyCopy.IsPrivate = policyCopy.IsPrivate === "true";
policyCopy.IsOriginLinkEnable =
policyCopy.IsOriginLinkEnable === "true";
policyCopy.MaxSize = parseInt(policyCopy.MaxSize);
policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split(
","
);
if (
policyCopy.OptionsSerialized.file_type.length === 1 &&
policyCopy.OptionsSerialized.file_type[0] === ""
) {
policyCopy.OptionsSerialized.file_type = [];
}
API.post("/admin/policy", {
policy: policyCopy,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
"存储策略已" + (props.policy ? "保存" : "添加"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
setLoading(false);
};
return (
<div>
<Typography variant={"h6"}>编辑存储策略</Typography>
<TableContainer>
<form onSubmit={submitPolicy}>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>设置项</TableCell>
<TableCell></TableCell>
<TableCell>描述</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell component="th" scope="row">
ID
</TableCell>
<TableCell>{policy.ID}</TableCell>
<TableCell>存储策略编号</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
类型
</TableCell>
<TableCell>{policy.Type}</TableCell>
<TableCell>存储策略类型</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
名称
</TableCell>
<TableCell>
<FormControl>
<Input
required
value={policy.Name}
onChange={handleChange("Name")}
/>
</FormControl>
</TableCell>
<TableCell>存储策名称</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Server
</TableCell>
<TableCell>
<FormControl>
<Input
value={policy.Server}
onChange={handleChange("Server")}
/>
</FormControl>
</TableCell>
<TableCell>存储端 Endpoint</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
BucketName
</TableCell>
<TableCell>
<FormControl>
<Input
value={policy.BucketName}
onChange={handleChange(
"BucketName"
)}
/>
</FormControl>
</TableCell>
<TableCell>存储桶标识</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
私有空间
</TableCell>
<TableCell>
<FormControl>
<RadioGroup
required
value={policy.IsPrivate}
onChange={handleChange("IsPrivate")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="是"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="否"
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>是否为私有空间</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
文件资源根URL
</TableCell>
<TableCell>
<FormControl>
<Input
value={policy.BaseURL}
onChange={handleChange("BaseURL")}
/>
</FormControl>
</TableCell>
<TableCell>
预览/获取文件外链时生成URL的前缀
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
AccessKey
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
rowsMax={10}
value={policy.AccessKey}
onChange={handleChange("AccessKey")}
/>
</FormControl>
</TableCell>
<TableCell>AccessKey / 刷新Token</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
SecretKey
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
rowsMax={10}
value={policy.SecretKey}
onChange={handleChange("SecretKey")}
/>
</FormControl>
</TableCell>
<TableCell>SecretKey</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
最大单文件尺寸 (Bytes)
</TableCell>
<TableCell>
<FormControl>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={policy.MaxSize}
onChange={handleChange("MaxSize")}
/>
</FormControl>
</TableCell>
<TableCell>
最大可上传的文件尺寸填写为0表示不限制
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
自动重命名
</TableCell>
<TableCell>
<FormControl>
<RadioGroup
required
value={policy.AutoRename}
onChange={handleChange(
"AutoRename"
)}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="是"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="否"
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>
是否根据规则对上传物理文件重命名
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
存储路径
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={policy.DirNameRule}
onChange={handleChange(
"DirNameRule"
)}
/>
</FormControl>
</TableCell>
<TableCell>文件物理存储路径</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
存储文件名
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={policy.FileNameRule}
onChange={handleChange(
"FileNameRule"
)}
/>
</FormControl>
</TableCell>
<TableCell>文件物理存储文件名</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
允许获取外链
</TableCell>
<TableCell>
<FormControl>
<RadioGroup
required
value={policy.IsOriginLinkEnable}
onChange={handleChange(
"IsOriginLinkEnable"
)}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="是"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="否"
/>
</RadioGroup>
</FormControl>
</TableCell>
<TableCell>
是否允许获取外链注意某些存储策略类型不支持即使在此开启获取的外链也无法使用
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
又拍云防盗链 Token
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized.token
}
onChange={handleOptionChange(
"token"
)}
/>
</FormControl>
</TableCell>
<TableCell>仅对又拍云存储策略有效</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
允许文件扩展名
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.file_type
}
onChange={handleOptionChange(
"file_type"
)}
/>
</FormControl>
</TableCell>
<TableCell>留空表示不限制</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
允许的 MimeType
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.mimetype
}
onChange={handleOptionChange(
"mimetype"
)}
/>
</FormControl>
</TableCell>
<TableCell>仅对七牛存储策略有效</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
OneDrive 重定向地址
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.od_redirect
}
onChange={handleOptionChange(
"od_redirect"
)}
/>
</FormControl>
</TableCell>
<TableCell>一般添加后无需修改</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
OneDrive 反代服务器地址
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.od_proxy
}
onChange={handleOptionChange(
"od_proxy"
)}
/>
</FormControl>
</TableCell>
<TableCell>
仅对 OneDrive 存储策略有效
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
OneDrive/SharePoint 驱动器资源标识
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.od_driver
}
onChange={handleOptionChange(
"od_driver"
)}
/>
</FormControl>
</TableCell>
<TableCell>
仅对 OneDrive
存储策略有效留空则使用用户的默认 OneDrive
驱动器
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Amazon S3 Region
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized.region
}
onChange={handleOptionChange(
"region"
)}
/>
</FormControl>
</TableCell>
<TableCell>
仅对 Amazon S3 存储策略有效
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
内网 EndPoint
</TableCell>
<TableCell>
<FormControl>
<Input
multiline
value={
policy.OptionsSerialized
.server_side_endpoint
}
onChange={handleOptionChange(
"server_side_endpoint"
)}
/>
</FormControl>
</TableCell>
<TableCell>仅对 OSS 存储策略有效</TableCell>
</TableRow>
</TableBody>
</Table>
<Button
type={"submit"}
color={"primary"}
variant={"contained"}
style={{ margin: 8 }}
>
保存更改
</Button>
</form>
</TableContainer>
</div>
);
}

@ -0,0 +1,776 @@
import Button from "@material-ui/core/Button";
import Collapse from "@material-ui/core/Collapse";
import FormControl from "@material-ui/core/FormControl";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import Link from "@material-ui/core/Link";
import Radio from "@material-ui/core/Radio";
import RadioGroup from "@material-ui/core/RadioGroup";
import Step from "@material-ui/core/Step";
import StepLabel from "@material-ui/core/StepLabel";
import Stepper from "@material-ui/core/Stepper";
import { lighten, makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import React, { useCallback, useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../../actions";
import API from "../../../../middleware/Api";
import DomainInput from "../../Common/DomainInput";
import SizeInput from "../../Common/SizeInput";
import MagicVar from "../../Dialogs/MagicVar";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
}));
const steps = [
{
title: "上传路径",
optional: false,
},
{
title: "直链设置",
optional: false,
},
{
title: "上传限制",
optional: false,
},
{
title: "完成",
optional: false,
},
];
export default function LocalGuide(props) {
const classes = useStyles();
const history = useHistory();
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(false);
const [skipped] = React.useState(new Set());
const [magicVar, setMagicVar] = useState("");
const [useCDN, setUseCDN] = useState("false");
const [policy, setPolicy] = useState(
props.policy
? props.policy
: {
Type: "local",
Name: "",
DirNameRule: "uploads/{uid}/{path}",
AutoRename: "true",
FileNameRule: "{randomkey8}_{originname}",
IsOriginLinkEnable: "false",
BaseURL: "",
IsPrivate: "true",
MaxSize: "0",
OptionsSerialized: {
file_type: "",
},
}
);
const handleChange = (name) => (event) => {
setPolicy({
...policy,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
[name]: event.target.value,
},
});
};
const isStepSkipped = (step) => {
return skipped.has(step);
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const checkPathSetting = (e) => {
e.preventDefault();
setLoading(true);
// 测试路径是否可用
API.post("/admin/policy/test/path", {
path: policy.DirNameRule,
})
.then(() => {
setActiveStep(1);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const submitPolicy = (e) => {
e.preventDefault();
setLoading(true);
const policyCopy = { ...policy };
policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized };
// 处理存储策略
if (useCDN === "false" || policy.IsOriginLinkEnable === "false") {
policyCopy.BaseURL = "";
}
// 类型转换
policyCopy.AutoRename = policyCopy.AutoRename === "true";
policyCopy.IsOriginLinkEnable =
policyCopy.IsOriginLinkEnable === "true";
policyCopy.MaxSize = parseInt(policyCopy.MaxSize);
policyCopy.IsPrivate = policyCopy.IsPrivate === "true";
policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split(
","
);
if (
policyCopy.OptionsSerialized.file_type.length === 1 &&
policyCopy.OptionsSerialized.file_type[0] === ""
) {
policyCopy.OptionsSerialized.file_type = [];
}
API.post("/admin/policy", {
policy: policyCopy,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
"存储策略已" + (props.policy ? "保存" : "添加"),
"success"
);
setActiveStep(4);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
setLoading(false);
};
return (
<div>
<Typography variant={"h6"}>
{props.policy ? "修改" : "添加"}本机存储策略
</Typography>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (label.optional) {
labelProps.optional = (
<Typography variant="caption">可选</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label.title} {...stepProps}>
<StepLabel {...labelProps}>{label.title}</StepLabel>
</Step>
);
})}
</Stepper>
{activeStep === 0 && (
<form
className={classes.stepContent}
onSubmit={checkPathSetting}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
请在下方输入文件的存储目录路径可以为绝对路径或相对路径相对于
Cloudreve路径中可以使用魔法变量文件在上传时会自动替换这些变量为相应值
可用魔法变量可参考{" "}
<Link
color={"secondary"}
onClick={(e) => {
e.preventDefault();
setMagicVar("path");
}}
>
路径魔法变量列表
</Link>{" "}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
存储目录
</InputLabel>
<Input
required
value={policy.DirNameRule}
onChange={handleChange("DirNameRule")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否需要对存储的物理文件进行重命名此处的重命名不会影响最终呈现给用户的
文件名文件名也可使用魔法变量
可用魔法变量可参考{" "}
<Link
color={"secondary"}
onClick={(e) => {
e.preventDefault();
setMagicVar("file");
}}
>
文件名魔法变量列表
</Link>{" "}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
aria-label="gender"
name="gender1"
value={policy.AutoRename}
onChange={handleChange("AutoRename")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="开启重命名"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不开启"
/>
</RadioGroup>
</FormControl>
</div>
<Collapse in={policy.AutoRename === "true"}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
命名规则
</InputLabel>
<Input
required={
policy.AutoRename === "true"
}
value={policy.FileNameRule}
onChange={handleChange(
"FileNameRule"
)}
/>
</FormControl>
</div>
</Collapse>
</div>
</div>
<div className={classes.stepFooter}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 1 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(2);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否允许获取文件永久直链
<br />
开启后用户可以请求获得能直接访问到文件内容的直链适用于图床应用或自用
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={policy.IsOriginLinkEnable}
onChange={handleChange(
"IsOriginLinkEnable"
)}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="允许"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="禁止"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.IsOriginLinkEnable === "true"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否要对下载/直链使用 CDN
<br />
开启后用户访问文件时的 URL
中的域名部分会被替换为 CDN 域名
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={useCDN}
onChange={(e) => {
if (
e.target.value === "false"
) {
setPolicy({
...policy,
BaseURL: "",
});
}
setUseCDN(e.target.value);
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="使用"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不使用"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={useCDN === "true"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>3</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
选择协议并填写 CDN 域名
</Typography>
<div className={classes.form}>
<DomainInput
value={policy.BaseURL}
onChange={handleChange("BaseURL")}
required={
policy.IsOriginLinkEnable ===
"true" && useCDN === "true"
}
label={"CDN 前缀"}
/>
</div>
</div>
</div>
</Collapse>
</Collapse>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(0)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 2 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(3);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否限制上传的单文件大小
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.MaxSize === "0"
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
MaxSize: "10485760",
});
} else {
setPolicy({
...policy,
MaxSize: "0",
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="限制"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不限制"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.MaxSize !== "0"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
输入限制
</Typography>
<div className={classes.form}>
<SizeInput
value={policy.MaxSize}
onChange={handleChange("MaxSize")}
min={0}
max={9223372036854775807}
label={"单文件大小限制"}
/>
</div>
</div>
</div>
</Collapse>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "3" : "2"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否限制上传文件扩展名
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.OptionsSerialized
.file_type === ""
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type:
"jpg,png,mp4,zip,rar",
},
});
} else {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type: "",
},
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="限制"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不限制"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.OptionsSerialized.file_type !== ""}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "4" : "3"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
输入允许上传的文件扩展名多个请以半角逗号 ,
隔开
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
扩展名列表
</InputLabel>
<Input
value={
policy.OptionsSerialized
.file_type
}
onChange={handleOptionChange(
"file_type"
)}
/>
</FormControl>
</div>
</div>
</div>
</Collapse>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(1)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 3 && (
<form className={classes.stepContent} onSubmit={submitPolicy}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}></div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
最后一步为此存储策略命名
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
存储策略名
</InputLabel>
<Input
required
value={policy.Name}
onChange={handleChange("Name")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(2)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
完成
</Button>
</div>
</form>
)}
{activeStep === 4 && (
<>
<form className={classes.stepContent}>
<Typography>
存储策略已{props.policy ? "保存" : "添加"}
</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
要使用此存储策略请到用户组管理页面为相应用户组绑定此存储策略
</Typography>
</form>
<div className={classes.stepFooter}>
<Button
color={"primary"}
className={classes.button}
onClick={() => history.push("/admin/policy")}
>
返回存储策略列表
</Button>
</div>
</>
)}
<MagicVar
open={magicVar === "file"}
isFile
onClose={() => setMagicVar("")}
/>
<MagicVar
open={magicVar === "path"}
onClose={() => setMagicVar("")}
/>
</div>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,977 @@
import Button from "@material-ui/core/Button";
import Collapse from "@material-ui/core/Collapse";
import FormControl from "@material-ui/core/FormControl";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import Link from "@material-ui/core/Link";
import Radio from "@material-ui/core/Radio";
import RadioGroup from "@material-ui/core/RadioGroup";
import Step from "@material-ui/core/Step";
import StepLabel from "@material-ui/core/StepLabel";
import Stepper from "@material-ui/core/Stepper";
import { lighten, makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import React, { useCallback, useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../../actions";
import API from "../../../../middleware/Api";
import { getNumber } from "../../../../utils";
import DomainInput from "../../Common/DomainInput";
import SizeInput from "../../Common/SizeInput";
import MagicVar from "../../Dialogs/MagicVar";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
}));
const steps = [
{
title: "存储空间",
optional: false,
},
{
title: "上传路径",
optional: false,
},
{
title: "直链设置",
optional: false,
},
{
title: "上传限制",
optional: false,
},
{
title: "完成",
optional: false,
},
];
export default function RemoteGuide(props) {
const classes = useStyles();
const history = useHistory();
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(false);
const [skipped] = React.useState(new Set());
const [magicVar, setMagicVar] = useState("");
// const [useCDN, setUseCDN] = useState("false");
const [policy, setPolicy] = useState(
props.policy
? props.policy
: {
Type: "qiniu",
Name: "",
SecretKey: "",
AccessKey: "",
BaseURL: "",
IsPrivate: "true",
DirNameRule: "uploads/{year}/{month}/{day}",
AutoRename: "true",
FileNameRule: "{randomkey8}_{originname}",
IsOriginLinkEnable: "false",
MaxSize: "0",
OptionsSerialized: {
file_type: "",
mimetype: "",
},
}
);
const handleChange = (name) => (event) => {
setPolicy({
...policy,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
[name]: event.target.value,
},
});
};
const isStepSkipped = (step) => {
return skipped.has(step);
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const submitPolicy = (e) => {
e.preventDefault();
setLoading(true);
const policyCopy = { ...policy };
policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized };
// 类型转换
policyCopy.AutoRename = policyCopy.AutoRename === "true";
policyCopy.IsOriginLinkEnable =
policyCopy.IsOriginLinkEnable === "true";
policyCopy.IsPrivate = policyCopy.IsPrivate === "true";
policyCopy.MaxSize = parseInt(policyCopy.MaxSize);
policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split(
","
);
if (
policyCopy.OptionsSerialized.file_type.length === 1 &&
policyCopy.OptionsSerialized.file_type[0] === ""
) {
policyCopy.OptionsSerialized.file_type = [];
}
API.post("/admin/policy", {
policy: policyCopy,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
"存储策略已" + (props.policy ? "保存" : "添加"),
"success"
);
setActiveStep(5);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
setLoading(false);
};
return (
<div>
<Typography variant={"h6"}>
{props.policy ? "修改" : "添加"} 七牛 存储策略
</Typography>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (label.optional) {
labelProps.optional = (
<Typography variant="caption">可选</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label.title} {...stepProps}>
<StepLabel {...labelProps}>{label.title}</StepLabel>
</Step>
);
})}
</Stepper>
{activeStep === 0 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(1);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>0</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
在使用七牛存储策略前请确保您在 参数设置 -
站点信息 - 站点URL 中填写的 地址与实际相符并且
<strong>能够被外网正常访问</strong>
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
前往
<Link
href={"https://portal.qiniu.com/create"}
target={"_blank"}
>
七牛控制面板
</Link>
创建对象存储资源
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
在下方填写您在七牛创建存储空间时指定的存储空间名称
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
存储空间名称
</InputLabel>
<Input
required
value={policy.BucketName}
onChange={handleChange("BucketName")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>3</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
在下方选择您创建的空间类型推荐选择私有空间以获得更高的安全性私有空间无法开启获取直链功能
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={policy.IsPrivate}
onChange={handleChange("IsPrivate")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="私有"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="公有"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>4</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
填写您为存储空间绑定的 CDN 加速域名
</Typography>
<div className={classes.form}>
<DomainInput
value={policy.BaseURL}
onChange={handleChange("BaseURL")}
required
label={"CDN 加速域名"}
/>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>5</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
在七牛控制面板进入 个人中心 -
密钥管理在下方填写获得到的 AKSK
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
AK
</InputLabel>
<Input
required
value={policy.AccessKey}
onChange={handleChange("AccessKey")}
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
SK
</InputLabel>
<Input
required
value={policy.SecretKey}
onChange={handleChange("SecretKey")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 1 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(2);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
请在下方输入文件的存储目录路径可以为绝对路径或相对路径相对于
从机的
Cloudreve路径中可以使用魔法变量文件在上传时会自动替换这些变量为相应值
可用魔法变量可参考{" "}
<Link
color={"secondary"}
onClick={(e) => {
e.preventDefault();
setMagicVar("path");
}}
>
路径魔法变量列表
</Link>{" "}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
存储目录
</InputLabel>
<Input
required
value={policy.DirNameRule}
onChange={handleChange("DirNameRule")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否需要对存储的物理文件进行重命名此处的重命名不会影响最终呈现给用户的
文件名文件名也可使用魔法变量
可用魔法变量可参考{" "}
<Link
color={"secondary"}
onClick={(e) => {
e.preventDefault();
setMagicVar("file");
}}
>
文件名魔法变量列表
</Link>{" "}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
aria-label="gender"
name="gender1"
value={policy.AutoRename}
onChange={handleChange("AutoRename")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="开启重命名"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不开启"
/>
</RadioGroup>
</FormControl>
</div>
<Collapse in={policy.AutoRename === "true"}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
命名规则
</InputLabel>
<Input
required={
policy.AutoRename === "true"
}
value={policy.FileNameRule}
onChange={handleChange(
"FileNameRule"
)}
/>
</FormControl>
</div>
</Collapse>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(0)}
>
上一步
</Button>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 2 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(3);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否允许获取文件永久直链
<br />
开启后用户可以请求获得能直接访问到文件内容的直链适用于图床应用或自用
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={policy.IsOriginLinkEnable}
onChange={(e) => {
if (
policy.IsPrivate === "true" &&
e.target.value === "true"
) {
ToggleSnackbar(
"top",
"right",
"私有空间无法开启此功能",
"warning"
);
return;
}
handleChange("IsOriginLinkEnable")(
e
);
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="允许"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="禁止"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(1)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 3 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(4);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否限制上传的单文件大小
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.MaxSize === "0"
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
MaxSize: "10485760",
});
} else {
setPolicy({
...policy,
MaxSize: "0",
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="限制"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不限制"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.MaxSize !== "0"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
输入限制
</Typography>
<div className={classes.form}>
<SizeInput
value={policy.MaxSize}
onChange={handleChange("MaxSize")}
min={0}
max={9223372036854775807}
label={"单文件大小限制"}
/>
</div>
</div>
</div>
</Collapse>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "3" : "2"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否限制上传文件扩展名
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.OptionsSerialized
.file_type === ""
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type:
"jpg,png,mp4,zip,rar",
},
});
} else {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type: "",
},
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="限制"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不限制"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.OptionsSerialized.file_type !== ""}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "4" : "3"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
输入允许上传的文件扩展名多个请以半角逗号 ,
隔开
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
扩展名列表
</InputLabel>
<Input
value={
policy.OptionsSerialized
.file_type
}
onChange={handleOptionChange(
"file_type"
)}
/>
</FormControl>
</div>
</div>
</div>
</Collapse>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{getNumber(3, [
policy.MaxSize !== "0",
policy.OptionsSerialized.file_type !== "",
])}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否限制上传文件 MimeType
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.OptionsSerialized
.mimetype === ""
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
mimetype: "image/*",
},
});
} else {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
mimetype: "",
},
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="限制"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不限制"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.OptionsSerialized.mimetype !== ""}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{getNumber(4, [
policy.MaxSize !== "0",
policy.OptionsSerialized.file_type !==
"",
])}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
输入允许上传的 MimeType多个请以半角逗号 ,
隔开七牛服务器会侦测文件内容以判断
MimeType再用判断值跟指定值进行匹配匹配成功则允许上传
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
MimeType 列表
</InputLabel>
<Input
value={
policy.OptionsSerialized
.mimetype
}
onChange={handleOptionChange(
"mimetype"
)}
/>
</FormControl>
</div>
</div>
</div>
</Collapse>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(2)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 4 && (
<form className={classes.stepContent} onSubmit={submitPolicy}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}></div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
最后一步为此存储策略命名
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
存储策略名
</InputLabel>
<Input
required
value={policy.Name}
onChange={handleChange("Name")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(3)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
完成
</Button>
</div>
</form>
)}
{activeStep === 5 && (
<>
<form className={classes.stepContent}>
<Typography>
存储策略已{props.policy ? "保存" : "添加"}
</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
要使用此存储策略请到用户组管理页面为相应用户组绑定此存储策略
</Typography>
</form>
<div className={classes.stepFooter}>
<Button
color={"primary"}
className={classes.button}
onClick={() => history.push("/admin/policy")}
>
返回存储策略列表
</Button>
</div>
</>
)}
<MagicVar
open={magicVar === "file"}
isFile
onClose={() => setMagicVar("")}
/>
<MagicVar
open={magicVar === "path"}
onClose={() => setMagicVar("")}
/>
</div>
);
}

@ -0,0 +1,992 @@
import Button from "@material-ui/core/Button";
import Collapse from "@material-ui/core/Collapse";
import FormControl from "@material-ui/core/FormControl";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import Link from "@material-ui/core/Link";
import Radio from "@material-ui/core/Radio";
import RadioGroup from "@material-ui/core/RadioGroup";
import Step from "@material-ui/core/Step";
import StepLabel from "@material-ui/core/StepLabel";
import Stepper from "@material-ui/core/Stepper";
import { lighten, makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import Alert from "@material-ui/lab/Alert";
import React, { useCallback, useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../../actions";
import API from "../../../../middleware/Api";
import { randomStr } from "../../../../utils";
import DomainInput from "../../Common/DomainInput";
import SizeInput from "../../Common/SizeInput";
import MagicVar from "../../Dialogs/MagicVar";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
"@global": {
code: {
color: "rgba(0, 0, 0, 0.87)",
display: "inline-block",
padding: "2px 6px",
fontSize: "14px",
fontFamily:
' Consolas, "Liberation Mono", Menlo, Courier, monospace',
borderRadius: "2px",
backgroundColor: "rgba(255,229,100,0.1)",
},
pre: {
margin: "24px 0",
padding: "12px 18px",
overflow: "auto",
direction: "ltr",
borderRadius: "4px",
backgroundColor: "#272c34",
color: "#fff",
},
},
}));
const steps = [
{
title: "存储端配置",
optional: false,
},
{
title: "上传路径",
optional: false,
},
{
title: "直链设置",
optional: false,
},
{
title: "上传限制",
optional: false,
},
{
title: "完成",
optional: false,
},
];
export default function RemoteGuide(props) {
const classes = useStyles();
const history = useHistory();
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(false);
const [skipped] = React.useState(new Set());
const [magicVar, setMagicVar] = useState("");
const [useCDN, setUseCDN] = useState("false");
const [policy, setPolicy] = useState(
props.policy
? props.policy
: {
Type: "remote",
Name: "",
Server: "https://example.com:5212",
SecretKey: randomStr(64),
DirNameRule: "uploads/{year}/{month}/{day}",
AutoRename: "true",
FileNameRule: "{randomkey8}_{originname}",
IsOriginLinkEnable: "false",
BaseURL: "",
IsPrivate: "true",
MaxSize: "0",
OptionsSerialized: {
file_type: "",
},
}
);
const handleChange = (name) => (event) => {
setPolicy({
...policy,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
[name]: event.target.value,
},
});
};
const isStepSkipped = (step) => {
return skipped.has(step);
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const testSlave = () => {
setLoading(true);
// 测试路径是否可用
API.post("/admin/policy/test/slave", {
server: policy.Server,
secret: policy.SecretKey,
})
.then(() => {
ToggleSnackbar("top", "right", "通信正常", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const submitPolicy = (e) => {
e.preventDefault();
setLoading(true);
const policyCopy = { ...policy };
policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized };
// 处理存储策略
if (useCDN === "false" || policy.IsOriginLinkEnable === "false") {
policyCopy.BaseURL = "";
}
// 类型转换
policyCopy.AutoRename = policyCopy.AutoRename === "true";
policyCopy.IsOriginLinkEnable =
policyCopy.IsOriginLinkEnable === "true";
policyCopy.MaxSize = parseInt(policyCopy.MaxSize);
policyCopy.IsPrivate = policyCopy.IsPrivate === "true";
policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split(
","
);
if (
policyCopy.OptionsSerialized.file_type.length === 1 &&
policyCopy.OptionsSerialized.file_type[0] === ""
) {
policyCopy.OptionsSerialized.file_type = [];
}
API.post("/admin/policy", {
policy: policyCopy,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
"存储策略已" + (props.policy ? "保存" : "添加"),
"success"
);
setActiveStep(5);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
setLoading(false);
};
return (
<div>
<Typography variant={"h6"}>
{props.policy ? "修改" : "添加"}从机存储策略
</Typography>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (label.optional) {
labelProps.optional = (
<Typography variant="caption">可选</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label.title} {...stepProps}>
<StepLabel {...labelProps}>{label.title}</StepLabel>
</Step>
);
})}
</Stepper>
{activeStep === 0 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(1);
}}
>
<Alert severity="info" style={{ marginBottom: 10 }}>
从机存储策略允许你使用同样运行了 Cloudreve
的服务器作为存储端 用户上传下载流量通过 HTTP 直传
</Alert>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
将和主站相同版本的 Cloudreve
程序拷贝至要作为从机的服务器上
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
下方为系统为您随机生成的从机端密钥一般无需改动如果有自定义需求
可将您的密钥填入下方
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
从机密钥
</InputLabel>
<Input
required
inputProps={{
minlength: 64,
}}
value={policy.SecretKey}
onChange={handleChange("SecretKey")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>3</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
修改从机配置文件
<br />
在从机端 Cloudreve 的同级目录下新建
<code>conf.ini</code>
文件填入从机配置启动/重启从机端 Cloudreve
以下为一个可供参考的配置例子其中密钥部分已帮您填写为上一步所生成的
</Typography>
<pre>
[System]
<br />
Mode = slave
<br />
Listen = :5212
<br />
<br />
[Slave]
<br />
Secret = {policy.SecretKey}
<br />
<br />
[CORS]
<br />
AllowOrigins = *<br />
AllowMethods = OPTIONS,GET,POST
<br />
AllowHeaders = *<br />
</pre>
<Typography variant={"body2"}>
从机端配置文件格式大致与主站端相同区别在于
<ul>
<li>
<code>System</code>
分区下的
<code>mode</code>
字段必须更改为<code>slave</code>
</li>
<li>
必须指定<code>Slave</code>
<code>Secret</code>
字段其值为第二步里填写或生成的密钥
</li>
<li>
必须启动跨域配置<code>CORS</code>
字段的内容
具体可参考上文范例或官方文档如果配置不正确用户将无法通过
Web 端向从机上传文件
</li>
</ul>
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>4</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
填写从机地址
<br />
如果主站启用了
HTTPS从机也需要启用并在下方填入 HTTPS
协议的地址
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
从机地址
</InputLabel>
<Input
fullWidth
required
type={"url"}
value={policy.Server}
onChange={handleChange("Server")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>5</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
完成以上步骤后你可以点击下方的测试按钮测试通信是否正常
</Typography>
<div className={classes.form}>
<Button
disabled={loading}
onClick={() => testSlave()}
variant={"outlined"}
color={"primary"}
>
测试从机通信
</Button>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 1 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(2);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
请在下方输入文件的存储目录路径可以为绝对路径或相对路径相对于
从机的
Cloudreve路径中可以使用魔法变量文件在上传时会自动替换这些变量为相应值
可用魔法变量可参考{" "}
<Link
color={"secondary"}
onClick={(e) => {
e.preventDefault();
setMagicVar("path");
}}
>
路径魔法变量列表
</Link>{" "}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
存储目录
</InputLabel>
<Input
required
value={policy.DirNameRule}
onChange={handleChange("DirNameRule")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否需要对存储的物理文件进行重命名此处的重命名不会影响最终呈现给用户的
文件名文件名也可使用魔法变量
可用魔法变量可参考{" "}
<Link
color={"secondary"}
onClick={(e) => {
e.preventDefault();
setMagicVar("file");
}}
>
文件名魔法变量列表
</Link>{" "}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
aria-label="gender"
name="gender1"
value={policy.AutoRename}
onChange={handleChange("AutoRename")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="开启重命名"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不开启"
/>
</RadioGroup>
</FormControl>
</div>
<Collapse in={policy.AutoRename === "true"}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
命名规则
</InputLabel>
<Input
required={
policy.AutoRename === "true"
}
value={policy.FileNameRule}
onChange={handleChange(
"FileNameRule"
)}
/>
</FormControl>
</div>
</Collapse>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(0)}
>
上一步
</Button>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 2 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(3);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否允许获取文件永久直链
<br />
开启后用户可以请求获得能直接访问到文件内容的直链适用于图床应用或自用
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={policy.IsOriginLinkEnable}
onChange={handleChange(
"IsOriginLinkEnable"
)}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="允许"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="禁止"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.IsOriginLinkEnable === "true"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否要对下载/直链使用 CDN
<br />
开启后用户访问文件时的 URL
中的域名部分会被替换为 CDN 域名
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={useCDN}
onChange={(e) => {
if (
e.target.value === "false"
) {
setPolicy({
...policy,
BaseURL: "",
});
}
setUseCDN(e.target.value);
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="使用"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不使用"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={useCDN === "true"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>3</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
选择协议并填写 CDN 域名
</Typography>
<div className={classes.form}>
<DomainInput
value={policy.BaseURL}
onChange={handleChange("BaseURL")}
required={
policy.IsOriginLinkEnable ===
"true" && useCDN === "true"
}
label={"CDN 前缀"}
/>
</div>
</div>
</div>
</Collapse>
</Collapse>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(1)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 3 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(4);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否限制上传的单文件大小
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.MaxSize === "0"
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
MaxSize: "10485760",
});
} else {
setPolicy({
...policy,
MaxSize: "0",
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="限制"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不限制"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.MaxSize !== "0"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
输入限制
</Typography>
<div className={classes.form}>
<SizeInput
value={policy.MaxSize}
onChange={handleChange("MaxSize")}
min={0}
max={9223372036854775807}
label={"单文件大小限制"}
/>
</div>
</div>
</div>
</Collapse>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "3" : "2"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否限制上传文件扩展名
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.OptionsSerialized
.file_type === ""
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type:
"jpg,png,mp4,zip,rar",
},
});
} else {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type: "",
},
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="限制"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不限制"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.OptionsSerialized.file_type !== ""}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "4" : "3"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
输入允许上传的文件扩展名多个请以半角逗号 ,
隔开
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
扩展名列表
</InputLabel>
<Input
value={
policy.OptionsSerialized
.file_type
}
onChange={handleOptionChange(
"file_type"
)}
/>
</FormControl>
</div>
</div>
</div>
</Collapse>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(2)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 4 && (
<form className={classes.stepContent} onSubmit={submitPolicy}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}></div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
最后一步为此存储策略命名
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
存储策略名
</InputLabel>
<Input
required
value={policy.Name}
onChange={handleChange("Name")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(3)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
完成
</Button>
</div>
</form>
)}
{activeStep === 5 && (
<>
<form className={classes.stepContent}>
<Typography>
存储策略已{props.policy ? "保存" : "添加"}
</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
要使用此存储策略请到用户组管理页面为相应用户组绑定此存储策略
</Typography>
</form>
<div className={classes.stepFooter}>
<Button
color={"primary"}
className={classes.button}
onClick={() => history.push("/admin/policy")}
>
返回存储策略列表
</Button>
</div>
</>
)}
<MagicVar
open={magicVar === "file"}
isFile
isSlave
onClose={() => setMagicVar("")}
/>
<MagicVar
open={magicVar === "path"}
isSlave
onClose={() => setMagicVar("")}
/>
</div>
);
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,909 @@
import Button from "@material-ui/core/Button";
import Collapse from "@material-ui/core/Collapse";
import FormControl from "@material-ui/core/FormControl";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import Link from "@material-ui/core/Link";
import Radio from "@material-ui/core/Radio";
import RadioGroup from "@material-ui/core/RadioGroup";
import Step from "@material-ui/core/Step";
import StepLabel from "@material-ui/core/StepLabel";
import Stepper from "@material-ui/core/Stepper";
import { lighten, makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import React, { useCallback, useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../../actions";
import API from "../../../../middleware/Api";
import DomainInput from "../../Common/DomainInput";
import SizeInput from "../../Common/SizeInput";
import MagicVar from "../../Dialogs/MagicVar";
const useStyles = makeStyles((theme) => ({
stepContent: {
padding: "16px 32px 16px 32px",
},
form: {
maxWidth: 400,
marginTop: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
subStepContainer: {
display: "flex",
marginBottom: 20,
padding: 10,
transition: theme.transitions.create("background-color", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
"&:focus-within": {
backgroundColor: theme.palette.background.default,
},
},
stepNumber: {
width: 20,
height: 20,
backgroundColor: lighten(theme.palette.secondary.light, 0.2),
color: theme.palette.secondary.contrastText,
textAlign: "center",
borderRadius: " 50%",
},
stepNumberContainer: {
marginRight: 10,
},
stepFooter: {
marginTop: 32,
},
button: {
marginRight: theme.spacing(1),
},
}));
const steps = [
{
title: "存储空间",
optional: false,
},
{
title: "上传路径",
optional: false,
},
{
title: "直链设置",
optional: false,
},
{
title: "上传限制",
optional: false,
},
{
title: "完成",
optional: false,
},
];
export default function UpyunGuide(props) {
const classes = useStyles();
const history = useHistory();
const [activeStep, setActiveStep] = useState(0);
const [loading, setLoading] = useState(false);
const [skipped] = React.useState(new Set());
const [magicVar, setMagicVar] = useState("");
const [policy, setPolicy] = useState(
props.policy
? props.policy
: {
Type: "upyun",
Name: "",
SecretKey: "",
AccessKey: "",
BaseURL: "",
IsPrivate: "false",
DirNameRule: "uploads/{year}/{month}/{day}",
AutoRename: "true",
FileNameRule: "{randomkey8}_{originname}",
IsOriginLinkEnable: "false",
MaxSize: "0",
OptionsSerialized: {
file_type: "",
token: "",
},
}
);
const handleChange = (name) => (event) => {
setPolicy({
...policy,
[name]: event.target.value,
});
};
const handleOptionChange = (name) => (event) => {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
[name]: event.target.value,
},
});
};
const isStepSkipped = (step) => {
return skipped.has(step);
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const submitPolicy = (e) => {
e.preventDefault();
setLoading(true);
const policyCopy = { ...policy };
policyCopy.OptionsSerialized = { ...policyCopy.OptionsSerialized };
// 类型转换
policyCopy.AutoRename = policyCopy.AutoRename === "true";
policyCopy.IsOriginLinkEnable =
policyCopy.IsOriginLinkEnable === "true";
policyCopy.IsPrivate = policyCopy.IsPrivate === "true";
policyCopy.MaxSize = parseInt(policyCopy.MaxSize);
policyCopy.OptionsSerialized.file_type = policyCopy.OptionsSerialized.file_type.split(
","
);
if (
policyCopy.OptionsSerialized.file_type.length === 1 &&
policyCopy.OptionsSerialized.file_type[0] === ""
) {
policyCopy.OptionsSerialized.file_type = [];
}
API.post("/admin/policy", {
policy: policyCopy,
})
.then(() => {
ToggleSnackbar(
"top",
"right",
"存储策略已" + (props.policy ? "保存" : "添加"),
"success"
);
setActiveStep(5);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
setLoading(false);
};
return (
<div>
<Typography variant={"h6"}>
{props.policy ? "修改" : "添加"} 又拍云 存储策略
</Typography>
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (label.optional) {
labelProps.optional = (
<Typography variant="caption">可选</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label.title} {...stepProps}>
<StepLabel {...labelProps}>{label.title}</StepLabel>
</Step>
);
})}
</Stepper>
{activeStep === 0 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(1);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>0</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
在使用又拍云存储策略前请确保您在 参数设置 -
站点信息 - 站点URL 中填写的 地址与实际相符并且
<strong>能够被外网正常访问</strong>
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
前往
<Link
href={
"https://console.upyun.com/services/create/file/"
}
target={"_blank"}
>
又拍云面板
</Link>
创建云存储服务
</Typography>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
在下方填写所创建的服务名称
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
服务名称
</InputLabel>
<Input
required
value={policy.BucketName}
onChange={handleChange("BucketName")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>3</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
为此服务创建或授权有读取写入删除权限的操作员然后将操作员信息填写在下方
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
操作员名
</InputLabel>
<Input
required
value={policy.AccessKey}
onChange={handleChange("AccessKey")}
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
操作员密码
</InputLabel>
<Input
required
value={policy.SecretKey}
onChange={handleChange("SecretKey")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>4</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
填写为云存储服务绑定的域名并根据实际情况选择是否使用
HTTPS
</Typography>
<div className={classes.form}>
<DomainInput
value={policy.BaseURL}
onChange={handleChange("BaseURL")}
required
label={"加速域名"}
/>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>5</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
此步骤可保持默认并跳过但是强烈建议您跟随此步骤操作
<br />
前往所创建云存储服务的 功能配置 面板转到
访问配置 选项卡开启 Token 防盗链并设定密码
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={policy.IsPrivate}
onChange={handleChange("IsPrivate")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="已开启 Token 防盗链"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="未开启 Token 防盗链"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.IsPrivate === "true"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>6</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
填写您所设置的 Token 防盗链 密钥
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
Token 防盗链 密钥
</InputLabel>
<Input
value={
policy.OptionsSerialized.token
}
onChange={handleOptionChange(
"token"
)}
required={
policy.IsPrivate === "true"
}
/>
</FormControl>
</div>
</div>
</div>
</Collapse>
<div className={classes.stepFooter}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 1 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(2);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
请在下方输入文件的存储目录路径可以为绝对路径或相对路径相对于
从机的
Cloudreve路径中可以使用魔法变量文件在上传时会自动替换这些变量为相应值
可用魔法变量可参考{" "}
<Link
color={"secondary"}
onClick={(e) => {
e.preventDefault();
setMagicVar("path");
}}
>
路径魔法变量列表
</Link>{" "}
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
存储目录
</InputLabel>
<Input
required
value={policy.DirNameRule}
onChange={handleChange("DirNameRule")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否需要对存储的物理文件进行重命名此处的重命名不会影响最终呈现给用户的
文件名文件名也可使用魔法变量
可用魔法变量可参考{" "}
<Link
color={"secondary"}
onClick={(e) => {
e.preventDefault();
setMagicVar("file");
}}
>
文件名魔法变量列表
</Link>{" "}
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
aria-label="gender"
name="gender1"
value={policy.AutoRename}
onChange={handleChange("AutoRename")}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="开启重命名"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不开启"
/>
</RadioGroup>
</FormControl>
</div>
<Collapse in={policy.AutoRename === "true"}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
命名规则
</InputLabel>
<Input
required={
policy.AutoRename === "true"
}
value={policy.FileNameRule}
onChange={handleChange(
"FileNameRule"
)}
/>
</FormControl>
</div>
</Collapse>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(0)}
>
上一步
</Button>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 2 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(3);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否允许获取文件永久直链
<br />
开启后用户可以请求获得能直接访问到文件内容的直链适用于图床应用或自用
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={policy.IsOriginLinkEnable}
onChange={(e) => {
if (
policy.IsPrivate === "true" &&
e.target.value === "true"
) {
ToggleSnackbar(
"top",
"right",
"开启 Token 防盗链后无法使用直链功能",
"warning"
);
return;
}
handleChange("IsOriginLinkEnable")(
e
);
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="允许"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="禁止"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(1)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 3 && (
<form
className={classes.stepContent}
onSubmit={(e) => {
e.preventDefault();
setActiveStep(4);
}}
>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>1</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否限制上传的单文件大小
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.MaxSize === "0"
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
MaxSize: "10485760",
});
} else {
setPolicy({
...policy,
MaxSize: "0",
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="限制"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不限制"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.MaxSize !== "0"}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>2</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
输入限制
</Typography>
<div className={classes.form}>
<SizeInput
value={policy.MaxSize}
onChange={handleChange("MaxSize")}
min={0}
max={9223372036854775807}
label={"单文件大小限制"}
/>
</div>
</div>
</div>
</Collapse>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "3" : "2"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
是否限制上传文件扩展名
</Typography>
<div className={classes.form}>
<FormControl required component="fieldset">
<RadioGroup
required
value={
policy.OptionsSerialized
.file_type === ""
? "false"
: "true"
}
onChange={(e) => {
if (e.target.value === "true") {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type:
"jpg,png,mp4,zip,rar",
},
});
} else {
setPolicy({
...policy,
OptionsSerialized: {
...policy.OptionsSerialized,
file_type: "",
},
});
}
}}
row
>
<FormControlLabel
value={"true"}
control={
<Radio color={"primary"} />
}
label="限制"
/>
<FormControlLabel
value={"false"}
control={
<Radio color={"primary"} />
}
label="不限制"
/>
</RadioGroup>
</FormControl>
</div>
</div>
</div>
<Collapse in={policy.OptionsSerialized.file_type !== ""}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}>
<div className={classes.stepNumber}>
{policy.MaxSize !== "0" ? "4" : "3"}
</div>
</div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
输入允许上传的文件扩展名多个请以半角逗号 ,
隔开
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
扩展名列表
</InputLabel>
<Input
value={
policy.OptionsSerialized
.file_type
}
onChange={handleOptionChange(
"file_type"
)}
/>
</FormControl>
</div>
</div>
</div>
</Collapse>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(2)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
下一步
</Button>
</div>
</form>
)}
{activeStep === 4 && (
<form className={classes.stepContent} onSubmit={submitPolicy}>
<div className={classes.subStepContainer}>
<div className={classes.stepNumberContainer}></div>
<div className={classes.subStepContent}>
<Typography variant={"body2"}>
最后一步为此存储策略命名
</Typography>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
存储策略名
</InputLabel>
<Input
required
value={policy.Name}
onChange={handleChange("Name")}
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.stepFooter}>
<Button
color={"default"}
className={classes.button}
onClick={() => setActiveStep(3)}
>
上一步
</Button>{" "}
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
完成
</Button>
</div>
</form>
)}
{activeStep === 5 && (
<>
<form className={classes.stepContent}>
<Typography>
存储策略已{props.policy ? "保存" : "添加"}
</Typography>
<Typography variant={"body2"} color={"textSecondary"}>
要使用此存储策略请到用户组管理页面为相应用户组绑定此存储策略
</Typography>
</form>
<div className={classes.stepFooter}>
<Button
color={"primary"}
className={classes.button}
onClick={() => history.push("/admin/policy")}
>
返回存储策略列表
</Button>
</div>
</>
)}
<MagicVar
open={magicVar === "file"}
isFile
onClose={() => setMagicVar("")}
/>
<MagicVar
open={magicVar === "path"}
onClose={() => setMagicVar("")}
/>
</div>
);
}

@ -0,0 +1,296 @@
import Button from "@material-ui/core/Button";
import IconButton from "@material-ui/core/IconButton";
import Menu from "@material-ui/core/Menu";
import MenuItem from "@material-ui/core/MenuItem";
import Paper from "@material-ui/core/Paper";
import Select from "@material-ui/core/Select";
import { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import Tooltip from "@material-ui/core/Tooltip";
import { Delete, Edit } from "@material-ui/icons";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory, useLocation } from "react-router";
import { toggleSnackbar } from "../../../actions";
import { policyTypeMap } from "../../../config";
import API from "../../../middleware/Api";
import { sizeToString } from "../../../utils";
import AddPolicy from "../Dialogs/AddPolicy";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
}));
const columns = [
{ id: "#", label: "#", minWidth: 50 },
{ id: "name", label: "名称", minWidth: 170 },
{ id: "type", label: "类型", minWidth: 170 },
{
id: "count",
label: "下属文件数",
minWidth: 50,
align: "right",
},
{
id: "size",
label: "数据量",
minWidth: 100,
align: "right",
},
{
id: "action",
label: "操作",
minWidth: 170,
align: "right",
},
];
function useQuery() {
return new URLSearchParams(useLocation().search);
}
export default function Policy() {
const classes = useStyles();
// const [loading, setLoading] = useState(false);
// const [tab, setTab] = useState(0);
const [policies, setPolicies] = useState([]);
const [statics, setStatics] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [addDialog, setAddDialog] = useState(false);
const [filter, setFilter] = useState("all");
const [anchorEl, setAnchorEl] = React.useState(null);
const [editID, setEditID] = React.useState(0);
const location = useLocation();
const history = useHistory();
const query = useQuery();
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
if (query.get("code") === "0") {
ToggleSnackbar("top", "right", "授权成功", "success");
} else if (query.get("msg") && query.get("msg") !== "") {
ToggleSnackbar(
"top",
"right",
query.get("msg") + ", " + query.get("err"),
"warning"
);
}
}, [location]);
const loadList = () => {
API.post("/admin/policy/list", {
page: page,
page_size: pageSize,
order_by: "id desc",
conditions: filter === "all" ? {} : { type: filter },
})
.then((response) => {
setPolicies(response.data.items);
setStatics(response.data.statics);
setTotal(response.data.total);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, filter]);
const deletePolicy = (id) => {
API.delete("/admin/policy/" + id)
.then(() => {
loadList();
ToggleSnackbar("top", "right", "存储策略已删除", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
const open = Boolean(anchorEl);
return (
<div>
<AddPolicy open={addDialog} onClose={() => setAddDialog(false)} />
<div className={classes.header}>
<Button
color={"primary"}
onClick={() => setAddDialog(true)}
variant={"contained"}
>
添加存储策略
</Button>
<div className={classes.headerRight}>
<Select
style={{
marginRight: 8,
}}
value={filter}
onChange={(e) => setFilter(e.target.value)}
>
<MenuItem value={"all"}>全部</MenuItem>
<MenuItem value={"local"}>本机</MenuItem>
<MenuItem value={"remote"}>从机</MenuItem>
<MenuItem value={"qiniu"}>七牛</MenuItem>
<MenuItem value={"upyun"}>又拍云</MenuItem>
<MenuItem value={"oss"}>阿里云 OSS</MenuItem>
<MenuItem value={"cos"}>腾讯云 COS</MenuItem>
<MenuItem value={"onedrive"}>OneDrive</MenuItem>
<MenuItem value={"s3"}>Amazon S3</MenuItem>
</Select>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
刷新
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
{columns.map((column) => (
<TableCell
key={column.id}
align={column.align}
style={{ minWidth: column.minWidth }}
>
{column.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{policies.map((row) => (
<TableRow hover key={row.ID}>
<TableCell>{row.ID}</TableCell>
<TableCell>{row.Name}</TableCell>
<TableCell>
{policyTypeMap[row.Type] !==
undefined &&
policyTypeMap[row.Type]}
</TableCell>
<TableCell align={"right"}>
{statics[row.ID] !== undefined &&
statics[row.ID][0].toLocaleString()}
</TableCell>
<TableCell align={"right"}>
{statics[row.ID] !== undefined &&
sizeToString(statics[row.ID][1])}
</TableCell>
<TableCell align={"right"}>
<Tooltip title={"删除"}>
<IconButton
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
<Tooltip title={"编辑"}>
<IconButton
onClick={(e) => {
setEditID(row.ID);
handleClick(e);
}}
size={"small"}
>
<Edit />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
<Menu
open={open}
anchorEl={anchorEl}
onClose={handleClose}
keepMounted
>
<MenuItem
onClick={(e) => {
handleClose(e);
history.push("/admin/policy/edit/pro/" + editID);
}}
>
专家模式编辑
</MenuItem>
<MenuItem
onClick={(e) => {
handleClose(e);
history.push("/admin/policy/edit/guide/" + editID);
}}
>
向导模式编辑
</MenuItem>
</Menu>
</div>
);
}

@ -0,0 +1,325 @@
import Button from "@material-ui/core/Button";
import FormControl from "@material-ui/core/FormControl";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import FormHelperText from "@material-ui/core/FormHelperText";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import { makeStyles } from "@material-ui/core/styles";
import Switch from "@material-ui/core/Switch";
import Typography from "@material-ui/core/Typography";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
import AlertDialog from "../Dialogs/Alert";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function Access() {
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
register_enabled: "1",
default_group: "1",
email_active: "0",
login_captcha: "0",
reg_captcha: "0",
forget_captcha: "0",
authn_enabled: "0",
});
const [siteURL, setSiteURL] = useState("");
const [groups, setGroups] = useState([]);
const [httpAlert, setHttpAlert] = useState(false);
const handleChange = (name) => (event) => {
let value = event.target.value;
if (event.target.checked !== undefined) {
value = event.target.checked ? "1" : "0";
}
setOptions({
...options,
[name]: value,
});
};
const handleInputChange = (name) => (event) => {
const value = event.target.value;
setOptions({
...options,
[name]: value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: [...Object.keys(options), "siteURL"],
})
.then((response) => {
setSiteURL(response.data.siteURL);
delete response.data.siteURL;
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
API.get("/admin/groups")
.then((response) => {
setGroups(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", "设置已更改", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<AlertDialog
title={"提示"}
msg={
"Web Authn 需要您的站点启用 HTTPS并确认 参数设置 - 站点信息 - 站点URL 也使用了 HTTPS 后才能开启。"
}
onClose={() => setHttpAlert(false)}
open={httpAlert}
/>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
注册与登录
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.register_enabled === "1"
}
onChange={handleChange(
"register_enabled"
)}
/>
}
label="允许新用户注册"
/>
<FormHelperText id="component-helper-text">
关闭后无法再通过前台注册新的用户
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.email_active === "1"
}
onChange={handleChange(
"email_active"
)}
/>
}
label="邮件激活"
/>
<FormHelperText id="component-helper-text">
开启后新用户注册需要点击邮件中的激活链接才能完成请确认邮件发送设置是否正确否则激活邮件无法送达
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.reg_captcha === "1"
}
onChange={handleChange(
"reg_captcha"
)}
/>
}
label="注册验证码"
/>
<FormHelperText id="component-helper-text">
是否启用注册表单验证码
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.login_captcha === "1"
}
onChange={handleChange(
"login_captcha"
)}
/>
}
label="登录验证码"
/>
<FormHelperText id="component-helper-text">
是否启用登录表单验证码
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.forget_captcha === "1"
}
onChange={handleChange(
"forget_captcha"
)}
/>
}
label="找回密码验证码"
/>
<FormHelperText id="component-helper-text">
是否启用找回密码表单验证码
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.authn_enabled === "1"
}
onChange={(e) => {
if (
!siteURL.startsWith(
"https://"
)
) {
setHttpAlert(true);
return;
}
handleChange("authn_enabled")(
e
);
}}
/>
}
label="Web Authn"
/>
<FormHelperText id="component-helper-text">
是否允许用户使用绑定的外部验证器登录站点必须启动
HTTPS 才能使用
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
默认用户组
</InputLabel>
<Select
value={options.default_group}
onChange={handleInputChange(
"default_group"
)}
required
>
{groups.map((v) => {
if (v.ID === 3) {
return null;
}
return (
<MenuItem
key={v.ID}
value={v.ID.toString()}
>
{v.Name}
</MenuItem>
);
})}
</Select>
<FormHelperText id="component-helper-text">
用户注册后的初始用户组
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
保存
</Button>
</div>
</form>
</div>
);
}

@ -0,0 +1,305 @@
import Button from "@material-ui/core/Button";
import FormControl from "@material-ui/core/FormControl";
import FormHelperText from "@material-ui/core/FormHelperText";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import Link from "@material-ui/core/Link";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import Alert from "@material-ui/lab/Alert";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function Aria2() {
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
aria2_rpcurl: "",
aria2_token: "",
aria2_temp_path: "",
aria2_options: "",
aria2_interval: "0",
aria2_call_timeout: "0",
});
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const reload = () => {
API.get("/admin/reload/aria2")
// eslint-disable-next-line @typescript-eslint/no-empty-function
.then(() => {})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
.then(() => {});
};
const test = () => {
setLoading(true);
API.post("/admin/aria2/test", {
server: options.aria2_rpcurl,
token: options.aria2_token,
})
.then((response) => {
ToggleSnackbar(
"top",
"right",
"连接成功Aria2 版本为:" + response.data,
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", "设置已更改", "success");
reload();
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
Aria2
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Alert severity="info" style={{ marginTop: 8 }}>
<Typography variant="body2">
Cloudreve 的离线下载功能由{" "}
<Link
href={"https://aria2.github.io/"}
target={"_blank"}
>
Aria2
</Link>{" "}
驱动如需使用请在同一设备上以和运行
Cloudreve 相同的用户身份启动 Aria2 并在
Aria2 的配置文件中开启 RPC
服务更多信息及指引请参考文档的{" "}
<Link
href={
"https://docs.cloudreve.org/use/aria2"
}
target={"_blank"}
>
离线下载
</Link>{" "}
章节
</Typography>
</Alert>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
RPC 服务器地址
</InputLabel>
<Input
type={"url"}
value={options.aria2_rpcurl}
onChange={handleChange("aria2_rpcurl")}
/>
<FormHelperText id="component-helper-text">
包含端口的完整 RPC
服务器地址例如http://127.0.0.1:6800/,留空表示不启用
Aria2 服务
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
RPC Secret
</InputLabel>
<Input
value={options.aria2_token}
onChange={handleChange("aria2_token")}
/>
<FormHelperText id="component-helper-text">
RPC 授权令牌 Aria2
配置文件中保持一致未设置请留空
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
临时下载目录
</InputLabel>
<Input
value={options.aria2_temp_path}
onChange={handleChange("aria2_temp_path")}
/>
<FormHelperText id="component-helper-text">
离线下载临时下载目录的
<strong>绝对路径</strong>Cloudreve
进程需要此目录的读执行权限
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
状态刷新间隔 ()
</InputLabel>
<Input
type={"number"}
inputProps={{
step: 1,
min: 1,
}}
required
value={options.aria2_interval}
onChange={handleChange("aria2_interval")}
/>
<FormHelperText id="component-helper-text">
Cloudreve Aria2 请求刷新任务状态的间隔
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
RPC 调用超时 ()
</InputLabel>
<Input
type={"number"}
inputProps={{
step: 1,
min: 1,
}}
required
value={options.aria2_call_timeout}
onChange={handleChange(
"aria2_call_timeout"
)}
/>
<FormHelperText id="component-helper-text">
调用 RPC 服务时最长等待时间
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
全局任务参数
</InputLabel>
<Input
multiline
required
value={options.aria2_options}
onChange={handleChange("aria2_options")}
/>
<FormHelperText id="component-helper-text">
创建下载任务时携带的额外设置参数 JSON
编码后的格式书写您可也可以将这些设置写在
Aria2 配置文件里可用参数请查阅官方文档
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
保存
</Button>
<Button
style={{ marginLeft: 8 }}
disabled={loading}
onClick={() => test()}
variant={"outlined"}
color={"secondary"}
>
测试连接
</Button>
</div>
</form>
</div>
);
}

@ -0,0 +1,408 @@
import React, { useCallback, useEffect, useState } from "react";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import InputLabel from "@material-ui/core/InputLabel";
import FormControl from "@material-ui/core/FormControl";
import FormHelperText from "@material-ui/core/FormHelperText";
import Button from "@material-ui/core/Button";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import Select from "@material-ui/core/Select";
import MenuItem from "@material-ui/core/MenuItem";
import Input from "@material-ui/core/Input";
import Link from "@material-ui/core/Link";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function Captcha() {
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
captcha_type: "normal",
captcha_height: "1",
captcha_width: "1",
captcha_mode: "3",
captcha_CaptchaLen: "",
captcha_ReCaptchaKey: "",
captcha_ReCaptchaSecret: "",
captcha_TCaptcha_CaptchaAppId: "",
captcha_TCaptcha_AppSecretKey: "",
captcha_TCaptcha_SecretId: "",
captcha_TCaptcha_SecretKey: "",
});
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", "设置已更改", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
验证码
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
验证码类型
</InputLabel>
<Select
value={options.captcha_type}
onChange={handleChange("captcha_type")}
required
>
<MenuItem value={"normal"}>普通</MenuItem>
<MenuItem value={"recaptcha"}>
reCAPTCHA V2
</MenuItem>
<MenuItem value={"tcaptcha"}>
腾讯云验证码
</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
验证码类型
</FormHelperText>
</FormControl>
</div>
</div>
</div>
{options.captcha_type === "normal" && (
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
普通验证码
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
宽度
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.captcha_width}
onChange={handleChange("captcha_width")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
高度
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.captcha_height}
onChange={handleChange(
"captcha_height"
)}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
模式
</InputLabel>
<Select
value={options.captcha_mode}
onChange={handleChange("captcha_mode")}
required
>
<MenuItem value={"0"}>数字</MenuItem>
<MenuItem value={"1"}>字母</MenuItem>
<MenuItem value={"2"}>算数</MenuItem>
<MenuItem value={"3"}>
数字+字母
</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
验证码的形式
</FormHelperText>
</FormControl>
</div>
</div>
</div>
)}
{options.captcha_type === "recaptcha" && (
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
reCAPTCHA V2
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
Site KEY
</InputLabel>
<Input
required
value={options.captcha_ReCaptchaKey}
onChange={handleChange(
"captcha_ReCaptchaKey"
)}
/>
<FormHelperText id="component-helper-text">
<Link
href={
"https://www.google.com/recaptcha/admin/create"
}
target={"_blank"}
>
应用管理页面
</Link>{" "}
获取到的的 网站密钥
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
Secret
</InputLabel>
<Input
required
value={
options.captcha_ReCaptchaSecret
}
onChange={handleChange(
"captcha_ReCaptchaSecret"
)}
/>
<FormHelperText id="component-helper-text">
<Link
href={
"https://www.google.com/recaptcha/admin/create"
}
target={"_blank"}
>
应用管理页面
</Link>{" "}
获取到的的 秘钥
</FormHelperText>
</FormControl>
</div>
</div>
</div>
</div>
)}
{options.captcha_type === "tcaptcha" && (
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
腾讯云验证码
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
SecretId
</InputLabel>
<Input
required
value={
options.captcha_TCaptcha_SecretId
}
onChange={handleChange(
"captcha_TCaptcha_SecretId"
)}
/>
<FormHelperText id="component-helper-text">
<Link
href={
"https://console.cloud.tencent.com/capi"
}
target={"_blank"}
>
访问密钥页面
</Link>{" "}
获取到的的 SecretId
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
SecretKey
</InputLabel>
<Input
required
value={
options.captcha_TCaptcha_SecretKey
}
onChange={handleChange(
"captcha_TCaptcha_SecretKey"
)}
/>
<FormHelperText id="component-helper-text">
<Link
href={
"https://console.cloud.tencent.com/capi"
}
target={"_blank"}
>
访问密钥页面
</Link>{" "}
获取到的的 SecretKey
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
APPID
</InputLabel>
<Input
required
value={
options.captcha_TCaptcha_CaptchaAppId
}
onChange={handleChange(
"captcha_TCaptcha_CaptchaAppId"
)}
/>
<FormHelperText id="component-helper-text">
<Link
href={
"https://console.cloud.tencent.com/captcha/graphical"
}
target={"_blank"}
>
图形验证页面
</Link>{" "}
获取到的的 APPID
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
App Secret Key
</InputLabel>
<Input
required
value={
options.captcha_TCaptcha_AppSecretKey
}
onChange={handleChange(
"captcha_TCaptcha_AppSecretKey"
)}
/>
<FormHelperText id="component-helper-text">
<Link
href={
"https://console.cloud.tencent.com/captcha/graphical"
}
target={"_blank"}
>
图形验证页面
</Link>{" "}
获取到的的 App Secret Key
</FormHelperText>
</FormControl>
</div>
</div>
</div>
</div>
)}
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
保存
</Button>
</div>
</form>
</div>
);
}

@ -0,0 +1,270 @@
import Button from "@material-ui/core/Button";
import FormControl from "@material-ui/core/FormControl";
import FormHelperText from "@material-ui/core/FormHelperText";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
import SizeInput from "../Common/SizeInput";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function ImageSetting() {
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
gravatar_server: "",
avatar_path: "",
avatar_size: "",
avatar_size_l: "",
avatar_size_m: "",
avatar_size_s: "",
thumb_width: "",
thumb_height: "",
});
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", "设置已更改", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
头像
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
Gravatar 服务器
</InputLabel>
<Input
type={"url"}
value={options.gravatar_server}
onChange={handleChange("gravatar_server")}
required
/>
<FormHelperText id="component-helper-text">
Gravatar 服务器地址可选择使用国内镜像
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
头像存储路径
</InputLabel>
<Input
value={options.avatar_path}
onChange={handleChange("avatar_path")}
required
/>
<FormHelperText id="component-helper-text">
用户上传自定义头像的存储路径
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<SizeInput
value={options.avatar_size}
onChange={handleChange("avatar_size")}
required
min={0}
max={2147483647}
label={"头像文件大小限制"}
/>
<FormHelperText id="component-helper-text">
用户可上传头像文件的最大大小
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
小头像尺寸
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.avatar_size_s}
onChange={handleChange("avatar_size_s")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
中头像尺寸
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.avatar_size_m}
onChange={handleChange("avatar_size_m")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
大头像尺寸
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.avatar_size_l}
onChange={handleChange("avatar_size_l")}
required
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
缩略图
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
宽度
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.thumb_width}
onChange={handleChange("thumb_width")}
required
/>
</FormControl>
</div>
</div>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
高度
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.thumb_height}
onChange={handleChange("thumb_height")}
required
/>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
保存
</Button>
</div>
</form>
</div>
);
}

@ -0,0 +1,433 @@
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import FormControl from "@material-ui/core/FormControl";
import FormHelperText from "@material-ui/core/FormHelperText";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import { makeStyles } from "@material-ui/core/styles";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Switch from "@material-ui/core/Switch";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
buttonMargin: {
marginLeft: 8,
},
}));
export default function Mail() {
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [test, setTest] = useState(false);
const [tesInput, setTestInput] = useState("");
const [options, setOptions] = useState({
fromName: "",
fromAdress: "",
smtpHost: "",
smtpPort: "",
replyTo: "",
smtpUser: "",
smtpPass: "",
smtpEncryption: "",
mail_keepalive: "30",
mail_activation_template: "",
mail_reset_pwd_template: "",
});
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const handleCheckChange = (name) => (event) => {
let value = event.target.value;
if (event.target.checked !== undefined) {
value = event.target.checked ? "1" : "0";
}
setOptions({
...options,
[name]: value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const sendTestMail = () => {
setLoading(true);
API.post("/admin/mailTest", {
to: tesInput,
})
.then(() => {
ToggleSnackbar("top", "right", "测试邮件已发送", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const reload = () => {
API.get("/admin/reload/email")
// eslint-disable-next-line @typescript-eslint/no-empty-function
.then(() => {})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
// eslint-disable-next-line @typescript-eslint/no-empty-function
.then(() => {});
};
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", "设置已更改", "success");
reload();
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<Dialog
open={test}
onClose={() => setTest(false)}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">发件测试</DialogTitle>
<DialogContent>
<DialogContentText>
<Typography>
发送测试邮件前请先保存已更改的邮件设置
</Typography>
<Typography>
邮件发送结果不会立即反馈如果您长时间未收到测试邮件请检查
Cloudreve 在终端输出的错误日志
</Typography>
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="name"
label="收件人地址"
value={tesInput}
onChange={(e) => setTestInput(e.target.value)}
type="email"
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setTest(false)} color="default">
取消
</Button>
<Button
onClick={() => sendTestMail()}
disabled={loading}
color="primary"
>
发送
</Button>
</DialogActions>
</Dialog>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
发信
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
发件人名
</InputLabel>
<Input
value={options.fromName}
onChange={handleChange("fromName")}
required
/>
<FormHelperText id="component-helper-text">
邮件中展示的发件人姓名
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
发件人邮箱
</InputLabel>
<Input
type={"email"}
required
value={options.fromAdress}
onChange={handleChange("fromAdress")}
/>
<FormHelperText id="component-helper-text">
发件邮箱的地址
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
SMTP 服务器
</InputLabel>
<Input
value={options.smtpHost}
onChange={handleChange("smtpHost")}
required
/>
<FormHelperText id="component-helper-text">
发件服务器地址不含端口号
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
SMTP 端口
</InputLabel>
<Input
inputProps={{ min: 1, step: 1 }}
type={"number"}
value={options.smtpPort}
onChange={handleChange("smtpPort")}
required
/>
<FormHelperText id="component-helper-text">
发件服务器地址端口号
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
SMTP 用户名
</InputLabel>
<Input
value={options.smtpUser}
onChange={handleChange("smtpUser")}
required
/>
<FormHelperText id="component-helper-text">
发信邮箱用户名一般与邮箱地址相同
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
SMTP 密码
</InputLabel>
<Input
type={"password"}
value={options.smtpPass}
onChange={handleChange("smtpPass")}
required
/>
<FormHelperText id="component-helper-text">
发信邮箱密码
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
回信邮箱
</InputLabel>
<Input
value={options.replyTo}
onChange={handleChange("replyTo")}
required
/>
<FormHelperText id="component-helper-text">
用户回复系统发送的邮件时用于接收回信的邮箱
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.smtpEncryption === "1"
}
onChange={handleCheckChange(
"smtpEncryption"
)}
/>
}
label="强制使用 SSL 连接"
/>
<FormHelperText id="component-helper-text">
是否强制使用 SSL
加密连接如果无法发送邮件可关闭此项
Cloudreve 会尝试使用 STARTTLS
并决定是否使用加密连接
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
SMTP 连接有效期 ()
</InputLabel>
<Input
inputProps={{ min: 1, step: 1 }}
type={"number"}
value={options.mail_keepalive}
onChange={handleChange("mail_keepalive")}
required
/>
<FormHelperText id="component-helper-text">
有效期内建立的 SMTP
连接会被新邮件发送请求复用
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
邮件模板
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
新用户激活
</InputLabel>
<Input
value={options.mail_activation_template}
onChange={handleChange(
"mail_activation_template"
)}
multiline
rowsMax="10"
required
/>
<FormHelperText id="component-helper-text">
新用户注册后激活邮件的模板
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
重置密码
</InputLabel>
<Input
value={options.mail_reset_pwd_template}
onChange={handleChange(
"mail_reset_pwd_template"
)}
multiline
rowsMax="10"
required
/>
<FormHelperText id="component-helper-text">
密码重置邮件模板
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
保存
</Button>
{" "}
<Button
className={classes.buttonMargin}
variant={"outlined"}
color={"primary"}
onClick={() => setTest(true)}
>
发送测试邮件
</Button>
</div>
</form>
</div>
);
}

@ -0,0 +1,323 @@
import Button from "@material-ui/core/Button";
import FormControl from "@material-ui/core/FormControl";
import FormHelperText from "@material-ui/core/FormHelperText";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function SiteInformation() {
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
siteURL: "",
siteName: "",
siteTitle: "",
siteDes: "",
siteICPId: "",
siteScript: "",
pwa_small_icon: "",
pwa_medium_icon: "",
pwa_large_icon: "",
pwa_display: "",
pwa_theme_color: "",
pwa_background_color: "",
});
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", "设置已更改", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
基本信息
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
主标题
</InputLabel>
<Input
value={options.siteName}
onChange={handleChange("siteName")}
required
/>
<FormHelperText id="component-helper-text">
站点的主标题
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
副标题
</InputLabel>
<Input
value={options.siteTitle}
onChange={handleChange("siteTitle")}
/>
<FormHelperText id="component-helper-text">
站点的副标题
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
站点描述
</InputLabel>
<Input
value={options.siteDes}
onChange={handleChange("siteDes")}
/>
<FormHelperText id="component-helper-text">
站点描述信息可能会在分享页面摘要内展示
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
站点URL
</InputLabel>
<Input
type={"url"}
value={options.siteURL}
onChange={handleChange("siteURL")}
required
/>
<FormHelperText id="component-helper-text">
非常重要请确保与实际情况一致使用云存储策略支付平台时请填入可以被外网访问的地址
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
网站备案号
</InputLabel>
<Input
value={options.siteICPId}
onChange={handleChange("siteICPId")}
/>
<FormHelperText id="component-helper-text">
工信部网站ICP备案号
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
页脚代码
</InputLabel>
<Input
multiline
value={options.siteScript}
onChange={handleChange("siteScript")}
/>
<FormHelperText id="component-helper-text">
在页面底部插入的自定义HTML代码
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
渐进式应用 (PWA)
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
小图标
</InputLabel>
<Input
value={options.pwa_small_icon}
onChange={handleChange("pwa_small_icon")}
/>
<FormHelperText id="component-helper-text">
扩展名为 ico 的小图标地址
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
中图标
</InputLabel>
<Input
value={options.pwa_medium_icon}
onChange={handleChange("pwa_medium_icon")}
/>
<FormHelperText id="component-helper-text">
192x192 的中等图标地址png 格式
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
大图标
</InputLabel>
<Input
value={options.pwa_large_icon}
onChange={handleChange("pwa_large_icon")}
/>
<FormHelperText id="component-helper-text">
512x512 的大图标地址png 格式
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
展示模式
</InputLabel>
<Select
value={options.pwa_display}
onChange={handleChange("pwa_display")}
>
<MenuItem value={"fullscreen"}>
fullscreen
</MenuItem>
<MenuItem value={"standalone"}>
standalone
</MenuItem>
<MenuItem value={"minimal-ui"}>
minimal-ui
</MenuItem>
<MenuItem value={"browser"}>
browser
</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
PWA 应用添加后的展示模式
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
主题色
</InputLabel>
<Input
value={options.pwa_theme_color}
onChange={handleChange("pwa_theme_color")}
/>
<FormHelperText id="component-helper-text">
CSS 色值影响 PWA
启动画面上状态栏内容页中状态栏地址栏的颜色
</FormHelperText>
</FormControl>
</div>
</div>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
背景色
</InputLabel>
<Input
value={options.pwa_background_color}
onChange={handleChange(
"pwa_background_color"
)}
/>
<FormHelperText id="component-helper-text">
CSS 色值
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
保存
</Button>
</div>
</form>
</div>
);
}

@ -0,0 +1,446 @@
import Button from "@material-ui/core/Button";
import FormControl from "@material-ui/core/FormControl";
import FormHelperText from "@material-ui/core/FormHelperText";
import IconButton from "@material-ui/core/IconButton";
import InputLabel from "@material-ui/core/InputLabel";
import Link from "@material-ui/core/Link";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TextField from "@material-ui/core/TextField";
import Typography from "@material-ui/core/Typography";
import { Delete } from "@material-ui/icons";
import Alert from "@material-ui/lab/Alert";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
import CreateTheme from "../Dialogs/CreateTheme";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 500,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
colorContainer: {
display: "flex",
},
colorDot: {
width: 20,
height: 20,
borderRadius: "50%",
marginLeft: 6,
},
}));
export default function Theme() {
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [theme, setTheme] = useState({});
const [options, setOptions] = useState({
themes: "{}",
defaultTheme: "",
home_view_method: "icon",
share_view_method: "list",
});
const [themeConfig, setThemeConfig] = useState({});
const [themeConfigError, setThemeConfigError] = useState({});
const [create, setCreate] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const deleteTheme = (color) => {
if (color === options.defaultTheme) {
ToggleSnackbar("top", "right", "不能删除默认配色", "warning");
return;
}
if (Object.keys(theme).length <= 1) {
ToggleSnackbar("top", "right", "请至少保留一个配色方案", "warning");
return;
}
const themeCopy = { ...theme };
delete themeCopy[color];
const resStr = JSON.stringify(themeCopy);
setOptions({
...options,
themes: resStr,
});
};
const addTheme = (newTheme) => {
setCreate(false);
if (theme[newTheme.palette.primary.main] !== undefined) {
ToggleSnackbar(
"top",
"right",
"主色调不能与已有配色重复",
"warning"
);
return;
}
const res = {
...theme,
[newTheme.palette.primary.main]: newTheme,
};
const resStr = JSON.stringify(res);
setOptions({
...options,
themes: resStr,
});
};
useEffect(() => {
const res = JSON.parse(options.themes);
const themeString = {};
Object.keys(res).forEach((k) => {
themeString[k] = JSON.stringify(res[k]);
});
setTheme(res);
setThemeConfig(themeString);
}, [options.themes]);
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", "设置已更改", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
主题配色
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>关键色</TableCell>
<TableCell>色彩配置</TableCell>
<TableCell>操作</TableCell>
</TableRow>
</TableHead>
<TableBody>
{Object.keys(theme).map((k) => (
<TableRow key={k}>
<TableCell
component="th"
scope="row"
>
<div
className={
classes.colorContainer
}
>
<div
style={{
backgroundColor:
theme[k].palette
.primary
.main,
}}
className={
classes.colorDot
}
/>
<div
style={{
backgroundColor:
theme[k].palette
.secondary
.main,
}}
className={
classes.colorDot
}
/>
</div>
</TableCell>
<TableCell>
<TextField
error={themeConfigError[k]}
helperText={
themeConfigError[k] &&
"格式不正确"
}
fullWidth
multiline
onChange={(e) => {
setThemeConfig({
...themeConfig,
[k]: e.target.value,
});
}}
onBlur={(e) => {
try {
const res = JSON.parse(
e.target.value
);
if (
!(
"palette" in
res
) ||
!(
"primary" in
res.palette
) ||
!(
"main" in
res.palette
.primary
) ||
!(
"secondary" in
res.palette
) ||
!(
"main" in
res.palette
.secondary
)
) {
throw e;
}
setTheme({
...theme,
[k]: res,
});
} catch (e) {
setThemeConfigError(
{
...themeConfigError,
[k]: true,
}
);
return;
}
setThemeConfigError({
...themeConfigError,
[k]: false,
});
}}
value={themeConfig[k]}
/>
</TableCell>
<TableCell>
<IconButton
onClick={() =>
deleteTheme(k)
}
>
<Delete />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div>
<Button
variant="outlined"
color="primary"
style={{ marginTop: 8 }}
onClick={() => setCreate(true)}
>
新建配色方案
</Button>
</div>
<Alert severity="info" style={{ marginTop: 8 }}>
<Typography variant="body2">
完整的配置项可在{" "}
<Link
href={
"https://material-ui.com/zh/customization/default-theme/"
}
target={"_blank"}
>
默认主题 - Material-UI
</Link>{" "}
查阅
</Typography>
</Alert>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
默认配色
</InputLabel>
<Select
value={options.defaultTheme}
onChange={handleChange("defaultTheme")}
>
{Object.keys(theme).map((k) => (
<MenuItem key={k} value={k}>
<div
className={
classes.colorContainer
}
>
<div
style={{
backgroundColor:
theme[k].palette
.primary.main,
}}
className={classes.colorDot}
/>
<div
style={{
backgroundColor:
theme[k].palette
.secondary.main,
}}
className={classes.colorDot}
/>
</div>
</MenuItem>
))}
</Select>
<FormHelperText id="component-helper-text">
用户未指定偏好配色时站点默认使用的配色方案
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
界面
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
个人文件列表默认样式
</InputLabel>
<Select
value={options.home_view_method}
onChange={handleChange("home_view_method")}
required
>
<MenuItem value={"icon"}>大图标</MenuItem>
<MenuItem value={"smallIcon"}>
小图标
</MenuItem>
<MenuItem value={"list"}>列表</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
用户未指定偏好样式时个人文件页面列表默认样式
</FormHelperText>
</FormControl>
</div>
</div>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
目录分享页列表默认样式
</InputLabel>
<Select
value={options.share_view_method}
onChange={handleChange("share_view_method")}
required
>
<MenuItem value={"icon"}>大图标</MenuItem>
<MenuItem value={"smallIcon"}>
小图标
</MenuItem>
<MenuItem value={"list"}>列表</MenuItem>
</Select>
<FormHelperText id="component-helper-text">
用户未指定偏好样式时目录分享页面的默认样式
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
保存
</Button>
</div>
</form>
<CreateTheme
onSubmit={addTheme}
open={create}
onClose={() => setCreate(false)}
/>
</div>
);
}

@ -0,0 +1,504 @@
import Button from "@material-ui/core/Button";
import FormControl from "@material-ui/core/FormControl";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import FormHelperText from "@material-ui/core/FormHelperText";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import { makeStyles } from "@material-ui/core/styles";
import Switch from "@material-ui/core/Switch";
import Typography from "@material-ui/core/Typography";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
import SizeInput from "../Common/SizeInput";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function UploadDownload() {
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [options, setOptions] = useState({
max_worker_num: "1",
max_parallel_transfer: "1",
temp_path: "",
maxEditSize: "0",
onedrive_chunk_retries: "0",
archive_timeout: "0",
download_timeout: "0",
preview_timeout: "0",
doc_preview_timeout: "0",
upload_credential_timeout: "0",
upload_session_timeout: "0",
slave_api_timeout: "0",
onedrive_monitor_timeout: "0",
share_download_session_timeout: "0",
onedrive_callback_check: "0",
reset_after_upload_failed: "0",
onedrive_source_timeout: "0",
});
const handleCheckChange = (name) => (event) => {
const value = event.target.checked ? "1" : "0";
setOptions({
...options,
[name]: value,
});
};
const handleChange = (name) => (event) => {
setOptions({
...options,
[name]: event.target.value,
});
};
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.post("/admin/setting", {
keys: Object.keys(options),
})
.then((response) => {
setOptions(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
// eslint-disable-next-line
}, []);
const submit = (e) => {
e.preventDefault();
setLoading(true);
const option = [];
Object.keys(options).forEach((k) => {
option.push({
key: k,
value: options[k],
});
});
API.patch("/admin/setting", {
options: option,
})
.then(() => {
ToggleSnackbar("top", "right", "设置已更改", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
存储与传输
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
Worker 数量
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.max_worker_num}
onChange={handleChange("max_worker_num")}
required
/>
<FormHelperText id="component-helper-text">
任务队列最多并行执行的任务数保存后需要重启
Cloudreve 生效
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
中转并行传输
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.max_parallel_transfer}
onChange={handleChange(
"max_parallel_transfer"
)}
required
/>
<FormHelperText id="component-helper-text">
任务队列中转任务传输时最大并行协程数
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
临时目录
</InputLabel>
<Input
value={options.temp_path}
onChange={handleChange("temp_path")}
required
/>
<FormHelperText id="component-helper-text">
用于存放打包下载解压缩压缩等任务产生的临时文件的目录路径
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<SizeInput
value={options.maxEditSize}
onChange={handleChange("maxEditSize")}
required
min={0}
max={2147483647}
label={"文本文件在线编辑大小"}
/>
<FormHelperText id="component-helper-text">
文本文件可在线编辑的最大大小超出此大小的文件无法在线编辑
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
OneDrive 分片错误重试
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 0,
step: 1,
}}
value={options.onedrive_chunk_retries}
onChange={handleChange(
"onedrive_chunk_retries"
)}
required
/>
<FormHelperText id="component-helper-text">
OneDrive
存储策略分片上传失败后重试的最大次数只适用于服务端上传或中转
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<FormControlLabel
control={
<Switch
checked={
options.reset_after_upload_failed ===
"1"
}
onChange={handleCheckChange(
"reset_after_upload_failed"
)}
/>
}
label="上传校验失败时强制重置连接"
/>
<FormHelperText id="component-helper-text">
开启后如果本次策略头像等数据上传校验失败服务器会强制重置连接
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
有效期 ()
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
打包下载
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.archive_timeout}
onChange={handleChange("archive_timeout")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
下载会话
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.download_timeout}
onChange={handleChange("download_timeout")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
预览链接
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.preview_timeout}
onChange={handleChange("preview_timeout")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
Office 文档预览连接
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.doc_preview_timeout}
onChange={handleChange(
"doc_preview_timeout"
)}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
上传凭证
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.upload_credential_timeout}
onChange={handleChange(
"upload_credential_timeout"
)}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
上传会话
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.upload_session_timeout}
onChange={handleChange(
"upload_session_timeout"
)}
required
/>
<FormHelperText id="component-helper-text">
超出后不再处理此上传的回调请求
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
从机API请求
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.slave_api_timeout}
onChange={handleChange("slave_api_timeout")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
分享下载会话
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={
options.share_download_session_timeout
}
onChange={handleChange(
"share_download_session_timeout"
)}
required
/>
<FormHelperText id="component-helper-text">
设定时间内重复下载分享文件不会被记入总下载次数
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
OneDrive 客户端上传监控间隔
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.onedrive_monitor_timeout}
onChange={handleChange(
"onedrive_monitor_timeout"
)}
required
/>
<FormHelperText id="component-helper-text">
每间隔所设定时间Cloudreve 会向 OneDrive
请求检查客户端上传情况已确保客户端上传可控
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
OneDrive 回调等待
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
step: 1,
}}
value={options.onedrive_callback_check}
onChange={handleChange(
"onedrive_callback_check"
)}
required
/>
<FormHelperText id="component-helper-text">
OneDrive
客户端上传完成后等待回调的最大时间如果超出会被认为上传失败
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl>
<InputLabel htmlFor="component-helper">
OneDrive 下载请求缓存
</InputLabel>
<Input
type={"number"}
inputProps={{
min: 1,
max: 3659,
step: 1,
}}
value={options.onedrive_source_timeout}
onChange={handleChange(
"onedrive_source_timeout"
)}
required
/>
<FormHelperText id="component-helper-text">
OneDrive 获取文件下载 URL
后可将结果缓存减轻热门文件下载API请求频率
</FormHelperText>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
保存
</Button>
</div>
</form>
</div>
);
}

@ -0,0 +1,487 @@
import { lighten } from "@material-ui/core";
import Badge from "@material-ui/core/Badge";
import Button from "@material-ui/core/Button";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import Link from "@material-ui/core/Link";
import Paper from "@material-ui/core/Paper";
import { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import Toolbar from "@material-ui/core/Toolbar";
import Tooltip from "@material-ui/core/Tooltip";
import Typography from "@material-ui/core/Typography";
import { Delete, FilterList } from "@material-ui/icons";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
import ShareFilter from "../Dialogs/ShareFilter";
import { formatLocalTime } from "../../../utils/datetime";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function Share() {
const classes = useStyles();
const [shares, setShares] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [users, setUsers] = useState({});
const [ids, setIds] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/share/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setUsers(response.data.users);
setIds(response.data.ids);
setShares(response.data.items);
setTotal(response.data.total);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/share/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", "分享已删除", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/share/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", "分享已删除", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = shares.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<ShareFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<div className={classes.headerRight}>
<Tooltip title="过滤">
<IconButton
style={{ marginRight: 8 }}
onClick={() => setFilterDialog(true)}
>
<Badge
color="secondary"
variant="dot"
invisible={
Object.keys(search).length === 0 &&
Object.keys(filter).length === 0
}
>
<FilterList />
</Badge>
</IconButton>
</Tooltip>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
刷新
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
已选择 {selected.length} 个对象
</Typography>
<Tooltip title="删除">
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < shares.length
}
checked={
shares.length > 0 &&
selected.length === shares.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 10 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 200 }}>
<TableSortLabel
active={orderBy[0] === "source_name"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"source_name",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
对象名
{orderBy[0] === "source_name" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 70 }}>
类型
</TableCell>
<TableCell
style={{ minWidth: 100 }}
align={"right"}
>
<TableSortLabel
active={orderBy[0] === "views"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"views",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
浏览
{orderBy[0] === "views" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell
style={{ minWidth: 100 }}
align={"right"}
>
<TableSortLabel
active={orderBy[0] === "downloads"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"downloads",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
下载
{orderBy[0] === "downloads" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 120 }}>
自动过期
</TableCell>
<TableCell style={{ minWidth: 120 }}>
分享者
</TableCell>
<TableCell style={{ minWidth: 150 }}>
分享于
</TableCell>
<TableCell style={{ minWidth: 100 }}>
操作
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{shares.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell
style={{ wordBreak: "break-all" }}
>
<Link
target={"_blank"}
color="inherit"
href={
"/s/" +
ids[row.ID] +
(row.Password === ""
? ""
: "?password=" +
row.Password)
}
>
{row.SourceName}
</Link>
</TableCell>
<TableCell>
{row.Password === "" ? "公开" : "私密"}
</TableCell>
<TableCell align={"right"}>
{row.Views}
</TableCell>
<TableCell align={"right"}>
{row.Downloads}
</TableCell>
<TableCell>
{row.RemainDownloads > -1 &&
row.RemainDownloads + " 次下载后"}
{row.RemainDownloads === -1 && "无"}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" + row.UserID
}
>
{users[row.UserID]
? users[row.UserID].Nick
: "未知"}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(
row.CreatedAt,
"YYYY-MM-DD H:mm:ss"
)}
</TableCell>
<TableCell>
<Tooltip title={"删除"}>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

@ -0,0 +1,397 @@
import { lighten } from "@material-ui/core";
import Button from "@material-ui/core/Button";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import Link from "@material-ui/core/Link";
import Paper from "@material-ui/core/Paper";
import { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import Toolbar from "@material-ui/core/Toolbar";
import Tooltip from "@material-ui/core/Tooltip";
import Typography from "@material-ui/core/Typography";
import { Delete } from "@material-ui/icons";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
import { sizeToString } from "../../../utils";
import ShareFilter from "../Dialogs/ShareFilter";
import { formatLocalTime } from "../../../utils/datetime";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function Download() {
const classes = useStyles();
const [downloads, setDownloads] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [users, setUsers] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/download/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setUsers(response.data.users);
setDownloads(response.data.items);
setTotal(response.data.total);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/download/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", "任务已删除", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/download/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", "任务已删除", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = downloads.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<ShareFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
刷新
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
已选择 {selected.length} 个对象
</Typography>
<Tooltip title="删除">
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < downloads.length
}
checked={
downloads.length > 0 &&
selected.length === downloads.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 10 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 130 }}>
源地址
</TableCell>
<TableCell style={{ minWidth: 90 }}>
状态
</TableCell>
<TableCell
style={{ minWidth: 150 }}
align={"right"}
>
<TableSortLabel
active={orderBy[0] === "total_size"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"total_size",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
大小
{orderBy[0] === "total_size" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 100 }}>
创建者
</TableCell>
<TableCell style={{ minWidth: 150 }}>
创建于
</TableCell>
<TableCell style={{ minWidth: 80 }}>
操作
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{downloads.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell
style={{ wordBreak: "break-all" }}
>
{row.Source}
</TableCell>
<TableCell>
{row.Status === 0 && "就绪"}
{row.Status === 1 && "下载中"}
{row.Status === 2 && "暂停中"}
{row.Status === 3 && "出错"}
{row.Status === 4 && "完成"}
{row.Status === 5 && "取消/停止"}
{row.Status === 6 && "未知"}
</TableCell>
<TableCell align={"right"}>
{sizeToString(row.TotalSize)}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" + row.UserID
}
>
{users[row.UserID]
? users[row.UserID].Nick
: "未知"}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(
row.CreatedAt,
"YYYY-MM-DD H:mm:ss"
)}
</TableCell>
<TableCell>
<Tooltip title={"删除"}>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

@ -0,0 +1,385 @@
import { lighten } from "@material-ui/core";
import Button from "@material-ui/core/Button";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import Link from "@material-ui/core/Link";
import Paper from "@material-ui/core/Paper";
import { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import Toolbar from "@material-ui/core/Toolbar";
import Tooltip from "@material-ui/core/Tooltip";
import Typography from "@material-ui/core/Typography";
import { Delete } from "@material-ui/icons";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import { getTaskProgress, getTaskStatus, getTaskType } from "../../../config";
import API from "../../../middleware/Api";
import ShareFilter from "../Dialogs/ShareFilter";
import { formatLocalTime } from "../../../utils/datetime";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function Task() {
const classes = useStyles();
const [tasks, setTasks] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [users, setUsers] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/task/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setUsers(response.data.users);
setTasks(response.data.items);
setTotal(response.data.total);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/task/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", "任务已删除", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/task/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", "任务已删除", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = tasks.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const getError = (error) => {
if (error === "") {
return "-";
}
try {
const res = JSON.parse(error);
return res.msg;
} catch (e) {
return "未知";
}
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<ShareFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<div className={classes.headerRight}>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
刷新
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
已选择 {selected.length} 个对象
</Typography>
<Tooltip title="删除">
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < tasks.length
}
checked={
tasks.length > 0 &&
selected.length === tasks.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 10 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 130 }}>
类型
</TableCell>
<TableCell style={{ minWidth: 90 }}>
状态
</TableCell>
<TableCell style={{ minWidth: 90 }}>
最后进度
</TableCell>
<TableCell style={{ minWidth: 150 }}>
错误信息
</TableCell>
<TableCell style={{ minWidth: 100 }}>
创建者
</TableCell>
<TableCell style={{ minWidth: 150 }}>
创建于
</TableCell>
<TableCell style={{ minWidth: 80 }}>
操作
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{tasks.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell
style={{ wordBreak: "break-all" }}
>
{getTaskType(row.Type)}
</TableCell>
<TableCell>
{getTaskStatus(row.Status)}
</TableCell>
<TableCell>
{getTaskProgress(
row.Type,
row.Progress
)}
</TableCell>
<TableCell className={classes.noWrap}>
{getError(row.Error)}
</TableCell>
<TableCell>
<Link
href={
"/admin/user/edit/" + row.UserID
}
>
{users[row.UserID]
? users[row.UserID].Nick
: "未知"}
</Link>
</TableCell>
<TableCell>
{formatLocalTime(
row.CreatedAt,
"YYYY-MM-DD H:mm:ss"
)}
</TableCell>
<TableCell>
<Tooltip title={"删除"}>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

@ -0,0 +1,36 @@
import React, { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router";
import API from "../../../middleware/Api";
import { useDispatch } from "react-redux";
import { toggleSnackbar } from "../../../actions";
import UserForm from "./UserForm";
export default function EditUserPreload() {
const [user, setUser] = useState({});
const { id } = useParams();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
setUser({});
API.get("/admin/user/" + id)
.then((response) => {
// 整型转换
["Status", "GroupID"].forEach((v) => {
response.data[v] = response.data[v].toString();
});
setUser(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, [id]);
return <div>{user.ID !== undefined && <UserForm user={user} />}</div>;
}

@ -0,0 +1,535 @@
import { lighten } from "@material-ui/core";
import Badge from "@material-ui/core/Badge";
import Button from "@material-ui/core/Button";
import Checkbox from "@material-ui/core/Checkbox";
import IconButton from "@material-ui/core/IconButton";
import Link from "@material-ui/core/Link";
import Paper from "@material-ui/core/Paper";
import { makeStyles, useTheme } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TablePagination from "@material-ui/core/TablePagination";
import TableRow from "@material-ui/core/TableRow";
import TableSortLabel from "@material-ui/core/TableSortLabel";
import Toolbar from "@material-ui/core/Toolbar";
import Tooltip from "@material-ui/core/Tooltip";
import Typography from "@material-ui/core/Typography";
import { Block, Delete, Edit, FilterList } from "@material-ui/icons";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
import { sizeToString } from "../../../utils";
import UserFilter from "../Dialogs/UserFilter";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
content: {
padding: theme.spacing(2),
},
container: {
overflowX: "auto",
},
tableContainer: {
marginTop: 16,
},
header: {
display: "flex",
justifyContent: "space-between",
},
headerRight: {},
highlight:
theme.palette.type === "light"
? {
color: theme.palette.secondary.main,
backgroundColor: lighten(theme.palette.secondary.light, 0.85),
}
: {
color: theme.palette.text.primary,
backgroundColor: theme.palette.secondary.dark,
},
visuallyHidden: {
border: 0,
clip: "rect(0 0 0 0)",
height: 1,
margin: -1,
overflow: "hidden",
padding: 0,
position: "absolute",
top: 20,
width: 1,
},
}));
export default function Group() {
const classes = useStyles();
const [users, setUsers] = useState([]);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [total, setTotal] = useState(0);
const [filter, setFilter] = useState({});
const [search, setSearch] = useState({});
const [orderBy, setOrderBy] = useState(["id", "desc"]);
const [filterDialog, setFilterDialog] = useState(false);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const history = useHistory();
const theme = useTheme();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const loadList = () => {
API.post("/admin/user/list", {
page: page,
page_size: pageSize,
order_by: orderBy.join(" "),
conditions: filter,
searches: search,
})
.then((response) => {
setUsers(response.data.items);
setTotal(response.data.total);
setSelected([]);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
};
useEffect(() => {
loadList();
}, [page, pageSize, orderBy, filter, search]);
const deletePolicy = (id) => {
setLoading(true);
API.post("/admin/user/delete", { id: [id] })
.then(() => {
loadList();
ToggleSnackbar("top", "right", "用户已删除", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const deleteBatch = () => {
setLoading(true);
API.post("/admin/user/delete", { id: selected })
.then(() => {
loadList();
ToggleSnackbar("top", "right", "用户已删除", "success");
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const block = (id) => {
setLoading(true);
API.patch("/admin/user/ban/" + id)
.then((response) => {
setUsers(
users.map((v) => {
if (v.ID === id) {
const newUser = { ...v, Status: response.data };
return newUser;
}
return v;
})
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
const handleSelectAllClick = (event) => {
if (event.target.checked) {
const newSelecteds = users.map((n) => n.ID);
setSelected(newSelecteds);
return;
}
setSelected([]);
};
const handleClick = (event, name) => {
const selectedIndex = selected.indexOf(name);
let newSelected = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selected, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selected.slice(1));
} else if (selectedIndex === selected.length - 1) {
newSelected = newSelected.concat(selected.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selected.slice(0, selectedIndex),
selected.slice(selectedIndex + 1)
);
}
setSelected(newSelected);
};
const isSelected = (id) => selected.indexOf(id) !== -1;
return (
<div>
<UserFilter
filter={filter}
open={filterDialog}
onClose={() => setFilterDialog(false)}
setSearch={setSearch}
setFilter={setFilter}
/>
<div className={classes.header}>
<Button
style={{ alignSelf: "center" }}
color={"primary"}
onClick={() => history.push("/admin/user/add")}
variant={"contained"}
>
新建用户
</Button>
<div className={classes.headerRight}>
<Tooltip title="过滤">
<IconButton
style={{ marginRight: 8 }}
onClick={() => setFilterDialog(true)}
>
<Badge
color="secondary"
variant="dot"
invisible={
Object.keys(search).length === 0 &&
Object.keys(filter).length === 0
}
>
<FilterList />
</Badge>
</IconButton>
</Tooltip>
<Button
color={"primary"}
onClick={() => loadList()}
variant={"outlined"}
>
刷新
</Button>
</div>
</div>
<Paper square className={classes.tableContainer}>
{selected.length > 0 && (
<Toolbar className={classes.highlight}>
<Typography
style={{ flex: "1 1 100%" }}
color="inherit"
variant="subtitle1"
>
已选择 {selected.length} 个对象
</Typography>
<Tooltip title="删除">
<IconButton
onClick={deleteBatch}
disabled={loading}
aria-label="delete"
>
<Delete />
</IconButton>
</Tooltip>
</Toolbar>
)}
<TableContainer className={classes.container}>
<Table aria-label="sticky table" size={"small"}>
<TableHead>
<TableRow style={{ height: 52 }}>
<TableCell padding="checkbox">
<Checkbox
indeterminate={
selected.length > 0 &&
selected.length < users.length
}
checked={
users.length > 0 &&
selected.length === users.length
}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all desserts",
}}
/>
</TableCell>
<TableCell style={{ minWidth: 59 }}>
<TableSortLabel
active={orderBy[0] === "id"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"id",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
#
{orderBy[0] === "id" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 120 }}>
<TableSortLabel
active={orderBy[0] === "nick"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"nick",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
昵称
{orderBy[0] === "nick" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 170 }}>
<TableSortLabel
active={orderBy[0] === "email"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"email",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
Email
{orderBy[0] === "email" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 70 }}>
用户组
</TableCell>
<TableCell style={{ minWidth: 50 }}>
状态
</TableCell>
<TableCell
align={"right"}
style={{ minWidth: 80 }}
>
<TableSortLabel
active={orderBy[0] === "storage"}
direction={orderBy[1]}
onClick={() =>
setOrderBy([
"storage",
orderBy[1] === "asc"
? "desc"
: "asc",
])
}
>
已用空间
{orderBy[0] === "storage" ? (
<span
className={
classes.visuallyHidden
}
>
{orderBy[1] === "desc"
? "sorted descending"
: "sorted ascending"}
</span>
) : null}
</TableSortLabel>
</TableCell>
<TableCell style={{ minWidth: 100 }}>
操作
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{users.map((row) => (
<TableRow
hover
key={row.ID}
role="checkbox"
selected={isSelected(row.ID)}
>
<TableCell padding="checkbox">
<Checkbox
onClick={(event) =>
handleClick(event, row.ID)
}
checked={isSelected(row.ID)}
/>
</TableCell>
<TableCell>{row.ID}</TableCell>
<TableCell>{row.Nick}</TableCell>
<TableCell>{row.Email}</TableCell>
<TableCell>
<Link
href={
"/admin/group/edit/" +
row.Group.ID
}
>
{row.Group.Name}
</Link>
</TableCell>
<TableCell>
{row.Status === 0 && (
<Typography
style={{
color:
theme.palette.success
.main,
}}
variant={"body2"}
>
正常
</Typography>
)}
{row.Status === 1 && (
<Typography
color={"textSecondary"}
variant={"body2"}
>
未激活
</Typography>
)}
{row.Status === 2 && (
<Typography
color={"error"}
variant={"body2"}
>
被封禁
</Typography>
)}
{row.Status === 3 && (
<Typography
color={"error"}
variant={"body2"}
>
超额封禁
</Typography>
)}
</TableCell>
<TableCell align={"right"}>
{sizeToString(row.Storage)}
</TableCell>
<TableCell>
<Tooltip title={"编辑"}>
<IconButton
onClick={() =>
history.push(
"/admin/user/edit/" +
row.ID
)
}
size={"small"}
>
<Edit />
</IconButton>
</Tooltip>
<Tooltip title={"封禁/解封"}>
<IconButton
disabled={loading}
onClick={() => block(row.ID)}
size={"small"}
>
<Block />
</IconButton>
</Tooltip>
<Tooltip title={"删除"}>
<IconButton
disabled={loading}
onClick={() =>
deletePolicy(row.ID)
}
size={"small"}
>
<Delete />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 100]}
component="div"
count={total}
rowsPerPage={pageSize}
page={page - 1}
onChangePage={(e, p) => setPage(p + 1)}
onChangeRowsPerPage={(e) => {
setPageSize(e.target.value);
setPage(1);
}}
/>
</Paper>
</div>
);
}

@ -0,0 +1,226 @@
import Button from "@material-ui/core/Button";
import FormControl from "@material-ui/core/FormControl";
import FormHelperText from "@material-ui/core/FormHelperText";
import Input from "@material-ui/core/Input";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import Select from "@material-ui/core/Select";
import { makeStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import React, { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { useHistory } from "react-router";
import { toggleSnackbar } from "../../../actions";
import API from "../../../middleware/Api";
const useStyles = makeStyles((theme) => ({
root: {
[theme.breakpoints.up("md")]: {
marginLeft: 100,
},
marginBottom: 40,
},
form: {
maxWidth: 400,
marginTop: 20,
marginBottom: 20,
},
formContainer: {
[theme.breakpoints.up("md")]: {
padding: "0px 24px 0 24px",
},
},
}));
export default function UserForm(props) {
const classes = useStyles();
const [loading, setLoading] = useState(false);
const [user, setUser] = useState(
props.user
? props.user
: {
ID: 0,
Email: "",
Nick: "",
Password: "", // 为空时只读
Status: "0", // 转换类型
GroupID: "2", // 转换类型
}
);
const [groups, setGroups] = useState([]);
const history = useHistory();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
useEffect(() => {
API.get("/admin/groups")
.then((response) => {
setGroups(response.data);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
});
}, []);
const handleChange = (name) => (event) => {
setUser({
...user,
[name]: event.target.value,
});
};
const submit = (e) => {
e.preventDefault();
const userCopy = { ...user };
// 整型转换
["Status", "GroupID", "Score"].forEach((v) => {
userCopy[v] = parseInt(userCopy[v]);
});
setLoading(true);
API.post("/admin/user", {
user: userCopy,
password: userCopy.Password,
})
.then(() => {
history.push("/admin/user");
ToggleSnackbar(
"top",
"right",
"用户已" + (props.user ? "保存" : "添加"),
"success"
);
})
.catch((error) => {
ToggleSnackbar("top", "right", error.message, "error");
})
.then(() => {
setLoading(false);
});
};
return (
<div>
<form onSubmit={submit}>
<div className={classes.root}>
<Typography variant="h6" gutterBottom>
{user.ID === 0 && "创建用户"}
{user.ID !== 0 && "编辑 " + user.Nick}
</Typography>
<div className={classes.formContainer}>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
邮箱
</InputLabel>
<Input
value={user.Email}
type={"email"}
onChange={handleChange("Email")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
昵称
</InputLabel>
<Input
value={user.Nick}
onChange={handleChange("Nick")}
required
/>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
密码
</InputLabel>
<Input
type={"password"}
value={user.Password}
onChange={handleChange("Password")}
required={user.ID === 0}
/>
<FormHelperText id="component-helper-text">
{user.ID !== 0 && "留空表示不修改"}
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
用户组
</InputLabel>
<Select
value={user.GroupID}
onChange={handleChange("GroupID")}
required
>
{groups.map((v) => {
if (v.ID === 3) {
return null;
}
return (
<MenuItem
key={v.ID}
value={v.ID.toString()}
>
{v.Name}
</MenuItem>
);
})}
</Select>
<FormHelperText id="component-helper-text">
用户所属用户组
</FormHelperText>
</FormControl>
</div>
<div className={classes.form}>
<FormControl fullWidth>
<InputLabel htmlFor="component-helper">
状态
</InputLabel>
<Select
value={user.Status}
onChange={handleChange("Status")}
required
>
<MenuItem value={"0"}>正常</MenuItem>
<MenuItem value={"1"}>未激活</MenuItem>
<MenuItem value={"2"}>被封禁</MenuItem>
<MenuItem value={"3"}>
超额使用被封禁
</MenuItem>
</Select>
</FormControl>
</div>
</div>
</div>
<div className={classes.root}>
<Button
disabled={loading}
type={"submit"}
variant={"contained"}
color={"primary"}
>
保存
</Button>
</div>
</form>
</div>
);
}

@ -0,0 +1,43 @@
import { Link, makeStyles } from "@material-ui/core";
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { useLocation } from "react-router";
import pageHelper from "../../utils/page";
const useStyles = makeStyles(() => ({
icp: {
padding: "8px 24px",
position: "absolute",
bottom: 0,
},
}));
export const ICPFooter = () => {
const siteICPId = useSelector((state) => state.siteConfig.siteICPId);
const classes = useStyles();
const location = useLocation();
const [show, setShow] = useState(true);
useEffect(() => {
// 只在分享和登录界面显示
const isSharePage = pageHelper.isSharePage(location.pathname);
const isLoginPage = pageHelper.isLoginPage(location.pathname);
setShow(siteICPId && (isSharePage || isLoginPage));
}, [siteICPId, location]);
if (!show) {
return <></>;
}
return (
<div className={classes.icp}>
{`备案号: `}
<Link
href="https://beian.miit.gov.cn/"
rel="noopener noreferrer"
target="_blank"
>
{siteICPId}
</Link>
</div>
);
};

@ -0,0 +1,154 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import {} from "../../actions";
import classNames from "classnames";
import ErrorIcon from "@material-ui/icons/Error";
import InfoIcon from "@material-ui/icons/Info";
import CloseIcon from "@material-ui/icons/Close";
import CheckCircleIcon from "@material-ui/icons/CheckCircle";
import WarningIcon from "@material-ui/icons/Warning";
import { green, amber } from "@material-ui/core/colors";
import {
withStyles,
SnackbarContent,
Snackbar,
IconButton,
} from "@material-ui/core";
const mapStateToProps = (state) => {
return {
snackbar: state.viewUpdate.snackbar,
};
};
const mapDispatchToProps = () => {
return {};
};
const variantIcon = {
success: CheckCircleIcon,
warning: WarningIcon,
error: ErrorIcon,
info: InfoIcon,
};
const styles1 = (theme) => ({
success: {
backgroundColor: theme.palette.success.main,
},
error: {
backgroundColor: theme.palette.error.dark,
},
info: {
backgroundColor: theme.palette.info.main,
},
warning: {
backgroundColor: theme.palette.warning.main,
},
icon: {
fontSize: 20,
},
iconVariant: {
opacity: 0.9,
marginRight: theme.spacing(1),
},
message: {
display: "flex",
alignItems: "center",
},
});
function MySnackbarContent(props) {
const { classes, className, message, onClose, variant, ...other } = props;
const Icon = variantIcon[variant];
return (
<SnackbarContent
className={classNames(classes[variant], className)}
aria-describedby="client-snackbar"
message={
<span id="client-snackbar" className={classes.message}>
<Icon
className={classNames(
classes.icon,
classes.iconVariant
)}
/>
{message}
</span>
}
action={[
<IconButton
key="close"
aria-label="Close"
color="inherit"
className={classes.close}
onClick={onClose}
>
<CloseIcon className={classes.icon} />
</IconButton>,
]}
{...other}
/>
);
}
MySnackbarContent.propTypes = {
classes: PropTypes.object.isRequired,
className: PropTypes.string,
message: PropTypes.node,
onClose: PropTypes.func,
variant: PropTypes.oneOf(["alert", "success", "warning", "error", "info"])
.isRequired,
};
const MySnackbarContentWrapper = withStyles(styles1)(MySnackbarContent);
const styles = (theme) => ({
margin: {
margin: theme.spacing(1),
},
});
class SnackbarCompoment extends Component {
state = {
open: false,
};
UNSAFE_componentWillReceiveProps = (nextProps) => {
if (nextProps.snackbar.toggle !== this.props.snackbar.toggle) {
this.setState({ open: true });
}
};
handleClose = () => {
this.setState({ open: false });
};
render() {
return (
<Snackbar
anchorOrigin={{
vertical: this.props.snackbar.vertical,
horizontal: this.props.snackbar.horizontal,
}}
open={this.state.open}
autoHideDuration={6000}
onClose={this.handleClose}
>
<MySnackbarContentWrapper
onClose={this.handleClose}
variant={this.props.snackbar.color}
message={this.props.snackbar.msg}
/>
</Snackbar>
);
}
}
const AlertBar = connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(SnackbarCompoment));
export default AlertBar;

@ -0,0 +1,45 @@
import React, { useCallback } from "react";
import { openRemoteDownloadDialog } from "../../actions";
import { useDispatch } from "react-redux";
import AutoHidden from "./AutoHidden";
import { makeStyles } from "@material-ui/core";
import Fab from "@material-ui/core/Fab";
import { Add } from "@material-ui/icons";
import Modals from "../FileManager/Modals";
const useStyles = makeStyles(() => ({
fab: {
margin: 0,
top: "auto",
right: 20,
bottom: 20,
left: "auto",
zIndex: 5,
position: "fixed",
},
}));
export default function RemoteDownloadButton() {
const classes = useStyles();
const dispatch = useDispatch();
const OpenRemoteDownloadDialog = useCallback(
() => dispatch(openRemoteDownloadDialog()),
[dispatch]
);
return (
<>
<Modals />
<AutoHidden enable>
<Fab
className={classes.fab}
color="secondary"
onClick={() => OpenRemoteDownloadDialog()}
>
<Add />
</Fab>
</AutoHidden>
</>
);
}

@ -0,0 +1,37 @@
import React, { useState, useEffect } from "react";
import Zoom from "@material-ui/core/Zoom";
function AutoHidden({ children, enable }) {
const [hidden, setHidden] = useState(false);
let prev = window.scrollY;
let lastUpdate = window.scrollY;
const show = 50;
useEffect(() => {
const handleNavigation = (e) => {
const window = e.currentTarget;
if (prev > window.scrollY) {
if (lastUpdate - window.scrollY > show) {
lastUpdate = window.scrollY;
setHidden(false);
}
} else if (prev < window.scrollY) {
if (window.scrollY - lastUpdate > show) {
lastUpdate = window.scrollY;
setHidden(true);
}
}
prev = window.scrollY;
};
if (enable) {
window.addEventListener("scroll", (e) => handleNavigation(e));
}
// eslint-disable-next-line
}, [enable]);
return <Zoom in={!hidden}>{children}</Zoom>;
}
export default AutoHidden;

@ -0,0 +1,164 @@
import React, { useCallback, useState, useEffect } from "react";
import { makeStyles, Badge } from "@material-ui/core";
import SpeedDial from "@material-ui/lab/SpeedDial";
import SpeedDialIcon from "@material-ui/lab/SpeedDialIcon";
import SpeedDialAction from "@material-ui/lab/SpeedDialAction";
import CreateNewFolderIcon from "@material-ui/icons/CreateNewFolder";
import PublishIcon from "@material-ui/icons/Publish";
import {
openCreateFileDialog,
openCreateFolderDialog,
toggleSnackbar,
} from "../../actions";
import { useDispatch } from "react-redux";
import AutoHidden from "./AutoHidden";
import statusHelper from "../../utils/page";
import Backdrop from "@material-ui/core/Backdrop";
import { FolderUpload, FilePlus } from "mdi-material-ui";
const useStyles = makeStyles(() => ({
fab: {
margin: 0,
top: "auto",
right: 20,
bottom: 20,
left: "auto",
zIndex: 5,
position: "fixed",
},
badge: {
position: "absolute",
bottom: 26,
top: "auto",
zIndex: 9999,
right: 7,
},
"@global": {
".MuiSpeedDialAction-staticTooltipLabel": {
width: 100,
},
},
}));
export default function UploadButton(props) {
const [open, setOpen] = useState(false);
const [queued, setQueued] = useState(5);
const classes = useStyles();
const dispatch = useDispatch();
const ToggleSnackbar = useCallback(
(vertical, horizontal, msg, color) =>
dispatch(toggleSnackbar(vertical, horizontal, msg, color)),
[dispatch]
);
const OpenNewFolderDialog = useCallback(
() => dispatch(openCreateFolderDialog()),
[dispatch]
);
const OpenNewFileDialog = useCallback(
() => dispatch(openCreateFileDialog()),
[dispatch]
);
useEffect(() => {
setQueued(props.Queued);
}, [props.Queued]);
const openUpload = (id) => {
const uploadButton = document.getElementsByClassName(id)[0];
if (document.body.contains(uploadButton)) {
uploadButton.click();
} else {
ToggleSnackbar("top", "right", "上传组件还未加载完成", "warning");
}
};
const uploadClicked = () => {
if (open) {
if (queued !== 0) {
props.openFileList();
} else {
openUpload("uploadFileForm");
}
}
};
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<AutoHidden enable>
<Badge
badgeContent={queued}
classes={{
badge: classes.badge, // class name, e.g. `root-x`
}}
className={classes.fab}
invisible={queued === 0}
color="primary"
>
<Backdrop open={open && statusHelper.isMobile()} />
<SpeedDial
ariaLabel="SpeedDial openIcon example"
hidden={false}
tooltipTitle="上传文件"
icon={
<SpeedDialIcon
openIcon={
!statusHelper.isMobile() && <PublishIcon />
}
/>
}
onClose={handleClose}
FabProps={{
onClick: () =>
!statusHelper.isMobile() && uploadClicked(),
color: "secondary",
}}
onOpen={handleOpen}
open={open}
>
{statusHelper.isMobile() && (
<SpeedDialAction
key="UploadFile"
icon={<PublishIcon />}
tooltipOpen
tooltipTitle="上传文件"
onClick={() => uploadClicked()}
title={"上传文件"}
/>
)}
{!statusHelper.isMobile() && (
<SpeedDialAction
key="UploadFolder"
icon={<FolderUpload />}
tooltipOpen
tooltipTitle="上传目录"
onClick={() => openUpload("uploadFolderForm")}
title={"上传目录"}
/>
)}
<SpeedDialAction
key="NewFolder"
icon={<CreateNewFolderIcon />}
tooltipOpen
tooltipTitle="新建目录"
onClick={() => OpenNewFolderDialog()}
title={"新建目录"}
/>
<SpeedDialAction
key="NewFile"
icon={<FilePlus />}
tooltipOpen
tooltipTitle="新建文件"
onClick={() => OpenNewFileDialog()}
title={"新建文件"}
/>
</SpeedDial>
</Badge>
</AutoHidden>
);
}

@ -0,0 +1,84 @@
import React from "react";
import { makeStyles } from "@material-ui/core";
import SaveIcon from "@material-ui/icons/Save";
import CheckIcon from "@material-ui/icons/Check";
import AutoHidden from "./AutoHidden";
import statusHelper from "../../utils/page";
import Fab from "@material-ui/core/Fab";
import Tooltip from "@material-ui/core/Tooltip";
import CircularProgress from "@material-ui/core/CircularProgress";
import { green } from "@material-ui/core/colors";
import clsx from "clsx";
const useStyles = makeStyles((theme) => ({
fab: {
margin: 0,
top: "auto",
right: 20,
bottom: 20,
left: "auto",
zIndex: 5,
position: "fixed",
},
badge: {
position: "absolute",
bottom: 26,
top: "auto",
zIndex: 9999,
right: 7,
},
fabProgress: {
color: green[500],
position: "absolute",
top: -6,
left: -6,
zIndex: 1,
},
wrapper: {
margin: theme.spacing(1),
position: "relative",
},
buttonSuccess: {
backgroundColor: green[500],
"&:hover": {
backgroundColor: green[700],
},
},
}));
export default function SaveButton(props) {
const classes = useStyles();
const buttonClassname = clsx({
[classes.buttonSuccess]: props.status === "success",
});
return (
<AutoHidden enable={statusHelper.isMobile()}>
<div className={classes.fab}>
<div className={classes.wrapper}>
<Tooltip title={"保存"} placement={"left"}>
<Fab
onClick={props.onClick}
color="primary"
className={buttonClassname}
disabled={props.status === "loading"}
aria-label="add"
>
{props.status === "success" ? (
<CheckIcon />
) : (
<SaveIcon />
)}
</Fab>
</Tooltip>
{props.status === "loading" && (
<CircularProgress
size={68}
className={classes.fabProgress}
/>
)}
</div>
</div>
</AutoHidden>
);
}

@ -0,0 +1,200 @@
import { Button, IconButton, Typography, withStyles } from "@material-ui/core";
import RefreshIcon from "@material-ui/icons/Refresh";
import React, { Component } from "react";
import { connect } from "react-redux";
import { toggleSnackbar } from "../../actions";
import API from "../../middleware/Api";
import DownloadingCard from "./DownloadingCard";
import FinishedCard from "./FinishedCard";
import RemoteDownloadButton from "../Dial/Aria2";
import Auth from "../../middleware/Auth";
const styles = (theme) => ({
actions: {
display: "flex",
},
title: {
marginTop: "20px",
},
layout: {
width: "auto",
marginTop: "30px",
marginLeft: theme.spacing(3),
marginRight: theme.spacing(3),
[theme.breakpoints.up(1100 + theme.spacing(3) * 2)]: {
width: 700,
marginLeft: "auto",
marginRight: "auto",
},
},
shareTitle: {
maxWidth: "200px",
},
avatarFile: {
backgroundColor: theme.palette.primary.light,
},
avatarFolder: {
backgroundColor: theme.palette.secondary.light,
},
gird: {
marginTop: "30px",
},
hide: {
display: "none",
},
loadingAnimation: {
borderRadius: "6px 6px 0 0",
},
shareFix: {
marginLeft: "20px",
},
loadMore: {
textAlign: "center",
marginTop: "20px",
marginBottom: "20px",
},
margin: {
marginTop: theme.spacing(2),
},
});
const mapStateToProps = () => {
return {};
};
const mapDispatchToProps = (dispatch) => {
return {
toggleSnackbar: (vertical, horizontal, msg, color) => {
dispatch(toggleSnackbar(vertical, horizontal, msg, color));
},
};
};
class DownloadComponent extends Component {
page = 0;
interval = 0;
state = {
downloading: [],
loading: false,
finishedList: [],
continue: true,
};
componentDidMount = () => {
this.loadDownloading();
this.loadMore();
};
componentWillUnmount() {
clearTimeout(this.interval);
}
loadDownloading = () => {
this.setState({
loading: true,
});
API.get("/aria2/downloading")
.then((response) => {
this.setState({
downloading: response.data,
loading: false,
});
// 设定自动更新
clearTimeout(this.interval);
if (response.data.length > 0) {
this.interval = setTimeout(
this.loadDownloading,
1000 * response.data[0].interval
);
}
})
.catch((error) => {
this.props.toggleSnackbar(
"top",
"right",
error.message,
"error"
);
});
};
loadMore = () => {
this.setState({
loading: true,
});
API.get("/aria2/finished?page=" + ++this.page)
.then((response) => {
this.setState({
finishedList: [
...this.state.finishedList,
...response.data,
],
loading: false,
continue: response.data.length >= 10,
});
})
.catch(() => {
this.props.toggleSnackbar("top", "right", "加载失败", "error");
this.setState({
loading: false,
});
});
};
render() {
const { classes } = this.props;
const user = Auth.GetUser();
return (
<div className={classes.layout}>
{user.group.allowRemoteDownload && <RemoteDownloadButton />}
<Typography
color="textSecondary"
variant="h4"
className={classes.title}
>
进行中
<IconButton
disabled={this.state.loading}
onClick={this.loadDownloading}
>
<RefreshIcon />
</IconButton>
</Typography>
{this.state.downloading.map((value, k) => (
<DownloadingCard key={k} task={value} />
))}
<Typography
color="textSecondary"
variant="h4"
className={classes.title}
>
已完成
</Typography>
<div className={classes.loadMore}>
{this.state.finishedList.map((value, k) => {
if (value.files) {
return <FinishedCard key={k} task={value} />;
}
return null;
})}
<Button
size="large"
className={classes.margin}
disabled={!this.state.continue}
onClick={this.loadMore}
>
加载更多
</Button>
</div>
</div>
);
}
}
const Download = connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(DownloadComponent));
export default Download;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save