@ -0,0 +1,22 @@
|
||||
# See https://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.idea
|
||||
|
||||
# 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*
|
@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
// This is a custom Jest transformer turning style imports into empty objects.
|
||||
// http://facebook.github.io/jest/docs/tutorial-webpack.html
|
||||
|
||||
module.exports = {
|
||||
process() {
|
||||
return 'module.exports = {};';
|
||||
},
|
||||
getCacheKey() {
|
||||
// The output is always the same.
|
||||
return 'cssTransform';
|
||||
},
|
||||
};
|
@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
// This is a custom Jest transformer turning file imports into filenames.
|
||||
// http://facebook.github.io/jest/docs/tutorial-webpack.html
|
||||
|
||||
module.exports = {
|
||||
process(src, filename) {
|
||||
return `module.exports = ${JSON.stringify(path.basename(filename))};`;
|
||||
},
|
||||
};
|
@ -0,0 +1,55 @@
|
||||
'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/facebookincubator/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(path, needsSlash) {
|
||||
const hasSlash = path.endsWith('/');
|
||||
if (hasSlash && !needsSlash) {
|
||||
return path.substr(path, path.length - 1);
|
||||
} else if (!hasSlash && needsSlash) {
|
||||
return `${path}/`;
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// config after eject: we're in ./config/
|
||||
module.exports = {
|
||||
dotenv: resolveApp('.env'),
|
||||
appBuild: resolveApp('build'),
|
||||
appPublic: resolveApp('public'),
|
||||
appHtml: resolveApp('public/index.html'),
|
||||
appIndexJs: resolveApp('src/index.js'),
|
||||
appPackageJson: resolveApp('package.json'),
|
||||
appSrc: resolveApp('src'),
|
||||
yarnLockFile: resolveApp('yarn.lock'),
|
||||
testsSetup: resolveApp('src/setupTests.js'),
|
||||
appNodeModules: resolveApp('node_modules'),
|
||||
publicUrl: getPublicUrl(resolveApp('package.json')),
|
||||
servedPath: getServedPath(resolveApp('package.json')),
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
if (typeof Promise === 'undefined') {
|
||||
// Rejection tracking prevents a common issue where React gets into an
|
||||
// inconsistent state due to an error, but it gets swallowed by a Promise,
|
||||
// and the user has no idea what causes React's erratic future behavior.
|
||||
require('promise/lib/rejection-tracking').enable();
|
||||
window.Promise = require('promise/lib/es6-extensions.js');
|
||||
}
|
||||
|
||||
// fetch() polyfill for making API calls.
|
||||
require('whatwg-fetch');
|
||||
|
||||
// Object.assign() is commonly used with React.
|
||||
// It will use the native implementation if it's present and isn't buggy.
|
||||
Object.assign = require('object-assign');
|
||||
|
||||
// In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet.
|
||||
// We don't polyfill it in the browser--this is user's responsibility.
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
require('raf').polyfill(global);
|
||||
}
|
@ -0,0 +1,263 @@
|
||||
'use strict';
|
||||
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
|
||||
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
||||
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
|
||||
const eslintFormatter = require('react-dev-utils/eslintFormatter');
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
|
||||
const getClientEnvironment = require('./env');
|
||||
const paths = require('./paths');
|
||||
|
||||
// Webpack uses `publicPath` to determine where the app is being served from.
|
||||
// In development, we always serve from the root. This makes config easier.
|
||||
const 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_PATH%/xyz looks better than %PUBLIC_PATH%xyz.
|
||||
const publicUrl = '';
|
||||
// Get environment variables to inject into our app.
|
||||
const env = getClientEnvironment(publicUrl);
|
||||
|
||||
// This is the development configuration.
|
||||
// It is focused on developer experience and fast rebuilds.
|
||||
// The production configuration is different and lives in a separate file.
|
||||
module.exports = {
|
||||
// You may want 'eval' instead if you prefer to see the compiled output in DevTools.
|
||||
// See the discussion in https://github.com/facebookincubator/create-react-app/issues/343.
|
||||
devtool: '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.
|
||||
// The first two entry points enable "hot" CSS and auto-refreshes for JS.
|
||||
entry: [
|
||||
// We ship a few polyfills by default:
|
||||
require.resolve('./polyfills'),
|
||||
// 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'),
|
||||
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.
|
||||
],
|
||||
output: {
|
||||
// Add /* filename */ comments to generated require()s in the output.
|
||||
pathinfo: true,
|
||||
// This does not produce a real file. It's just the virtual path that is
|
||||
// served by WebpackDevServer in development. This is the JS bundle
|
||||
// containing code from all our entry points, and the Webpack runtime.
|
||||
filename: 'static/js/bundle.js',
|
||||
// There are also additional JS chunk files if you use code splitting.
|
||||
chunkFilename: 'static/js/[name].chunk.js',
|
||||
// This is the URL that app is served from. We use "/" in development.
|
||||
publicPath: publicPath,
|
||||
// Point sourcemap entries to original disk location (format as URL on Windows)
|
||||
devtoolModuleFilenameTemplate: info =>
|
||||
path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'),
|
||||
},
|
||||
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/facebookincubator/create-react-app/issues/253
|
||||
modules: ['node_modules', paths.appNodeModules].concat(
|
||||
// It is guaranteed to exist because we tweak it in `env.js`
|
||||
process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
|
||||
),
|
||||
// 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/facebookincubator/create-react-app/issues/290
|
||||
// `web` extension prefixes have been added for better support
|
||||
// for React Native Web.
|
||||
extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx'],
|
||||
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',
|
||||
},
|
||||
plugins: [
|
||||
// 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]),
|
||||
],
|
||||
},
|
||||
module: {
|
||||
strictExportPresence: true,
|
||||
rules: [
|
||||
// TODO: Disable require.ensure as it's not a standard language feature.
|
||||
// We are waiting for https://github.com/facebookincubator/create-react-app/issues/2176.
|
||||
// { parser: { requireEnsure: false } },
|
||||
|
||||
// First, run the linter.
|
||||
// It's important to do this before Babel processes the JS.
|
||||
{
|
||||
test: /\.(js|jsx|mjs)$/,
|
||||
enforce: 'pre',
|
||||
use: [
|
||||
{
|
||||
options: {
|
||||
formatter: eslintFormatter,
|
||||
eslintPath: require.resolve('eslint'),
|
||||
|
||||
},
|
||||
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: 10000,
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
// Process JS with Babel.
|
||||
{
|
||||
test: /\.(js|jsx|mjs)$/,
|
||||
include: paths.appSrc,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
|
||||
// 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,
|
||||
},
|
||||
},
|
||||
// "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 a plugin to extract that CSS to a file, but
|
||||
// in development "style" loader enables hot editing of CSS.
|
||||
{
|
||||
test: /\.(css|less)$/,
|
||||
use: [
|
||||
require.resolve('style-loader'),
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
// Necessary for external CSS imports to work
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2677
|
||||
ident: 'postcss',
|
||||
plugins: () => [
|
||||
require('postcss-flexbugs-fixes'),
|
||||
autoprefixer({
|
||||
browsers: [
|
||||
'>1%',
|
||||
'last 4 versions',
|
||||
'Firefox ESR',
|
||||
'not ie < 9', // React doesn't support IE8 anyway
|
||||
],
|
||||
flexbox: 'no-2009',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
{loader: require.resolve('less-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.
|
||||
{
|
||||
// Exclude `js` files to keep "css" loader working as it injects
|
||||
// it's runtime that would otherwise processed through "file" loader.
|
||||
// Also exclude `html` and `json` extensions so they get processed
|
||||
// by webpacks internal loaders.
|
||||
exclude: [/\.js$/, /\.html$/, /\.json$/],
|
||||
loader: require.resolve('file-loader'),
|
||||
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: [
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
// In development, this will be an empty string.
|
||||
new InterpolateHtmlPlugin(env.raw),
|
||||
// Generates an `index.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
template: paths.appHtml,
|
||||
}),
|
||||
// Add module names to factory functions so they appear in browser profiler.
|
||||
new webpack.NamedModulesPlugin(),
|
||||
// Makes some environment variables available to the JS code, for example:
|
||||
// if (process.env.NODE_ENV === 'development') { ... }. See `./env.js`.
|
||||
new webpack.DefinePlugin(env.stringified),
|
||||
// This is necessary to emit hot updates (currently CSS only):
|
||||
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/facebookincubator/create-react-app/issues/240
|
||||
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/facebookincubator/create-react-app/issues/186
|
||||
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
|
||||
// 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$/),
|
||||
],
|
||||
// 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: {
|
||||
dgram: 'empty',
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
child_process: 'empty',
|
||||
},
|
||||
// Turn off performance hints during development because we don't do any
|
||||
// splitting or minification in interest of speed. These warnings become
|
||||
// cumbersome.
|
||||
performance: {
|
||||
hints: false,
|
||||
},
|
||||
};
|
@ -0,0 +1,342 @@
|
||||
'use strict';
|
||||
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const ExtractTextPlugin = require('extract-text-webpack-plugin');
|
||||
const ManifestPlugin = require('webpack-manifest-plugin');
|
||||
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
||||
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
|
||||
const eslintFormatter = require('react-dev-utils/eslintFormatter');
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
|
||||
const paths = require('./paths');
|
||||
const getClientEnvironment = require('./env');
|
||||
|
||||
// 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.
|
||||
const publicPath = paths.servedPath;
|
||||
// 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 === './';
|
||||
// Source maps are resource heavy and can cause out of memory issue for large source files.
|
||||
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
|
||||
// `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 = publicPath.slice(0, -1);
|
||||
// Get environment variables to inject into our app.
|
||||
const env = getClientEnvironment(publicUrl);
|
||||
|
||||
// Assert this just to be safe.
|
||||
// Development builds of React are slow and not intended for production.
|
||||
if (env.stringified['process.env'].NODE_ENV !== '"production"') {
|
||||
throw new Error('Production builds must have NODE_ENV=production.');
|
||||
}
|
||||
|
||||
// Note: defined here because it will be used more than once.
|
||||
const cssFilename = 'static/css/[name].[contenthash:8].css';
|
||||
|
||||
// ExtractTextPlugin expects the build output to be flat.
|
||||
// (See https://github.com/webpack-contrib/extract-text-webpack-plugin/issues/27)
|
||||
// However, our output is structured with css, js and media folders.
|
||||
// To have this structure working with relative paths, we have to use custom options.
|
||||
const extractTextPluginOptions = shouldUseRelativeAssetPaths
|
||||
? // Making sure that the publicPath goes back to to build folder.
|
||||
{ publicPath: Array(cssFilename.split('/').length).join('../') }
|
||||
: {};
|
||||
|
||||
// This is the production configuration.
|
||||
// It compiles slowly and is focused on producing a fast and minimal bundle.
|
||||
// The development configuration is different and lives in a separate file.
|
||||
module.exports = {
|
||||
// Don't attempt to continue if there are any errors.
|
||||
bail: true,
|
||||
// We generate sourcemaps in production. This is slow but gives good results.
|
||||
// You can exclude the *.map files from the build during deployment.
|
||||
devtool: shouldUseSourceMap ? 'source-map' : false,
|
||||
// In production, we only want to load the polyfills and the app code.
|
||||
entry: [require.resolve('./polyfills'), paths.appIndexJs],
|
||||
output: {
|
||||
// The build folder.
|
||||
path: paths.appBuild,
|
||||
// Generated JS file names (with nested folders).
|
||||
// There will be one main bundle, and one file per asynchronous chunk.
|
||||
// We don't currently advertise code splitting but Webpack supports it.
|
||||
filename: 'static/js/[name].[chunkhash:8].js',
|
||||
chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
|
||||
// We inferred the "public path" (such as / or /my-project) from homepage.
|
||||
publicPath: publicPath,
|
||||
// Point sourcemap entries to original disk location (format as URL on Windows)
|
||||
devtoolModuleFilenameTemplate: info =>
|
||||
path
|
||||
.relative(paths.appSrc, info.absoluteResourcePath)
|
||||
.replace(/\\/g, '/'),
|
||||
},
|
||||
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/facebookincubator/create-react-app/issues/253
|
||||
modules: ['node_modules', paths.appNodeModules].concat(
|
||||
// It is guaranteed to exist because we tweak it in `env.js`
|
||||
process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
|
||||
),
|
||||
// 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/facebookincubator/create-react-app/issues/290
|
||||
// `web` extension prefixes have been added for better support
|
||||
// for React Native Web.
|
||||
extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx'],
|
||||
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',
|
||||
},
|
||||
plugins: [
|
||||
// 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]),
|
||||
],
|
||||
},
|
||||
module: {
|
||||
strictExportPresence: true,
|
||||
rules: [
|
||||
// TODO: Disable require.ensure as it's not a standard language feature.
|
||||
// We are waiting for https://github.com/facebookincubator/create-react-app/issues/2176.
|
||||
// { parser: { requireEnsure: false } },
|
||||
|
||||
// First, run the linter.
|
||||
// It's important to do this before Babel processes the JS.
|
||||
{
|
||||
test: /\.(js|jsx|mjs)$/,
|
||||
enforce: 'pre',
|
||||
use: [
|
||||
{
|
||||
options: {
|
||||
formatter: eslintFormatter,
|
||||
eslintPath: require.resolve('eslint'),
|
||||
|
||||
},
|
||||
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 just like "file" loader but it also embeds
|
||||
// assets smaller than specified size as data URLs to avoid requests.
|
||||
{
|
||||
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
|
||||
loader: require.resolve('url-loader'),
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
// Process JS with Babel.
|
||||
{
|
||||
test: /\.(js|jsx|mjs)$/,
|
||||
include: paths.appSrc,
|
||||
loader: require.resolve('babel-loader'),
|
||||
options: {
|
||||
|
||||
compact: true,
|
||||
},
|
||||
},
|
||||
// The notation here is somewhat confusing.
|
||||
// "postcss" loader applies autoprefixer to our CSS.
|
||||
// "css" loader resolves paths in CSS and adds assets as dependencies.
|
||||
// "style" loader normally turns CSS into JS modules injecting <style>,
|
||||
// but unlike in development configuration, we do something different.
|
||||
// `ExtractTextPlugin` first applies the "postcss" and "css" loaders
|
||||
// (second argument), then grabs the result CSS and puts it into a
|
||||
// separate file in our build process. This way we actually ship
|
||||
// a single CSS file in production instead of JS code injecting <style>
|
||||
// tags. If you use code splitting, however, any async bundles will still
|
||||
// use the "style" loader inside the async code so CSS from them won't be
|
||||
// in the main CSS file.
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: ExtractTextPlugin.extract(
|
||||
Object.assign(
|
||||
{
|
||||
fallback: {
|
||||
loader: require.resolve('style-loader'),
|
||||
options: {
|
||||
hmr: false,
|
||||
},
|
||||
},
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: {
|
||||
importLoaders: 1,
|
||||
minimize: true,
|
||||
sourceMap: shouldUseSourceMap,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
// Necessary for external CSS imports to work
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2677
|
||||
ident: 'postcss',
|
||||
plugins: () => [
|
||||
require('postcss-flexbugs-fixes'),
|
||||
autoprefixer({
|
||||
browsers: [
|
||||
'>1%',
|
||||
'last 4 versions',
|
||||
'Firefox ESR',
|
||||
'not ie < 9', // React doesn't support IE8 anyway
|
||||
],
|
||||
flexbox: 'no-2009',
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
extractTextPluginOptions
|
||||
)
|
||||
),
|
||||
// Note: this won't work without `new ExtractTextPlugin()` in `plugins`.
|
||||
},
|
||||
// "file" loader makes sure assets end up in the `build` folder.
|
||||
// When you `import` an asset, you get its filename.
|
||||
// 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
|
||||
// it's runtime that would otherwise processed through "file" loader.
|
||||
// Also exclude `html` and `json` extensions so they get processed
|
||||
// by webpacks internal loaders.
|
||||
exclude: [/\.js$/, /\.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: [
|
||||
// Makes some environment variables available in index.html.
|
||||
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
|
||||
// <link rel="shortcut 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.
|
||||
new InterpolateHtmlPlugin(env.raw),
|
||||
// Generates an `index.html` file with the <script> injected.
|
||||
new HtmlWebpackPlugin({
|
||||
inject: true,
|
||||
template: paths.appHtml,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
removeRedundantAttributes: true,
|
||||
useShortDoctype: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true,
|
||||
keepClosingSlash: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true,
|
||||
minifyURLs: true,
|
||||
},
|
||||
}),
|
||||
// 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 was set to production here.
|
||||
// Otherwise React will be compiled in the very slow development mode.
|
||||
new webpack.DefinePlugin(env.stringified),
|
||||
// Minify the code.
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false,
|
||||
// Disabled because of an issue with Uglify breaking seemingly valid code:
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2376
|
||||
// Pending further investigation:
|
||||
// https://github.com/mishoo/UglifyJS2/issues/2011
|
||||
comparisons: false,
|
||||
},
|
||||
mangle: {
|
||||
safari10: true,
|
||||
},
|
||||
output: {
|
||||
comments: false,
|
||||
// Turned on because emoji and regex is not minified properly using default
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2488
|
||||
ascii_only: true,
|
||||
},
|
||||
sourceMap: shouldUseSourceMap,
|
||||
}),
|
||||
// Note: this won't work without ExtractTextPlugin.extract(..) in `loaders`.
|
||||
new ExtractTextPlugin({
|
||||
filename: cssFilename,
|
||||
}),
|
||||
// Generate a manifest file which contains a mapping of all asset filenames
|
||||
// to their corresponding output file so that tools can pick it up without
|
||||
// having to parse `index.html`.
|
||||
new ManifestPlugin({
|
||||
fileName: 'asset-manifest.json',
|
||||
}),
|
||||
// Generate a service worker script that will precache, and keep up to date,
|
||||
// the HTML & assets that are part of the Webpack build.
|
||||
new SWPrecacheWebpackPlugin({
|
||||
// By default, a cache-busting query parameter is appended to requests
|
||||
// used to populate the caches, to ensure the responses are fresh.
|
||||
// If a URL is already hashed by Webpack, then there is no concern
|
||||
// about it being stale, and the cache-busting can be skipped.
|
||||
dontCacheBustUrlsMatching: /\.\w{8}\./,
|
||||
filename: 'service-worker.js',
|
||||
logger(message) {
|
||||
if (message.indexOf('Total precache size is') === 0) {
|
||||
// This message occurs for every build and is a bit too noisy.
|
||||
return;
|
||||
}
|
||||
if (message.indexOf('Skipping static resource') === 0) {
|
||||
// This message obscures real errors so we ignore it.
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2612
|
||||
return;
|
||||
}
|
||||
console.log(message);
|
||||
},
|
||||
minify: true,
|
||||
// For unknown URLs, fallback to the index page
|
||||
navigateFallback: publicUrl + '/index.html',
|
||||
// Ignores URLs starting from /__ (useful for Firebase):
|
||||
// https://github.com/facebookincubator/create-react-app/issues/2237#issuecomment-302693219
|
||||
navigateFallbackWhitelist: [/^(?!\/__).*/],
|
||||
// Don't precache sourcemaps (they're large) and build asset manifest:
|
||||
staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/],
|
||||
}),
|
||||
// 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$/),
|
||||
],
|
||||
// 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: {
|
||||
dgram: 'empty',
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty',
|
||||
child_process: 'empty',
|
||||
},
|
||||
};
|
@ -0,0 +1,127 @@
|
||||
{
|
||||
"name": "campus-card",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"antd": "^3.4.0",
|
||||
"autoprefixer": "7.1.6",
|
||||
"babel-core": "6.26.0",
|
||||
"babel-eslint": "7.2.3",
|
||||
"babel-jest": "20.0.3",
|
||||
"babel-loader": "7.1.2",
|
||||
"babel-preset-react-app": "^3.1.1",
|
||||
"babel-runtime": "6.26.0",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"case-sensitive-paths-webpack-plugin": "2.1.1",
|
||||
"chalk": "1.1.3",
|
||||
"connect-mongo": "^2.0.1",
|
||||
"connect-multiparty": "^2.1.0",
|
||||
"cookie-parser": "^1.4.3",
|
||||
"css-loader": "0.28.7",
|
||||
"dotenv": "4.0.0",
|
||||
"echarts": "^4.0.4",
|
||||
"eslint": "4.10.0",
|
||||
"eslint-config-react-app": "^2.1.0",
|
||||
"eslint-loader": "1.9.0",
|
||||
"eslint-plugin-flowtype": "2.39.1",
|
||||
"eslint-plugin-import": "2.8.0",
|
||||
"eslint-plugin-jsx-a11y": "5.1.1",
|
||||
"eslint-plugin-react": "7.4.0",
|
||||
"express": "^4.16.3",
|
||||
"express-session": "^1.15.6",
|
||||
"extract-text-webpack-plugin": "3.0.2",
|
||||
"file-loader": "1.1.5",
|
||||
"fs-extra": "3.0.1",
|
||||
"html-webpack-plugin": "2.29.0",
|
||||
"jest": "20.0.4",
|
||||
"mongoose": "^4.13.12",
|
||||
"object-assign": "4.1.1",
|
||||
"postcss-flexbugs-fixes": "3.2.0",
|
||||
"postcss-loader": "2.0.8",
|
||||
"promise": "^8.0.1",
|
||||
"raf": "3.4.0",
|
||||
"react": "^16.3.1",
|
||||
"react-addons-pure-render-mixin": "^15.6.2",
|
||||
"react-dev-utils": "^4.2.1",
|
||||
"react-dom": "^16.3.1",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-router": "^3.2.1",
|
||||
"redux": "^3.7.2",
|
||||
"style-loader": "0.19.0",
|
||||
"sw-precache-webpack-plugin": "0.11.4",
|
||||
"url-loader": "0.6.2",
|
||||
"webpack": "3.8.1",
|
||||
"webpack-dev-server": "2.9.4",
|
||||
"webpack-manifest-plugin": "1.3.2",
|
||||
"whatwg-fetch": "2.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node scripts/start.js",
|
||||
"server": "set NODE_ENV=development && nodemon --watch server server/server.js",
|
||||
"build": "node scripts/build.js",
|
||||
"test": "node scripts/test.js --env=jsdom"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-plugin-import": "^1.7.0",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-preset-react-native-stage-0": "^1.0.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"less": "^2.7.3",
|
||||
"less-loader": "^4.1.0",
|
||||
"morgan": "^1.9.0"
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
"src/**/*.{js,jsx,mjs}"
|
||||
],
|
||||
"setupFiles": [
|
||||
"<rootDir>/config/polyfills.js"
|
||||
],
|
||||
"testMatch": [
|
||||
"<rootDir>/src/**/__tests__/**/*.{js,jsx,mjs}",
|
||||
"<rootDir>/src/**/?(*.)(spec|test).{js,jsx,mjs}"
|
||||
],
|
||||
"testEnvironment": "node",
|
||||
"testURL": "http://localhost",
|
||||
"transform": {
|
||||
"^.+\\.(js|jsx|mjs)$": "<rootDir>/node_modules/babel-jest",
|
||||
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
|
||||
"^(?!.*\\.(js|jsx|mjs|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
|
||||
},
|
||||
"transformIgnorePatterns": [
|
||||
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs)$"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^react-native$": "react-native-web"
|
||||
},
|
||||
"moduleFileExtensions": [
|
||||
"web.js",
|
||||
"mjs",
|
||||
"js",
|
||||
"json",
|
||||
"web.jsx",
|
||||
"jsx",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"react-app"
|
||||
],
|
||||
"plugins": [
|
||||
[
|
||||
"import",
|
||||
{
|
||||
"libraryName": "antd",
|
||||
"style": true
|
||||
}
|
||||
],
|
||||
[
|
||||
"transform-decorators-legacy"
|
||||
]
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 902 B |
@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="theme-color" content="#000000">
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is added to the
|
||||
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>校园一卡通平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
You need to enable JavaScript to run this app.
|
||||
</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
'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('chalk');
|
||||
const fs = require('fs-extra');
|
||||
const webpack = require('webpack');
|
||||
const config = require('../config/webpack.config.prod');
|
||||
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;
|
||||
|
||||
// Warn and crash if required files are missing
|
||||
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// First, read the current file sizes in build directory.
|
||||
// This lets us display how much they changed later.
|
||||
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 => {
|
||||
console.log(chalk.red('Failed to compile.\n'));
|
||||
printBuildError(err);
|
||||
process.exit(1);
|
||||
}
|
||||
);
|
||||
|
||||
// Create the production build and print the deployment instructions.
|
||||
function build(previousFileSizes) {
|
||||
console.log('Creating an optimized production build...');
|
||||
|
||||
let compiler = webpack(config);
|
||||
return new Promise((resolve, reject) => {
|
||||
compiler.run((err, stats) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
const messages = formatWebpackMessages(stats.toJson({}, 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,92 @@
|
||||
'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('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 config = require('../config/webpack.config.dev');
|
||||
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';
|
||||
|
||||
// We attempt to use the default port but if it is busy, we offer the user to
|
||||
// run on a different port. `detect()` Promise resolves to the next free port.
|
||||
choosePort(HOST, DEFAULT_PORT)
|
||||
.then(port => {
|
||||
if (port == null) {
|
||||
// We have not found a port.
|
||||
return;
|
||||
}
|
||||
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||
const appName = require(paths.appPackageJson).name;
|
||||
const urls = prepareUrls(protocol, HOST, port);
|
||||
// Create a webpack compiler that is configured with custom messages.
|
||||
const compiler = createCompiler(webpack, config, appName, urls, useYarn);
|
||||
// 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 sever.
|
||||
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();
|
||||
}
|
||||
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,27 @@
|
||||
'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 argv = process.argv.slice(2);
|
||||
|
||||
// Watch unless on CI or in coverage mode
|
||||
if (!process.env.CI && argv.indexOf('--coverage') < 0) {
|
||||
argv.push('--watch');
|
||||
}
|
||||
|
||||
|
||||
jest.run(argv);
|
@ -0,0 +1,54 @@
|
||||
const Card = require('../controllers/Card');
|
||||
const Admin = require('../controllers/Admin');
|
||||
const Notice = require('../controllers/Notice');
|
||||
const Instruction = require('../controllers/Instruction');
|
||||
const Img = require('../controllers/Img');
|
||||
const File = require('../controllers/File');
|
||||
|
||||
module.exports = app => {
|
||||
//退出登录
|
||||
app.post('/logout', Card.logout);
|
||||
|
||||
//管理员
|
||||
app.post('/admin/login', Admin.login);
|
||||
app.post('/card/register', Admin.adminRequired, Card.upload, Card.register);
|
||||
app.get('/card/getCode', Admin.adminRequired, Card.getCode);
|
||||
app.get('/card/list', Admin.adminRequired, Card.cardList);
|
||||
app.post('/card/delete', Admin.adminRequired, Card.deleteCard);
|
||||
app.post('/admin/resetPassword', Admin.adminRequired, Card.resetPassword);
|
||||
|
||||
//用户
|
||||
app.post('/card/login', Card.login);
|
||||
app.post('/card/changePassword', Admin.loginRequired, Card.changePassword);
|
||||
app.post('/card/shop', Admin.loginRequired, Card.shop);
|
||||
|
||||
//公共
|
||||
app.get('/card/detail', Admin.loginRequired, Card.detail);
|
||||
app.post('/card/update', Admin.loginRequired, Card.upload, Card.update);
|
||||
app.post('/card/frozen', Admin.loginRequired, Card.frozen);
|
||||
app.post('/card/recharge', Admin.loginRequired, Card.recharge);
|
||||
app.post('/card/billList', Admin.loginRequired, Card.billList);
|
||||
app.post('/card/beforeUpload', Admin.loginRequired, Card.beforeUpload);
|
||||
|
||||
//公告
|
||||
app.post('/notice/new', Admin.adminRequired, Notice.new);
|
||||
app.post('/notice/update', Admin.adminRequired, Notice.update);
|
||||
app.get('/notice/detail', Notice.detail);
|
||||
app.post('/notice/list', Notice.list);
|
||||
app.post('/notice/delete', Admin.adminRequired, Notice.delete);
|
||||
app.post('/notice/show', Admin.adminRequired, Notice.show);
|
||||
|
||||
//使用说明
|
||||
app.post('/instruction/save', Admin.adminRequired, Instruction.save);
|
||||
app.get('/instruction/detail', Instruction.detail);
|
||||
|
||||
//图片服务器代理
|
||||
app.get('/imgs/*', Img.photo);
|
||||
|
||||
//文件
|
||||
app.get('/file/fileList', File.fileFist);
|
||||
app.post('/file/upload', Admin.adminRequired, File.upload);
|
||||
app.post('/file/download', File.download);
|
||||
app.post('/file/delete', Admin.adminRequired, File.delete);
|
||||
|
||||
};
|
@ -0,0 +1,65 @@
|
||||
const Admin = require('../models/Admin');
|
||||
const setJson = require('../until/SetJson');
|
||||
|
||||
const comparePasswordPromise = (card, password) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
card.comparePassword(password, (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
//管理员登录
|
||||
exports.login = async (req, res) => {
|
||||
console.log(req.session);
|
||||
let _user = req.body;
|
||||
let code = _user.code;
|
||||
let password = _user.password;
|
||||
try {
|
||||
let user = await Admin.findOne({code});
|
||||
//首先检查用户是否存在
|
||||
if (!user) {
|
||||
console.log('用户名不存在');
|
||||
res.json(setJson(false, '管理员不存在', null));
|
||||
} else {
|
||||
let isMatch = await comparePasswordPromise(user, password);
|
||||
//密码是否正确
|
||||
if (isMatch) {
|
||||
console.log(`${code}:登陆成功`);
|
||||
req.session.user = user;
|
||||
req.session.isAdmin = true;
|
||||
res.json(setJson(true, '登陆成功', user));
|
||||
} else {
|
||||
res.json(setJson(false, '密码错误', null));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e.stack);
|
||||
res.json(setJson(false, e.message, null))
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 验证管理员权限,中间件
|
||||
exports.adminRequired = (req, res, next) => {
|
||||
let user = req.session.user;
|
||||
if (user&&user.isAdmin) {
|
||||
next();
|
||||
} else {
|
||||
res.json(setJson(false, '权限不足', null));
|
||||
}
|
||||
};
|
||||
// 验证是否登录,中间件
|
||||
exports.loginRequired = (req, res, next) => {
|
||||
let user = req.session.user;
|
||||
if (user) {
|
||||
next();
|
||||
} else {
|
||||
res.json(setJson(false, '请登录', null));
|
||||
}
|
||||
};
|
@ -0,0 +1,118 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const setJson = require('../until/SetJson');
|
||||
const File = require('../models/File');
|
||||
|
||||
|
||||
const readFilePromise = (path) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(path, (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
const writeFilePromise = (file, data) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.writeFile(file, data, (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
const unlinkPromise = (path) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.unlink(path, (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
exports.upload = async (req, res, next) => {
|
||||
try {
|
||||
let fileData = req.files.file;
|
||||
let filePath = fileData ? fileData.path : '';
|
||||
let originalFilename = fileData ? fileData.originalFilename : '';
|
||||
//存在上传的文件则保存到服务器
|
||||
if (originalFilename) {
|
||||
let data = await readFilePromise(filePath);
|
||||
let timestamp = Date.now();
|
||||
let type = originalFilename.split('.')[originalFilename.split('.').length - 1];
|
||||
let file = `${timestamp}.${type}`;
|
||||
let newPath = path.join(__dirname, '../../', '/server/public/upload/files/' + file);
|
||||
await writeFilePromise(newPath, data);
|
||||
let newFile = new File({filename: file, originalName: originalFilename});
|
||||
await newFile.save();
|
||||
res.json(setJson(true, '上传文件成功', null))
|
||||
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e.stack);
|
||||
res.json(setJson(false, e.message, null));
|
||||
}
|
||||
};
|
||||
|
||||
//文件列表
|
||||
exports.fileFist = async (req, res) => {
|
||||
try {
|
||||
let files = await File.find({})
|
||||
.sort({'meta.updateAt': -1});
|
||||
res.json(setJson(true, '查询列表情成功', files))
|
||||
} catch (e) {
|
||||
console.log(e.stack);
|
||||
res.json(setJson(false, e.message, null))
|
||||
}
|
||||
};
|
||||
|
||||
//删除文件
|
||||
exports.delete = async (req, res) => {
|
||||
try {
|
||||
let _id = req.body._id;
|
||||
let file = await File.findOneAndRemove({_id});
|
||||
//删除文件资源
|
||||
if (file && file.filename) {
|
||||
let oldPath = path.join(__dirname, '../../', `/server/public/upload/files/${file.filename}`);
|
||||
await unlinkPromise(oldPath)
|
||||
}
|
||||
res.json(setJson(true, '删除文件成功', file))
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e.stack);
|
||||
res.json(setJson(false, e.message, null))
|
||||
}
|
||||
};
|
||||
|
||||
//下载文件
|
||||
exports.download = async (req, res) => {
|
||||
try {
|
||||
let file = req.body.filename;
|
||||
let filePath = path.join(__dirname, '../../', `/server/public/upload/files/${file}`);
|
||||
let content = await readFilePromise(filePath, "binary");
|
||||
res.setHeader(
|
||||
"Content-type", "application/octet-stream"
|
||||
);
|
||||
await File.findOneAndUpdate({filename:file}, {$inc: {downloadNum: 1}});
|
||||
let findFile=await File.findOne({filename:file});
|
||||
console.log(findFile);
|
||||
res.end(new Buffer(content, 'binary'));
|
||||
|
||||
} catch (e) {
|
||||
res.end(`系统异常:${e.message}`)
|
||||
|
||||
}
|
||||
};
|
||||
|
@ -0,0 +1,32 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
|
||||
const readFilePromise = (path) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(path, (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
//显示图片
|
||||
exports.photo = async (req, res) => {
|
||||
try {
|
||||
let img = req.params[0];
|
||||
let imgPath = path.join(__dirname, '../../', `/server/public/upload/imgs/${img}`);
|
||||
let content = await readFilePromise(imgPath, "binary");
|
||||
res.setHeader('Content-Type', 'image/png');
|
||||
res.end(new Buffer(content, 'binary'));
|
||||
|
||||
} catch (e) {
|
||||
let imgPath = path.join(__dirname, '../../', '/server/public/upload/imgs/404/404.png');
|
||||
let content = await readFilePromise(imgPath, "binary");
|
||||
res.setHeader('Content-Type', 'image/png');
|
||||
res.end(new Buffer(content, 'binary'));
|
||||
}
|
||||
};
|
||||
|
@ -0,0 +1,38 @@
|
||||
const Instruction = require('../models/Instruction');
|
||||
const setJson = require('../until/SetJson');
|
||||
|
||||
//新增(保存)使用说明
|
||||
exports.save = async (req, res) => {
|
||||
try {
|
||||
let _instruction = req.body, instruction;
|
||||
let instructions = await Instruction.find();
|
||||
if (instructions.length) {
|
||||
instruction = instructions[0];
|
||||
Object.assign(instruction, _instruction)
|
||||
} else {
|
||||
instruction = new Instruction(_instruction);
|
||||
}
|
||||
instruction = await instruction.save();
|
||||
res.json(setJson(true, '保存使用说明成功', instruction))
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e.stack);
|
||||
res.json(setJson(false, e.message, null))
|
||||
}
|
||||
};
|
||||
|
||||
//使用说明详情
|
||||
exports.detail = async (req, res) => {
|
||||
try {
|
||||
let instruction = await Instruction.findOne();
|
||||
if (instruction) {
|
||||
res.json(setJson(true, '查看使用说明成功', instruction))
|
||||
} else {
|
||||
res.json(setJson(false, '查看使用说明失败', null))
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e.stack);
|
||||
res.json(setJson(false, e.message, null))
|
||||
}
|
||||
};
|
@ -0,0 +1,111 @@
|
||||
const Notice = require('../models/Notice');
|
||||
const setJson = require('../until/SetJson');
|
||||
|
||||
//新增通知
|
||||
exports.new = async (req, res) => {
|
||||
let _notice = req.body;
|
||||
try {
|
||||
if (_notice.isShow === 'true') {
|
||||
_notice.isShow = true
|
||||
} else if (_notice.isShow === 'false') {
|
||||
_notice.isShow = false;
|
||||
}
|
||||
_notice.createPerson = '校园一卡通平台';
|
||||
let notice = new Notice(_notice);
|
||||
notice = await notice.save();
|
||||
res.json(setJson(true, '新增通知成功', notice))
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e.stack);
|
||||
res.json(setJson(false, e.message, null))
|
||||
}
|
||||
};
|
||||
|
||||
//公告详情
|
||||
exports.detail = async (req, res) => {
|
||||
let _id = req.query._id;
|
||||
let user = req.session.user;
|
||||
try {
|
||||
//不是管理员查看则浏览量+1
|
||||
if (!user.isAdmin) {
|
||||
await Notice.findOneAndUpdate({_id}, {$inc: {pv: 1}});
|
||||
}
|
||||
let notice = await Notice.findOne({_id});
|
||||
if (notice) {
|
||||
res.json(setJson(true, '新增通知成功', notice))
|
||||
} else {
|
||||
res.json(setJson(false, '查看公告详情失败', null))
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e.stack);
|
||||
res.json(setJson(false, e.message, null))
|
||||
}
|
||||
};
|
||||
|
||||
//通知列表
|
||||
exports.list = async (req, res) => {
|
||||
try {
|
||||
let user = req.session.user, notices;
|
||||
if (user && user.isAdmin && req.body.listTable) {
|
||||
notices = await Notice.find().sort({'meta.updateAt': -1});
|
||||
} else {
|
||||
notices = await Notice.find({isShow: true}).sort({'meta.updateAt': -1});
|
||||
}
|
||||
res.json(setJson(true, '获取公告列表', notices))
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e.stack);
|
||||
res.json(setJson(false, e.message, null))
|
||||
}
|
||||
};
|
||||
|
||||
//删除通知
|
||||
exports.delete = async (req, res) => {
|
||||
try {
|
||||
let _id = req.body._id;
|
||||
let notice = await Notice.findOneAndRemove({_id});
|
||||
res.json(setJson(true, '删除公告成功', notice))
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e.stack);
|
||||
res.json(setJson(false, e.message, null))
|
||||
}
|
||||
};
|
||||
//更新通知
|
||||
exports.update = async (req, res) => {
|
||||
try {
|
||||
let _notice = req.body;
|
||||
let _id = _notice._id;
|
||||
if (_notice.isShow === 'true') {
|
||||
_notice.isShow = true
|
||||
} else if (_notice.isShow === 'false') {
|
||||
_notice.isShow = false;
|
||||
}
|
||||
let notice = await Notice.findOne({_id});
|
||||
Object.assign(notice, _notice);
|
||||
notice = await notice.save();
|
||||
res.json(setJson(true, '更新公告成功', notice))
|
||||
}
|
||||
catch (e) {
|
||||
console.log(e.stack);
|
||||
res.json(setJson(false, e.message, null))
|
||||
}
|
||||
};
|
||||
|
||||
//显示操作
|
||||
exports.show = async (req, res) => {
|
||||
let _id = req.body._id;
|
||||
try {
|
||||
let notice = await Notice.findOne({_id});
|
||||
notice.isShow = !notice.isShow;
|
||||
notice = await notice.save();
|
||||
let str = notice.isShow ? '显示' : '取消显示';
|
||||
res.json(setJson(true, `${str}成功`, notice))
|
||||
} catch (e) {
|
||||
console.log(e.stack);
|
||||
res.json(setJson(false, e.message, null))
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,4 @@
|
||||
const mongoose = require('mongoose');
|
||||
const AdminSchema = require('../schemas/AdminSchema');
|
||||
const Admin = mongoose.model('Admin', AdminSchema);
|
||||
module.exports = Admin;
|
@ -0,0 +1,4 @@
|
||||
const mongoose = require('mongoose');
|
||||
const CardSchema = require('../schemas/CardSchema');
|
||||
const Card = mongoose.model('Card', CardSchema);
|
||||
module.exports = Card;
|
@ -0,0 +1,4 @@
|
||||
const mongoose = require('mongoose');
|
||||
const FileSchema = require('../schemas/FileSchema');
|
||||
const File = mongoose.model('File', FileSchema);
|
||||
module.exports = File;
|
@ -0,0 +1,4 @@
|
||||
const mongoose = require('mongoose');
|
||||
const InstructionSchema = require('../schemas/InstructionSchema');
|
||||
const Instruction = mongoose.model('Instruction', InstructionSchema);
|
||||
module.exports = Instruction;
|
@ -0,0 +1,4 @@
|
||||
const mongoose = require('mongoose');
|
||||
const NoticeSchema = require('../schemas/NoticeSchema');
|
||||
const Notice = mongoose.model('Notice', NoticeSchema);
|
||||
module.exports = Notice;
|
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 9.8 KiB |
After Width: | Height: | Size: 7.1 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 8.6 KiB |
After Width: | Height: | Size: 5.5 KiB |
@ -0,0 +1,69 @@
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcrypt-nodejs');
|
||||
const SALT_WORK_FACTOR = 10;
|
||||
const Schema = mongoose.Schema;
|
||||
const AdminSchema = new Schema({
|
||||
code: {
|
||||
type: String,
|
||||
unique: true,
|
||||
required: true
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isAdmin:{
|
||||
type:Boolean,
|
||||
default:true
|
||||
},
|
||||
meta: {
|
||||
createAt: {
|
||||
type: Date,
|
||||
default: Date.now()
|
||||
},
|
||||
updateAt: {
|
||||
type: Date,
|
||||
default: Date.now()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//新增用户
|
||||
AdminSchema.pre('save', function (next) {
|
||||
let user = this;
|
||||
if (this.isNew) {
|
||||
this.meta.createAt = this.meta.updateAt = Date.now()
|
||||
} else {
|
||||
this.meta.updateAt = Date.now()
|
||||
}
|
||||
bcrypt.genSalt(SALT_WORK_FACTOR, (err, salt) => {
|
||||
if (err) {
|
||||
return next(err)
|
||||
} else {
|
||||
//密码加盐
|
||||
bcrypt.hash(user.password, salt, null, (err, hash) => {
|
||||
if (err) {
|
||||
return next(err)
|
||||
} else {
|
||||
user.password = hash;
|
||||
next()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
//匹配密码
|
||||
AdminSchema.methods = {
|
||||
comparePassword(_password, cb) {
|
||||
bcrypt.compare(_password, this.password, (err, isMatch) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return cb(err)
|
||||
} else {
|
||||
cb(null, isMatch)
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
module.exports = AdminSchema;
|
@ -0,0 +1,119 @@
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcrypt-nodejs');
|
||||
const SALT_WORK_FACTOR = 10;
|
||||
const Schema = mongoose.Schema;
|
||||
const ObjectId = Schema.Types.ObjectId;
|
||||
const CardSchema = new Schema({
|
||||
cardholder: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
code: {
|
||||
type: String,
|
||||
unique: true,
|
||||
required: true
|
||||
},
|
||||
sex: {
|
||||
type: String,
|
||||
enum: ['男', '女'],
|
||||
default: '男'
|
||||
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['临时卡', '学生卡', '教师卡']
|
||||
},
|
||||
photo: String,
|
||||
balance: {
|
||||
type: Number,
|
||||
default: 80.00
|
||||
},
|
||||
isFrozen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isLost: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isAdmin: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
bills: [{
|
||||
time: {
|
||||
type: Date,
|
||||
default: Date.now(),
|
||||
required: true
|
||||
},
|
||||
place: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
amount: {
|
||||
type: Number,
|
||||
default: 0.00,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['充值', '消费'],
|
||||
required: true
|
||||
}
|
||||
}],
|
||||
meta: {
|
||||
createAt: {
|
||||
type: Date,
|
||||
default: Date.now()
|
||||
},
|
||||
updateAt: {
|
||||
type: Date,
|
||||
default: Date.now()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//新增用户
|
||||
CardSchema.pre('save', function (next) {
|
||||
let user = this;
|
||||
if (this.isNew) {
|
||||
this.meta.createAt = this.meta.updateAt = Date.now()
|
||||
} else {
|
||||
this.meta.updateAt = Date.now()
|
||||
}
|
||||
bcrypt.genSalt(SALT_WORK_FACTOR, (err, salt) => {
|
||||
if (err) {
|
||||
return next(err)
|
||||
} else {
|
||||
//密码加盐
|
||||
bcrypt.hash(user.password, salt, null, (err, hash) => {
|
||||
if (err) {
|
||||
return next(err)
|
||||
} else {
|
||||
user.password = hash;
|
||||
next()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
//匹配密码
|
||||
CardSchema.methods = {
|
||||
comparePassword(_password, cb) {
|
||||
bcrypt.compare(_password, this.password, (err, isMatch) => {
|
||||
if (err) {
|
||||
console.log(err);
|
||||
return cb(err)
|
||||
} else {
|
||||
cb(null, isMatch)
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = CardSchema;
|
@ -0,0 +1,38 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
const FileSchema = new Schema({
|
||||
|
||||
filename: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
originalName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
downloadNum: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
|
||||
meta: {
|
||||
createAt: {
|
||||
type: Date,
|
||||
default: Date.now()
|
||||
},
|
||||
updateAt: {
|
||||
type: Date,
|
||||
default: Date.now()
|
||||
}
|
||||
}
|
||||
});
|
||||
FileSchema.pre('save', function (next) {
|
||||
if (this.isNew) {
|
||||
this.meta.createAt = this.meta.updateAt = Date.now()
|
||||
} else {
|
||||
this.meta.updateAt = Date.now()
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
module.exports = FileSchema;
|
@ -0,0 +1,31 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
const InstructionSchema = new Schema({
|
||||
|
||||
instruction: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
||||
meta: {
|
||||
createAt: {
|
||||
type: Date,
|
||||
default: Date.now()
|
||||
},
|
||||
updateAt: {
|
||||
type: Date,
|
||||
default: Date.now()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
InstructionSchema.pre('save', function (next) {
|
||||
if (this.isNew) {
|
||||
this.meta.createAt = this.meta.updateAt = Date.now()
|
||||
} else {
|
||||
this.meta.updateAt = Date.now()
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
module.exports = InstructionSchema;
|
@ -0,0 +1,45 @@
|
||||
const mongoose = require('mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
const NoticeSchema = new Schema({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
maxlength: 1000
|
||||
},
|
||||
isShow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
required: true
|
||||
},
|
||||
createPerson: String,
|
||||
pv: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
required: true
|
||||
},
|
||||
meta: {
|
||||
createAt: {
|
||||
type: Date,
|
||||
default: Date.now()
|
||||
},
|
||||
updateAt: {
|
||||
type: Date,
|
||||
default: Date.now()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
NoticeSchema.pre('save', function (next) {
|
||||
if (this.isNew) {
|
||||
this.meta.createAt = this.meta.updateAt = Date.now()
|
||||
} else {
|
||||
this.meta.updateAt = Date.now()
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
module.exports = NoticeSchema;
|
@ -0,0 +1,7 @@
|
||||
module.exports = function setJson(...arg) {
|
||||
return {
|
||||
success: arg[0],
|
||||
msg: arg[1],
|
||||
backData: arg[2]
|
||||
}
|
||||
};
|
@ -0,0 +1,8 @@
|
||||
import * as actionTypes from '../constants/menuKey'
|
||||
|
||||
export function update(data) {
|
||||
return {
|
||||
type: actionTypes.MENU_KEY_UPDATE,
|
||||
data
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import * as actionTypes from '../constants/modalVisible'
|
||||
|
||||
export function update(data) {
|
||||
return {
|
||||
type: actionTypes.MODAL_VISIBLE_UPDATE,
|
||||
data
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import * as actionTypes from '../constants/userInfo'
|
||||
|
||||
export function update(data) {
|
||||
return {
|
||||
type: actionTypes.USER_INFO_UPDATE,
|
||||
data
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
.about-us-modal {
|
||||
.ant-modal-content {
|
||||
.inner-content {
|
||||
letter-spacing: 0.2em;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
.balance-state{
|
||||
margin: 60px auto;
|
||||
text-align: center;
|
||||
.balance-num{
|
||||
transform: scale(2);
|
||||
}
|
||||
.balance-form{
|
||||
margin-top: 40px;
|
||||
}
|
||||
.after-balance-num{
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
@ -0,0 +1,148 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Calendar, Badge} from 'antd'
|
||||
import Moment from 'moment'
|
||||
import {post} from "../../fetch/post";
|
||||
import {message} from "antd";
|
||||
let _this = null;
|
||||
export default class BillCalendar extends React.Component {
|
||||
// 构造
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
// 初始状态
|
||||
this.state = {
|
||||
pagination: {},
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
_this = this;
|
||||
let {userInfo} = this.props;
|
||||
let code = userInfo.code;
|
||||
code && code !== 'admin' && this.init(code)
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
}
|
||||
|
||||
onPanelChange(date) {
|
||||
|
||||
|
||||
}
|
||||
|
||||
//初始化(获取出卡人信息)
|
||||
init(code, value) {
|
||||
this.setState({loading: true});
|
||||
post('/card/billList', {code, date: value}, (data) => {
|
||||
this.setState({loading: false});
|
||||
if (data.success) {
|
||||
delete data.backData._id;
|
||||
this.setState({
|
||||
data: data.backData
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
data: {}
|
||||
});
|
||||
message.error(data.msg)
|
||||
}
|
||||
}, () => {
|
||||
this.setState({loading: false});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Calendar dateCellRender={dateCellRender} monthCellRender={monthCellRender}
|
||||
onPanelChange={this.onPanelChange.bind(this)}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function getListData(value) {
|
||||
let bills = _this.state.data ? _this.state.data.bills : null;
|
||||
let listData, income = 0.00, outlay = 0.00;
|
||||
if (Array.isArray(bills)) {
|
||||
bills.map(bill => {
|
||||
bill.time = new Moment(bill.time);
|
||||
if (bill.time.year() === value.year() && bill.time.month() === value.month() && bill.time.date() === value.date()) {
|
||||
if (bill.type === '充值') {
|
||||
income += bill.amount;
|
||||
} else {
|
||||
outlay += bill.amount;
|
||||
}
|
||||
}
|
||||
return true
|
||||
});
|
||||
}
|
||||
if (outlay || income) {
|
||||
listData = [
|
||||
{type: 'error', content: `消费:${outlay.toFixed(2)}`},
|
||||
{type: 'success', content: `充值:${income.toFixed(2)}`},
|
||||
];
|
||||
}
|
||||
|
||||
return listData
|
||||
}
|
||||
|
||||
function dateCellRender(value) {
|
||||
const listData = getListData(value);
|
||||
return (
|
||||
<ul className="events">
|
||||
{
|
||||
listData ? listData.map(item => (
|
||||
<li style={{listStyle: 'none'}} key={item.content}>
|
||||
<Badge status={item.type} text={item.content}/>
|
||||
</li>
|
||||
)) : null
|
||||
}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
function getMonthData(value) {
|
||||
let bills = _this.state.data ? _this.state.data.bills : null;
|
||||
|
||||
let listData = [], income = 0.00, outlay = 0.00;
|
||||
if (Array.isArray(bills)) {
|
||||
bills.map(bill => {
|
||||
bill.time = new Moment(bill.time);
|
||||
console.log(bill.time.month());
|
||||
if (bill.time.year() === value.year() && bill.time.month() === value.month()) {
|
||||
if (bill.type === '充值') {
|
||||
income += bill.amount;
|
||||
} else {
|
||||
outlay += bill.amount;
|
||||
}
|
||||
}
|
||||
return true
|
||||
});
|
||||
}
|
||||
if (outlay || income) {
|
||||
listData = [
|
||||
{type: 'error', content: `消费:${outlay.toFixed(2)}`},
|
||||
{type: 'success', content: `充值:${income.toFixed(2)}`},
|
||||
];
|
||||
}
|
||||
console.log(listData);
|
||||
return listData
|
||||
}
|
||||
|
||||
function monthCellRender(value) {
|
||||
const listData = getMonthData(value);
|
||||
return listData ? (
|
||||
<div className="notes-month">
|
||||
<ul className="events">
|
||||
{
|
||||
listData ? listData.map(item => (
|
||||
<li style={{listStyle: 'none'}} key={item.content}>
|
||||
<Badge status={item.type} text={item.content}/>
|
||||
</li>
|
||||
)) : null
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import moment from 'moment'
|
||||
import {Table} from 'antd';
|
||||
|
||||
export default class BillListTable extends React.Component {
|
||||
// 构造
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
// 初始状态
|
||||
this.state = {
|
||||
pagination: {},
|
||||
};
|
||||
}
|
||||
|
||||
//表格分页
|
||||
handleTableChange = (pagination, filters, sorter) => {
|
||||
|
||||
};
|
||||
|
||||
render() {
|
||||
const columns = [
|
||||
{
|
||||
title: '地点',
|
||||
dataIndex: 'place',
|
||||
}, {
|
||||
title: '金额',
|
||||
dataIndex: 'amount',
|
||||
sorter: (a, b) => a.code - b.code,
|
||||
render: (text, record, index) => record.type==='充值'?
|
||||
<span style={{color:'green',fontWeight:'bold'}}>+{text.toFixed(2)}</span>:
|
||||
<span style={{color:'red',fontWeight:'bold'}}>-{text.toFixed(2)}</span>
|
||||
|
||||
}, {
|
||||
title: '时间',
|
||||
dataIndex: 'time',
|
||||
sorter: (a, b) => a['meta.createAt'] - b['meta.createAt'],
|
||||
render: (text, record, index) => moment(text).format('YYYY-MM-DD HH:mm')
|
||||
},{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
filters: [
|
||||
{text: '充值', value: '充值'},
|
||||
{text: '消费', value: '消费'},
|
||||
],
|
||||
onFilter: (value, record) => record.type.includes(value),
|
||||
}
|
||||
];
|
||||
return <Table columns={columns}
|
||||
rowKey={record => record._id}
|
||||
dataSource={this.props.data}
|
||||
// pagination={this.state.pagination}
|
||||
onChange={this.handleTableChange}
|
||||
className='movie-table-list'
|
||||
/>
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Steps, Icon, Tag} from 'antd';
|
||||
import './style.less'
|
||||
const Step = Steps.Step;
|
||||
export default class CardBaseInfo extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
|
||||
// 初始状态
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let {data} = this.props;
|
||||
return (
|
||||
<Steps className='base-info'>
|
||||
<Step status="finish" title={`卡号`}
|
||||
description={data.code ? <Tag color="purple">{data.code}</Tag> : ''}
|
||||
icon={<Icon type="credit-card" />}/>
|
||||
<Step status="finish"
|
||||
title={`姓名`}
|
||||
description={data.cardholder ? <Tag color="cyan">{data.cardholder}</Tag> : ''}
|
||||
icon={<Icon type="user"/>}/>
|
||||
<Step status="finish" title={`性别`}
|
||||
description={data.sex ? <Tag color="geekblue">{data.sex}</Tag> : ''}
|
||||
icon={<Icon type="exception" />}/>
|
||||
|
||||
</Steps>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,5 @@
|
||||
.base-info {
|
||||
.ant-tag {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
@ -0,0 +1,271 @@
|
||||
import React from 'react'
|
||||
import {Form, Input, Tooltip, Icon, Button, Radio, Upload, message} from 'antd';
|
||||
import typeRadioData from '../../viewDatas/typeRadio'
|
||||
import * as domainConstants from '../../fetch/domainConstants'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const RadioGroup = Radio.Group;
|
||||
|
||||
class CardDetailInfoForm extends React.Component {
|
||||
// 构造
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// 初始状态
|
||||
this.state = {
|
||||
confirmDirty: false,
|
||||
codeDisabled: false,
|
||||
loading: false,
|
||||
imageUrl: '',
|
||||
};
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
codeDisabled: false,
|
||||
isFrozenDisabled: false,
|
||||
isLostDisabled: false,
|
||||
|
||||
};
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.data.code !== this.props.data.code) {
|
||||
if (nextProps.data && JSON.stringify(nextProps.data) !== '{}') {
|
||||
let value = this.props.form.getFieldsValue();
|
||||
for (let key in value) {
|
||||
if (value.hasOwnProperty(key)) {
|
||||
value[key] = nextProps.data[key]
|
||||
}
|
||||
}
|
||||
|
||||
if (nextProps.data.photo) {
|
||||
this.setState({loading: true});
|
||||
this.setState({imageUrl: `${domainConstants.DOMAIN}/imgs/${nextProps.data.photo}`, loading: true});
|
||||
}
|
||||
|
||||
this.props.form.setFieldsValue(value)
|
||||
} else {
|
||||
this.props.form.resetFields()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.form.validateFieldsAndScroll((err, values) => {
|
||||
if (!err) {
|
||||
this.props.handleSubmit(values);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
handleConfirmBlur = (e) => {
|
||||
const value = e.target.value;
|
||||
this.setState({confirmDirty: this.state.confirmDirty || !!value});
|
||||
};
|
||||
|
||||
checkPassword = (rule, value, callback) => {
|
||||
const form = this.props.form;
|
||||
if (value && value !== form.getFieldValue('password')) {
|
||||
callback('两次输入的密码不一致!');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
checkConfirm = (rule, value, callback) => {
|
||||
const form = this.props.form;
|
||||
if (value && this.state.confirmDirty) {
|
||||
form.validateFields(['confirm'], {force: true});
|
||||
}
|
||||
if (value.length < 6) {
|
||||
callback(`密码不能少于6个字符`);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
handleChange = (info) => {
|
||||
if (info.file.status === 'uploading') {
|
||||
this.setState({loading: true});
|
||||
return;
|
||||
}
|
||||
if (info.file.status === 'done') {
|
||||
// Get this url from response in real world.
|
||||
getBase64(info.file.originFileObj, imageUrl => this.setState({
|
||||
imageUrl,
|
||||
loading: false,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {getFieldDecorator} = this.props.form;
|
||||
let {codeDisabled, type, showPassword, userInfo} = this.props;
|
||||
const uploadButton = (
|
||||
<div>
|
||||
<Icon type={this.state.loading ? 'loading' : 'plus'}/>
|
||||
<div className="ant-upload-text">上传</div>
|
||||
</div>
|
||||
);
|
||||
const {imageUrl} = this.state;
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 3},
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 12},
|
||||
},
|
||||
};
|
||||
const tailFormItemLayout = {
|
||||
wrapperCol: {
|
||||
xs: {
|
||||
span: 24,
|
||||
offset: 0,
|
||||
},
|
||||
sm: {
|
||||
span: 16,
|
||||
offset: 8,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label={(
|
||||
<span>
|
||||
一卡通账号
|
||||
<Tooltip title="一卡通账号为系统自动计算">
|
||||
<Icon type="question-circle-o"/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
{getFieldDecorator('code', {
|
||||
rules: [{
|
||||
required: true, message: '一卡通账号不能为空!',
|
||||
}],
|
||||
})(
|
||||
<Input disabled={codeDisabled}/>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label={(
|
||||
<span>
|
||||
持卡人姓名
|
||||
<Tooltip title="请输入持卡人的真实姓名">
|
||||
<Icon type="question-circle-o"/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
{getFieldDecorator('cardholder', {
|
||||
rules: [{required: true, message: '持卡人姓名不能为空!', whitespace: true}],
|
||||
})(
|
||||
<Input/>
|
||||
)}
|
||||
</FormItem>
|
||||
{showPassword ? [<FormItem key='password'{...formItemLayout} label={(
|
||||
<span>密码 <Tooltip title="系统默认密码为666666">
|
||||
<Icon type="question-circle-o"/>
|
||||
</Tooltip></span>)}>
|
||||
{getFieldDecorator('password', {
|
||||
rules: [{
|
||||
required: true, message: '密码不能为空!',
|
||||
}, {
|
||||
validator: this.checkConfirm,
|
||||
}],
|
||||
})(
|
||||
<Input type="password"/>
|
||||
)}
|
||||
</FormItem>,
|
||||
<FormItem key='confirm'{...formItemLayout} label="确认密码">
|
||||
{getFieldDecorator('confirm', {
|
||||
rules: [{
|
||||
required: true, message: '确认密码为必填!',
|
||||
}, {
|
||||
validator: this.checkPassword,
|
||||
}],
|
||||
})(
|
||||
<Input type="password" onBlur={this.handleConfirmBlur}/>
|
||||
)}
|
||||
</FormItem>] : ''}
|
||||
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label="头像"
|
||||
>
|
||||
<div className="dropbox">
|
||||
{getFieldDecorator('photo')(
|
||||
<Upload
|
||||
name="photo"
|
||||
withCredentials
|
||||
listType="picture-card"
|
||||
className="avatar-uploader"
|
||||
showUploadList={false}
|
||||
action={`${domainConstants.DOMAIN}/card/beforeUpload`}
|
||||
beforeUpload={beforeUpload}
|
||||
onChange={this.handleChange}
|
||||
>
|
||||
{imageUrl ? <img style={{width: '100%'}} src={imageUrl} alt=""/> : uploadButton}
|
||||
</Upload>
|
||||
)}
|
||||
</div>
|
||||
</FormItem>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label="性别">
|
||||
{getFieldDecorator('sex', {
|
||||
rules: [{required: true, message: '请选择性别!'}],
|
||||
})(<RadioGroup>
|
||||
<Radio value='男'>男</Radio>
|
||||
<Radio value='女'>女</Radio>
|
||||
</RadioGroup>)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label="卡类别"
|
||||
>
|
||||
{getFieldDecorator('type', {
|
||||
rules: [{required: true, message: '请选择卡类别!'}],
|
||||
})(
|
||||
<RadioGroup disabled={!userInfo.isAdmin}>
|
||||
{
|
||||
typeRadioData.map(item =>
|
||||
<Radio value={item.key} key={item.key}>{item.value}</Radio>)
|
||||
}
|
||||
</RadioGroup>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem {...tailFormItemLayout}>
|
||||
<Button type="primary" htmlType="submit">{type}</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CardDetailInfoForm = Form.create({})(CardDetailInfoForm);
|
||||
|
||||
|
||||
function getBase64(img, callback) {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => callback(reader.result));
|
||||
reader.readAsDataURL(img);
|
||||
}
|
||||
|
||||
function beforeUpload(file) {
|
||||
const isJPG = file.type === 'image/jpeg' || 'image/png';
|
||||
if (!isJPG) {
|
||||
message.error('只能上传JPG或PNG文件!');
|
||||
}
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
if (!isLt2M) {
|
||||
message.error('图片大小必选小于2MB!');
|
||||
}
|
||||
return isJPG && isLt2M;
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import moment from 'moment'
|
||||
import {Table, Menu, Dropdown, Icon, Popconfirm, message, Avatar} from 'antd';
|
||||
import {post} from "../../fetch/post";
|
||||
import selectedKeyUntil from "../../until/selectedKeyUntil";
|
||||
import * as domainConstants from "../../fetch/domainConstants";
|
||||
|
||||
export default class CardListTable extends React.Component {
|
||||
// 构造
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
// 初始状态
|
||||
this.state = {
|
||||
pagination: {},
|
||||
};
|
||||
}
|
||||
|
||||
//渲染行操作按钮
|
||||
renderOperate(text, record, index) {
|
||||
return <Dropdown
|
||||
overlay={<Menu onClick={this.handleOperate.bind(this, text, record, index)}>
|
||||
<Menu.Item key='delete'>
|
||||
<Popconfirm title={`确定删除${record.cardholder}吗?`}
|
||||
onConfirm={this.confirm.bind(this, text, record, index)}
|
||||
onCancel={this.cancel} okText="Yes" cancelText="No">
|
||||
<Icon type="delete"/> 删除
|
||||
</Popconfirm>
|
||||
</Menu.Item>
|
||||
<Menu.Item key='update'>
|
||||
<Icon type="edit"/> 编辑
|
||||
</Menu.Item>
|
||||
<Menu.Item key='operate'>
|
||||
<Icon type="safety"/> 操作卡
|
||||
</Menu.Item>
|
||||
<Menu.Item key='billList'>
|
||||
<Icon type="eye-o"/> 查看账单
|
||||
</Menu.Item>
|
||||
|
||||
</Menu>}>
|
||||
<span style={{color: '#1890FF', cursor: 'pointer'}}>
|
||||
操作 <Icon type="down"/>
|
||||
</span>
|
||||
</Dropdown>
|
||||
}
|
||||
|
||||
//渲染头像
|
||||
renderPhoto(text, record, index) {
|
||||
return text ? <Avatar src={`${domainConstants.DOMAIN}/imgs/${text}`}/> :
|
||||
<Avatar icon="user" style={{backgroundColor: '#1890FF'}}/>
|
||||
}
|
||||
|
||||
//表格分页
|
||||
handleTableChange = (pagination, filters, sorter) => {
|
||||
|
||||
};
|
||||
|
||||
//行操作按钮绑定事件
|
||||
handleOperate(text, record, index, e) {
|
||||
switch (e.key) {
|
||||
case'update':
|
||||
this.routeTo(record, `admin/cardholderDetail`);
|
||||
break;
|
||||
case'billList':
|
||||
this.routeTo(record, `admin/billList`);
|
||||
break;
|
||||
case'operate':
|
||||
this.routeTo(record, `admin/operate`);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//确定删除
|
||||
confirm(text, record, index, e) {
|
||||
post(`/card/delete`, {_id: record._id}, (data) => {
|
||||
if (data.success) {
|
||||
message.success(data.msg);
|
||||
this.props.getDataSource();
|
||||
} else {
|
||||
message.error(data.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//取消删除卡片
|
||||
cancel() {
|
||||
|
||||
}
|
||||
|
||||
//跳转到详情页面
|
||||
routeTo(record, url) {
|
||||
let {menuKey, menuKeyActions} = this.props;
|
||||
selectedKeyUntil.update(menuKey, menuKeyActions, url, true, {code: record.code})
|
||||
}
|
||||
|
||||
render() {
|
||||
const columns = [
|
||||
{
|
||||
title: '持卡人姓名',
|
||||
dataIndex: 'cardholder',
|
||||
fixed: 'left',
|
||||
width: 130,
|
||||
render: (text, record, index) => <a
|
||||
onClick={this.routeTo.bind(this, record, 'admin/cardholderDetail')}>{text}</a>,
|
||||
}, {
|
||||
title: '卡号',
|
||||
dataIndex: 'code',
|
||||
sorter: (a, b) => a.code - b.code,
|
||||
fixed: 'left',
|
||||
width: 100,
|
||||
}, {
|
||||
title: '头像',
|
||||
dataIndex: 'photo',
|
||||
width: 100,
|
||||
render: this.renderPhoto.bind(this),
|
||||
}, {
|
||||
title: '性别',
|
||||
dataIndex: 'sex',
|
||||
filters: [
|
||||
{text: '男', value: '男'},
|
||||
{text: '女', value: '女'},
|
||||
],
|
||||
onFilter: (value, record) => record.sex.includes(value),
|
||||
width: 100,
|
||||
}, {
|
||||
title: '卡类别',
|
||||
dataIndex: 'type',
|
||||
filters: [
|
||||
{text: '临时卡', value: '临时卡'},
|
||||
{text: '学生卡', value: '学生卡'},
|
||||
{text: '教师卡', value: '教师卡'}
|
||||
],
|
||||
onFilter: (value, record) => record.type.includes(value),
|
||||
width: 100,
|
||||
}, {
|
||||
title: '余额',
|
||||
dataIndex: 'balance',
|
||||
width: 150,
|
||||
render: (text, record, index) => text.toFixed(2)
|
||||
|
||||
}, {
|
||||
title: '是否挂失',
|
||||
dataIndex: 'isFrozen',
|
||||
width: 100,
|
||||
render: (text, record, index) => text ? '是' : '否'
|
||||
|
||||
}, {
|
||||
title: '创建时间',
|
||||
dataIndex: 'meta.createAt',
|
||||
width: 150,
|
||||
// sorter: (a, b) => a['meta.createAt'] - b['meta.createAt'],
|
||||
render: (text, record, index) => moment(text).format('YYYY-MM-DD HH:mm')
|
||||
|
||||
}, {
|
||||
title: '更新时间',
|
||||
dataIndex: 'meta.updateAt',
|
||||
width: 150,
|
||||
// sorter: (a, b) => a['meta.updateAt'] - b['meta.updateAt'],
|
||||
render: (text, record, index) => moment(text).format('YYYY-MM-DD HH:mm')
|
||||
}, {
|
||||
title: '操作',
|
||||
dataIndex: 'operate',
|
||||
fixed: 'right',
|
||||
width: 100,
|
||||
render: this.renderOperate.bind(this)
|
||||
}
|
||||
];
|
||||
return <Table columns={columns}
|
||||
rowKey={record => record._id}
|
||||
dataSource={this.props.data}
|
||||
// pagination={this.state.pagination}
|
||||
onChange={this.handleTableChange}
|
||||
scroll={{x: 1300}}
|
||||
className='movie-table-list'
|
||||
/>
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
import React from 'react'
|
||||
import {Form, Input, Modal, Button, message, Spin} from 'antd';
|
||||
import {post} from "../../fetch/post";
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
class ChangePasswordModal extends React.Component {
|
||||
// 构造
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// 初始状态
|
||||
this.state = {
|
||||
loading: false,
|
||||
confirmDirty: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
//修改密码
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
let {modalVisible, modalVisibleActions} = this.props;
|
||||
this.props.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
this.setState({loading: true});
|
||||
//发送请求
|
||||
post('/card/changePassword', values,
|
||||
(data) => {
|
||||
if (data.success) {
|
||||
this.successCb(data);
|
||||
} else {
|
||||
message.error(data.msg);
|
||||
this.setState({loading: false});
|
||||
}
|
||||
},
|
||||
() => {
|
||||
//更新弹窗状态
|
||||
modalVisible.changePasswordVisible = false;
|
||||
modalVisibleActions.update(modalVisible);
|
||||
//更新按钮状态
|
||||
this.setState({loading: false});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
successCb(data) {
|
||||
let {modalVisible, modalVisibleActions} = this.props;
|
||||
//更新弹窗状态
|
||||
modalVisible.changePasswordVisible = false;
|
||||
modalVisibleActions.update(modalVisible);
|
||||
message.success(data.msg);
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
//点击确认
|
||||
handleOk = () => {
|
||||
let {modalVisible, modalVisibleActions} = this.props;
|
||||
modalVisible.changePasswordVisible = false;
|
||||
this.setState({loading: false});
|
||||
modalVisibleActions.update(modalVisible);
|
||||
};
|
||||
|
||||
//关闭弹窗
|
||||
handleCancel = () => {
|
||||
let {modalVisible, modalVisibleActions} = this.props;
|
||||
modalVisible.changePasswordVisible = false;
|
||||
modalVisibleActions.update(modalVisible);
|
||||
};
|
||||
|
||||
handleConfirmBlur = (e) => {
|
||||
const value = e.target.value;
|
||||
this.setState({confirmDirty: this.state.confirmDirty || !!value});
|
||||
};
|
||||
|
||||
checkPassword = (rule, value, callback) => {
|
||||
const form = this.props.form;
|
||||
if (value && value !== form.getFieldValue('password')) {
|
||||
callback('两次输入的密码不一致!');
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
checkConfirm = (rule, value, callback) => {
|
||||
const form = this.props.form;
|
||||
if (value && this.state.confirmDirty) {
|
||||
form.validateFields(['confirm'], {force: true});
|
||||
}
|
||||
|
||||
if (value && value.length < 6) {
|
||||
callback(`密码不能少于6个字符`);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {modalVisible} = this.props;
|
||||
const {getFieldDecorator} = this.props.form;
|
||||
const {loading} = this.state;
|
||||
return (
|
||||
<Modal
|
||||
visible={modalVisible.changePasswordVisible}
|
||||
title="修改密码"
|
||||
onOk={this.handleOk}
|
||||
onCancel={this.handleCancel}
|
||||
footer=''
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<FormItem>
|
||||
{getFieldDecorator('oldPassword', {
|
||||
rules: [{required: true, message: '请输入一卡通账号!'}],
|
||||
})(
|
||||
<Input type='password' placeholder="原密码"/>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
{getFieldDecorator('password', {
|
||||
rules: [{
|
||||
required: true, message: '密码不能为空!',
|
||||
}, {
|
||||
validator: this.checkConfirm,
|
||||
}],
|
||||
})(
|
||||
<Input type="password" placeholder="新密码"/>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
{getFieldDecorator('confirm', {
|
||||
rules: [{
|
||||
required: true, message: '确认密码为必填!',
|
||||
}, {
|
||||
validator: this.checkPassword,
|
||||
}],
|
||||
})(
|
||||
<Input type="password" onBlur={this.handleConfirmBlur}
|
||||
placeholder="确认新密码"/>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button className='float-right login-button'
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
>
|
||||
确 认 修 改
|
||||
</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ChangePasswordModal = Form.create({})(ChangePasswordModal);
|
@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Timeline} from 'antd';
|
||||
import moment from 'moment'
|
||||
import {download} from "../../fetch/download";
|
||||
import love from '../../static/imgs/爱心.svg'
|
||||
import time from '../../static/imgs/时间.svg'
|
||||
import './style.less'
|
||||
|
||||
export default class FileList extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
|
||||
// 初始状态
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
downloadFile(filename, originalName) {
|
||||
download('/file/download', filename, originalName);
|
||||
}
|
||||
|
||||
render() {
|
||||
let {data} = this.props;
|
||||
return (
|
||||
<Timeline className='file-list'>
|
||||
{data.map((file, index) => <Timeline.Item key={file._id} color="green">
|
||||
<p style={{'color': '#1890ff', 'cursor': 'pointer','display':'inline-block'}}
|
||||
onClick={this.downloadFile.bind(this, file.filename, file.originalName)}>{index + 1}、{file.originalName}</p>
|
||||
<p>已下载 {file.downloadNum} 次 <img src={love} alt=''/></p>
|
||||
<p>发布于 {moment(file.meta.createAt).format('YYYY-MM-DD HH:mm')}<img src={time} alt=''/></p>
|
||||
</Timeline.Item>)}
|
||||
</Timeline>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
.file-list {
|
||||
margin: 20px auto;
|
||||
img {
|
||||
margin-bottom: 4px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
img:hover {
|
||||
-webkit-transform: rotateY(40deg);
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import moment from 'moment'
|
||||
import {Table, Icon, Popconfirm} from 'antd';
|
||||
|
||||
export default class FileListTable extends React.Component {
|
||||
// 构造
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
// 初始状态
|
||||
this.state = {
|
||||
pagination: {},
|
||||
};
|
||||
}
|
||||
|
||||
//表格分页
|
||||
handleTableChange = (pagination, filters, sorter) => {
|
||||
|
||||
};
|
||||
|
||||
|
||||
//确定删除
|
||||
confirm(text, record, index, e) {
|
||||
this.props.delFile(record._id);
|
||||
}
|
||||
|
||||
//取消删除卡片
|
||||
cancel() {
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const columns = [
|
||||
{
|
||||
title: '文件名',
|
||||
dataIndex: 'originalName',
|
||||
}, {
|
||||
title: '时间戳名',
|
||||
dataIndex: 'filename',
|
||||
}, {
|
||||
title: '下载次数',
|
||||
dataIndex: 'downloadNum',
|
||||
}, {
|
||||
title: '创建时间',
|
||||
dataIndex: 'meta.createAt',
|
||||
sorter: (a, b) => a['meta.createAt'] - b['meta.createAt'],
|
||||
render: (text, record, index) => moment(text).format('YYYY-MM-DD HH:mm')
|
||||
|
||||
}, {
|
||||
title: '更新时间',
|
||||
dataIndex: 'meta.updateAt',
|
||||
sorter: (a, b) => a['meta.updateAt'] - b['meta.updateAt'],
|
||||
render: (text, record, index) => moment(text).format('YYYY-MM-DD HH:mm')
|
||||
}, {
|
||||
title: '操作',
|
||||
dataIndex: 'operate',
|
||||
fixed: 'right',
|
||||
width: 100,
|
||||
render: (text, record, index) => <Popconfirm title={`确定删除《${record.originalName}》吗?`}
|
||||
onConfirm={this.confirm.bind(this, text, record, index)}
|
||||
onCancel={this.cancel} okText="Yes" cancelText="No">
|
||||
<span style={{'color': '#1890ff', 'cursor': 'pointer'}}><Icon type="delete"/> 删除</span>
|
||||
</Popconfirm>
|
||||
}
|
||||
];
|
||||
return <Table columns={columns}
|
||||
rowKey={record => record._id}
|
||||
dataSource={this.props.data}
|
||||
// pagination={this.state.pagination}
|
||||
onChange={this.handleTableChange}
|
||||
className='movie-table-list'
|
||||
/>
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Upload, Icon, message} from 'antd';
|
||||
import * as domainConstants from "../../fetch/domainConstants";
|
||||
|
||||
const Dragger = Upload.Dragger;
|
||||
|
||||
const props = {
|
||||
name: 'file',
|
||||
multiple: true,
|
||||
withCredentials: true,
|
||||
action: `${domainConstants.DOMAIN}/file/upload`,
|
||||
onChange(info) {
|
||||
const status = info.file.status;
|
||||
if (status !== 'uploading') {
|
||||
console.log(info.file, info.fileList);
|
||||
}
|
||||
if (status === 'done') {
|
||||
message.success(`${info.file.name} 文件上传成功.`);
|
||||
} else if (status === 'error') {
|
||||
message.error(`${info.file.name} 文件上传失败.`);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default class NotFound extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
// 初始状态
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Dragger {...props}>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<Icon type="inbox"/>
|
||||
</p>
|
||||
<p className="ant-upload-text">点击或拖动文件到这个区域上传</p>
|
||||
<p className="ant-upload-hint">支持单个或批量上传, 严格禁止上传公司数据或其他频段文件</p>
|
||||
</Dragger>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Switch} from 'antd'
|
||||
import './style.less'
|
||||
|
||||
export default class FrozenState extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
|
||||
// 初始状态
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
this.props.frozenOperate();
|
||||
}
|
||||
|
||||
render() {
|
||||
let {data, loading} = this.props;
|
||||
return (
|
||||
<div className='frozen-state'>
|
||||
{data.code ?
|
||||
<Switch checkedChildren="已挂失" unCheckedChildren="未挂失" checked={data.isFrozen}
|
||||
loading={loading} onChange={this.handleChange.bind(this)}/> : ''}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
.frozen-state{
|
||||
text-align: center;
|
||||
margin: 100px auto;
|
||||
.ant-switch{
|
||||
transform: scale(2);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Card, Avatar, Button} from 'antd'
|
||||
import logo from '../../static/imgs/logo.svg'
|
||||
import './style.less'
|
||||
|
||||
const {Meta} = Card;
|
||||
|
||||
export default class GoodList extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
// 初始状态
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
handleClick(good) {
|
||||
this.props.shopping(good);
|
||||
}
|
||||
|
||||
render() {
|
||||
let {goodsData} = this.props;
|
||||
return (
|
||||
<div className='good-list'>
|
||||
{goodsData.map((good, index) =>
|
||||
<Card
|
||||
key={`good${index}`}
|
||||
hoverable
|
||||
cover={<img alt="example" src={good.img}/>}
|
||||
actions={[<Button type='primary' onClick={this.handleClick.bind(this, good)}>购买</Button>]}
|
||||
>
|
||||
<Meta
|
||||
avatar={<Avatar src={logo}/>}
|
||||
title={<h3>{good.name}</h3>}
|
||||
description={`¥${good.price.toFixed(2)}`}
|
||||
/>
|
||||
</Card>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
.good-list{
|
||||
display:grid;
|
||||
grid-column-gap: 26px;
|
||||
grid-row-gap: 46px;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
.ant-card-cover{
|
||||
img {
|
||||
margin: 0 auto;
|
||||
width: 213px;
|
||||
height: 216px;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Layout} from 'antd'
|
||||
|
||||
const {Footer} = Layout;
|
||||
|
||||
export default class HomeFooter extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
// 初始状态
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Footer style={{textAlign: 'center'}}>
|
||||
{this.props.content}
|
||||
</Footer>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,106 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Layout, Avatar, Menu, Dropdown, message} from 'antd'
|
||||
import {post} from '../../fetch/post'
|
||||
import sessionStorage from '../../until/sessionStorage'
|
||||
import LoginModal from '../LoginModal/LoginModal'
|
||||
import AboutUsModal from '../AboutUsModal/AboutUsModal'
|
||||
import * as domainConstants from '../../fetch/domainConstants'
|
||||
import title from '../../static/imgs/campusCardTitle.png'
|
||||
import './style.less'
|
||||
import {hashHistory} from "react-router";
|
||||
import selectedKeyUntil from "../../until/selectedKeyUntil";
|
||||
|
||||
const {Header} = Layout;
|
||||
|
||||
export default class HomeHeader extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
// 初始状态
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
|
||||
//点击用户相关按钮
|
||||
static handleClick(e) {
|
||||
let {userInfo, userInfoActions, modalVisible, modalVisibleActions} = this.props;
|
||||
switch (e.key) {
|
||||
case 'login':
|
||||
if (!userInfo.isLogin) {
|
||||
modalVisible.loginVisible = true;
|
||||
modalVisibleActions.update(modalVisible);
|
||||
}
|
||||
break;
|
||||
case 'aboutUs':
|
||||
modalVisible.aboutUsVisible = true;
|
||||
modalVisibleActions.update(modalVisible);
|
||||
break;
|
||||
case 'logout':
|
||||
post('/logout', {}, (data) => {
|
||||
if (data.success) {
|
||||
let initUserInfo = {
|
||||
user: '',
|
||||
isLogin: false,
|
||||
isAdmin: false,
|
||||
code: ''
|
||||
};
|
||||
Object.assign(userInfo, initUserInfo);
|
||||
userInfoActions.update(userInfo);
|
||||
sessionStorage.setItem('userInfo', JSON.stringify(initUserInfo));
|
||||
message.success(data.msg);
|
||||
let url = hashHistory.getCurrentLocation().pathname;
|
||||
//退出登录时返回首页
|
||||
if (url !== '/') {
|
||||
let {menuKey, menuKeyActions} = this.props;
|
||||
selectedKeyUntil.update(menuKey, menuKeyActions, '/')
|
||||
}
|
||||
} else {
|
||||
message.error(data.msg);
|
||||
}
|
||||
}, () => {
|
||||
message.error(`退出登录异常`)
|
||||
});
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let {userInfo, modalVisible, userInfoActions, modalVisibleActions, menuKey, menuKeyActions} = this.props;
|
||||
const menu = (
|
||||
<Menu onClick={HomeHeader.handleClick.bind(this)}>
|
||||
<Menu.Item key="login">
|
||||
{userInfo.isLogin ? userInfo.user : '请登录'}
|
||||
</Menu.Item>
|
||||
<Menu.Item key="aboutUs">
|
||||
关于我们
|
||||
</Menu.Item>
|
||||
<Menu.Divider/>
|
||||
{userInfo.isLogin ? <Menu.Item key="logout">退出登录</Menu.Item> : ''}
|
||||
</Menu>
|
||||
);
|
||||
return (
|
||||
<Header className='header'>
|
||||
<img className='header_title' src={title} alt="校园一卡通平台"/>
|
||||
<Dropdown overlay={menu}>
|
||||
{!userInfo.isLogin ? <Avatar className='avatar float-right' size="large" icon="user"/> :
|
||||
userInfo.photo ? <Avatar className='avatar float-right' src={`${domainConstants.DOMAIN}/imgs/${userInfo.photo}`}/> :
|
||||
<Avatar className='avatar float-right' size="large" icon="user"
|
||||
style={{backgroundColor: '#1890FF'}}/>
|
||||
}
|
||||
|
||||
</Dropdown>
|
||||
<LoginModal modalVisible={modalVisible} modalVisibleActions={modalVisibleActions}
|
||||
userInfo={userInfo} userInfoActions={userInfoActions}
|
||||
menuKey={menuKey}
|
||||
menuKeyActions={menuKeyActions}/>
|
||||
<AboutUsModal modalVisible={modalVisible} modalVisibleActions={modalVisibleActions}/>
|
||||
</Header>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,17 @@
|
||||
.header {
|
||||
background: #fff;
|
||||
padding: 0;
|
||||
line-height: 64px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
.header_title {
|
||||
height: 60px;
|
||||
}
|
||||
.avatar {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 2%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {hashHistory} from 'react-router'
|
||||
import {Layout, Menu, Icon} from 'antd'
|
||||
import SessionStorage from '../../until/sessionStorage'
|
||||
import selectedKeyUntil from '../../until/selectedKeyUntil'
|
||||
import menuData from '../../viewDatas/menu'
|
||||
import logo from '../../static/imgs/logo.svg'
|
||||
import './style.less'
|
||||
|
||||
const SubMenu = Menu.SubMenu;
|
||||
const {Sider} = Layout;
|
||||
|
||||
export default class LeftAside extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
// 初始状态
|
||||
this.state = {
|
||||
collapsed: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
//从缓存中取selectedKey并设置
|
||||
let selectedKey = SessionStorage.getItem('selectedKey');
|
||||
let {menuKey, menuKeyActions} = this.props;
|
||||
if (selectedKey) {
|
||||
selectedKeyUntil.update(menuKey, menuKeyActions, selectedKey, false);
|
||||
} else {
|
||||
selectedKeyUntil.update(menuKey, menuKeyActions, '/');
|
||||
}
|
||||
}
|
||||
|
||||
//左侧菜单折叠
|
||||
onCollapse = (collapsed) => {
|
||||
this.setState({collapsed});
|
||||
};
|
||||
|
||||
//点击左侧菜单
|
||||
onSelectHandler = (arg) => {
|
||||
// { item, key, selectedKeys }z
|
||||
let {menuKey, menuKeyActions} = this.props;
|
||||
if (hashHistory.getCurrentLocation().pathname !== arg.key) {
|
||||
selectedKeyUntil.update(menuKey, menuKeyActions, arg.key);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
render() {
|
||||
let {menuKey, userInfo} = this.props,
|
||||
nav = menuData.map(item => {
|
||||
if (item.children && item.children.length) {
|
||||
switch (item.key) {
|
||||
case 'admin':
|
||||
if (userInfo.isAdmin) {
|
||||
return (
|
||||
<SubMenu
|
||||
key={item.key}
|
||||
title={<span><Icon type={item.icon}/><span>{item.title}</span></span>}
|
||||
>
|
||||
{item.children.map(subItem => {
|
||||
return (
|
||||
<Menu.Item key={subItem.key}>{subItem.title}</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</SubMenu>
|
||||
)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
case 'userCenter':
|
||||
if (!userInfo.isAdmin && userInfo.isLogin) {
|
||||
return (
|
||||
<SubMenu
|
||||
key={item.key}
|
||||
title={<span><Icon type={item.icon}/><span>{item.title}</span></span>}
|
||||
>
|
||||
{item.children.map(subItem => {
|
||||
return (
|
||||
<Menu.Item key={subItem.key}>{subItem.title}</Menu.Item>
|
||||
);
|
||||
})}
|
||||
</SubMenu>
|
||||
)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return (<Menu.Item key={item.key}>
|
||||
<Icon type={item.icon}/>
|
||||
<span>{item.title}</span>
|
||||
</Menu.Item>)
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Sider
|
||||
collapsible
|
||||
collapsed={this.state.collapsed}
|
||||
onCollapse={this.onCollapse}
|
||||
>
|
||||
<div className="logo">
|
||||
<img src={logo} alt="logo"/>
|
||||
</div>
|
||||
<Menu className='left-aside' theme="dark" defaultOpenKeys={['admin', 'userCenter']}
|
||||
selectedKeys={[menuKey.selectedKey]}
|
||||
mode="inline" onSelect={this.onSelectHandler}>
|
||||
{nav}
|
||||
</Menu>
|
||||
</Sider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,13 @@
|
||||
.ant-layout-sider-children > .logo {
|
||||
height: 32px;
|
||||
background: rgba(255, 255, 255, .2);
|
||||
margin: 16px;
|
||||
color: #fff;
|
||||
img {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.left-aside {
|
||||
margin-bottom: 60px;
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
import React from 'react'
|
||||
import {Form, Icon, Input, Modal, Button, message} from 'antd';
|
||||
import {post} from "../../fetch/post";
|
||||
import sessionStorage from '../../until/sessionStorage'
|
||||
import selectedKeyUntil from '../../until/selectedKeyUntil'
|
||||
|
||||
import './style.less'
|
||||
import {hashHistory} from "react-router";
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
function hasErrors(fieldsError) {
|
||||
return Object.keys(fieldsError).some(field => fieldsError[field]);
|
||||
}
|
||||
|
||||
class LoginModal extends React.Component {
|
||||
// 构造
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// 初始状态
|
||||
this.state = {
|
||||
loading: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// 初始化经禁用登录按钮
|
||||
this.props.form.validateFields();
|
||||
}
|
||||
|
||||
//登录
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
let {modalVisible, modalVisibleActions} = this.props;
|
||||
this.props.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
this.setState({loading: true});
|
||||
let code = this.props.form.getFieldValue('code'),
|
||||
url = code === 'admin' ? '/admin/login' : '/card/login';
|
||||
//发送请求
|
||||
post(url, values,
|
||||
(data) => {
|
||||
if (data.success) {
|
||||
this.loginHandle(data);
|
||||
} else {
|
||||
message.error(data.msg);
|
||||
this.setState({loading: false});
|
||||
}
|
||||
},
|
||||
() => {
|
||||
//更新弹窗状态
|
||||
modalVisible.loginVisible = false;
|
||||
modalVisibleActions.update(modalVisible);
|
||||
//更新按钮状态
|
||||
this.setState({loading: false});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
loginHandle(data) {
|
||||
let {modalVisible, modalVisibleActions, userInfo, userInfoActions, menuKey, menuKeyActions} = this.props;
|
||||
//更新弹窗状态
|
||||
modalVisible.loginVisible = false;
|
||||
modalVisibleActions.update(modalVisible);
|
||||
//更新用户信息
|
||||
userInfo.code = data.backData.code;
|
||||
userInfo.isAdmin = data.backData.isAdmin;
|
||||
userInfo.user = data.backData.cardholder || 'admin';
|
||||
userInfo.isLogin = true;
|
||||
userInfo.photo = data.backData.photo;
|
||||
userInfoActions.update(userInfo);
|
||||
//将用户信息存储到sessionStorage
|
||||
sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
|
||||
message.success(data.msg);
|
||||
//更新按钮状态
|
||||
this.setState({loading: false});
|
||||
//跳转至首页
|
||||
let url = hashHistory.getCurrentLocation().pathname;
|
||||
if ((userInfo.isAdmin && url.indexOf('userCenter') > -1) ||
|
||||
(!userInfo.isAdmin && url.indexOf('admin') > -1)) {
|
||||
selectedKeyUntil.update(menuKey, menuKeyActions, '/')
|
||||
}
|
||||
}
|
||||
|
||||
//关闭弹窗
|
||||
handleCancel = () => {
|
||||
let {modalVisible, modalVisibleActions} = this.props;
|
||||
modalVisible.loginVisible = false;
|
||||
modalVisibleActions.update(modalVisible);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {modalVisible} = this.props;
|
||||
const {getFieldDecorator, getFieldsError, getFieldError, isFieldTouched} = this.props.form;
|
||||
// Only show error after a field is touched.
|
||||
const codeError = isFieldTouched('code') && getFieldError('code');
|
||||
const passwordError = isFieldTouched('password') && getFieldError('password');
|
||||
const {loading} = this.state;
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
visible={modalVisible.loginVisible}
|
||||
title="登录"
|
||||
onOk={this.handleOk}
|
||||
onCancel={this.handleCancel}
|
||||
footer=''
|
||||
>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<FormItem
|
||||
validateStatus={codeError ? 'error' : ''}
|
||||
help={codeError || ''}
|
||||
>
|
||||
{getFieldDecorator('code', {
|
||||
rules: [{required: true, message: '请输入一卡通账号!'}],
|
||||
})(
|
||||
<Input prefix={<Icon type="user" style={{color: 'rgba(0,0,0,.25)'}}/>}
|
||||
placeholder="一卡通账号"/>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
validateStatus={passwordError ? 'error' : ''}
|
||||
help={passwordError || ''}
|
||||
>
|
||||
{getFieldDecorator('password', {
|
||||
rules: [{required: true, message: '请输入一卡通密码!'}],
|
||||
})(
|
||||
<Input prefix={<Icon type="lock" style={{color: 'rgba(0,0,0,.25)'}}/>} type="password"
|
||||
placeholder="一卡通密码"/>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Button className='float-right login-button'
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={hasErrors(getFieldsError())}
|
||||
loading={loading}
|
||||
>
|
||||
登 录
|
||||
</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LoginModal = Form.create({})(LoginModal);
|
@ -0,0 +1,3 @@
|
||||
.login-button{
|
||||
width: 100%;
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Collapse} from 'antd';
|
||||
import moment from 'moment'
|
||||
import listItem from '../../static/imgs/listItem.svg'
|
||||
import './style.less'
|
||||
|
||||
const Panel = Collapse.Panel;
|
||||
|
||||
export default class NoticeDetailDisplay extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
// 初始状态
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
let {data} = this.props;
|
||||
return (
|
||||
<Collapse className='home-notice-detail' bordered={false}>
|
||||
{data.map(item =>
|
||||
<Panel id={item._id} key={item._id}
|
||||
header={<div className='notice-item'><span className='notice-title'>
|
||||
<img src={listItem} alt="{listItem}"/>{item.title}</span>
|
||||
<span
|
||||
className='notice-extra'>{item.createPerson} 发布于 {moment(item.meta.updateAt).format('YYYY-MM-DD HH:mm')}
|
||||
</span></div>}>
|
||||
<div className='notice-detail-content'>{item.content}</div>
|
||||
</Panel>)}
|
||||
</Collapse>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
.home-notice-detail {
|
||||
.ant-collapse-item {
|
||||
.ant-collapse-header {
|
||||
padding: 16px 0 2px 40px;
|
||||
}
|
||||
}
|
||||
.notice-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.notice-title {
|
||||
display: inline-block;
|
||||
width: 600px;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
.notice-detail-content {
|
||||
width: 900px;
|
||||
margin: 20px auto;
|
||||
font-size: 18px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
import React from 'react'
|
||||
import {Form, Input, Button, Switch} from 'antd';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const {TextArea} = Input;
|
||||
|
||||
class NoticeDetailInfoForm extends React.Component {
|
||||
// 构造
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// 初始状态
|
||||
this.state = {
|
||||
type: '新增'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.data.title !== this.props.data.title) {
|
||||
if (nextProps.data && JSON.stringify(nextProps.data) !== '{}') {
|
||||
let value = this.props.form.getFieldsValue();
|
||||
for (let key in value) {
|
||||
if (value.hasOwnProperty(key)) {
|
||||
value[key] = nextProps.data[key]
|
||||
}
|
||||
}
|
||||
this.setState({type: '更新'});
|
||||
this.props.form.setFieldsValue(value)
|
||||
} else {
|
||||
this.props.form.resetFields()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.form.validateFieldsAndScroll((err, values) => {
|
||||
if (!err) {
|
||||
if (this.props.data._id) {
|
||||
values._id = this.props.data._id;
|
||||
this.props.updateNotice(values)
|
||||
} else {
|
||||
this.props.newNotice(values)
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {getFieldDecorator} = this.props.form;
|
||||
const {type} = this.state;
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 3},
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 12},
|
||||
},
|
||||
};
|
||||
const tailFormItemLayout = {
|
||||
wrapperCol: {
|
||||
xs: {
|
||||
span: 24,
|
||||
offset: 0,
|
||||
},
|
||||
sm: {
|
||||
span: 16,
|
||||
offset: 8,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label={(
|
||||
<span>
|
||||
文章标题
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
{getFieldDecorator('title', {
|
||||
rules: [{
|
||||
required: true, message: '文章标题不能为空!',
|
||||
}],
|
||||
})(
|
||||
<Input/>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem {...formItemLayout} label={(<span>公告正文 </span>)}>
|
||||
{getFieldDecorator('content', {
|
||||
rules: [{required: true, message: '公告正文不能为空!', whitespace: true}],
|
||||
})(
|
||||
<TextArea placeholder="请输入公告的具体信息" autosize={{minRows: 4, maxRows: 20}}/>
|
||||
)}
|
||||
</FormItem>
|
||||
<FormItem
|
||||
{...formItemLayout}
|
||||
label={(<span>是否显示 </span>)}>
|
||||
{getFieldDecorator('isShow', {
|
||||
rules: [{
|
||||
required: true, message: '该项为必填!',
|
||||
}],
|
||||
initialValue: true
|
||||
})(
|
||||
<Switch checkedChildren="是" unCheckedChildren="否" defaultChecked/>
|
||||
)}
|
||||
</FormItem>
|
||||
|
||||
<FormItem {...tailFormItemLayout}>
|
||||
<Button type="primary" htmlType="submit">{type}</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default NoticeDetailInfoForm = Form.create({})(NoticeDetailInfoForm);
|
@ -0,0 +1,121 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import moment from 'moment'
|
||||
import {Table, Menu, Dropdown, Icon, Popconfirm} from 'antd';
|
||||
import selectedKeyUntil from "../../until/selectedKeyUntil";
|
||||
// import {hashHistory} from 'react-router'
|
||||
|
||||
export default class NoticeListTable extends React.Component {
|
||||
// 构造
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
// 初始状态
|
||||
this.state = {
|
||||
pagination: {},
|
||||
};
|
||||
}
|
||||
|
||||
//渲染行操作按钮
|
||||
renderOperate(text, record, index) {
|
||||
return <Dropdown
|
||||
overlay={<Menu onClick={this.handleOperate.bind(this, text, record, index)}>
|
||||
<Menu.Item key='delete'>
|
||||
<Popconfirm title={`确定删除《${record.title}》吗?`}
|
||||
onConfirm={this.confirm.bind(this, text, record, index)}
|
||||
onCancel={this.cancel} okText="Yes" cancelText="No">
|
||||
<Icon type="delete"/> 删除
|
||||
</Popconfirm>
|
||||
</Menu.Item>
|
||||
<Menu.Item key='update'>
|
||||
<Icon type="edit"/> 编辑
|
||||
</Menu.Item>
|
||||
<Menu.Item key='show'>
|
||||
<Icon type="eye"/> {record.isShow ? '取消显示' : '显示'}
|
||||
</Menu.Item>
|
||||
</Menu>}>
|
||||
<span style={{color: '#1890FF', cursor: 'pointer'}}>
|
||||
操作 <Icon type="down"/>
|
||||
</span>
|
||||
</Dropdown>
|
||||
}
|
||||
|
||||
//表格分页
|
||||
handleTableChange = (pagination, filters, sorter) => {
|
||||
|
||||
};
|
||||
|
||||
//行操作按钮绑定事件
|
||||
handleOperate(text, record, index, e) {
|
||||
switch (e.key) {
|
||||
case'update':
|
||||
this.routeTo(record, `admin/newNotice`);
|
||||
break;
|
||||
case 'show':
|
||||
this.props.isShowHandler(record._id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//确定删除
|
||||
confirm(text, record, index, e) {
|
||||
this.props.delNotice(record._id);
|
||||
}
|
||||
|
||||
//取消删除卡片
|
||||
cancel() {
|
||||
|
||||
}
|
||||
|
||||
//跳转到详情页面
|
||||
routeTo(record, url) {
|
||||
let {menuKey, menuKeyActions} = this.props;
|
||||
selectedKeyUntil.update(menuKey, menuKeyActions, url, true, {_id: record._id})
|
||||
}
|
||||
|
||||
render() {
|
||||
const columns = [
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
width: 500,
|
||||
render: (text, record, index) => <a
|
||||
onClick={this.routeTo.bind(this, record, `admin/newNotice`)}>{text}</a>,
|
||||
}, {
|
||||
title: '发布人',
|
||||
dataIndex: 'createPerson',
|
||||
}, {
|
||||
title: '是否显示',
|
||||
dataIndex: 'isShow',
|
||||
render: (text, record, index) => text ? '是' : '否'
|
||||
|
||||
}, {
|
||||
title: '创建时间',
|
||||
dataIndex: 'meta.createAt',
|
||||
// sorter: (a, b) => a['meta.createAt'] - b['meta.createAt'],
|
||||
render: (text, record, index) => moment(text).format('YYYY-MM-DD HH:mm')
|
||||
|
||||
}, {
|
||||
title: '更新时间',
|
||||
dataIndex: 'meta.updateAt',
|
||||
// sorter: (a, b) => a['meta.updateAt'] - b['meta.updateAt'],
|
||||
render: (text, record, index) => moment(text).format('YYYY-MM-DD HH:mm')
|
||||
}, {
|
||||
title: '操作',
|
||||
dataIndex: 'operate',
|
||||
fixed: 'right',
|
||||
width: 100,
|
||||
render: this.renderOperate.bind(this)
|
||||
}
|
||||
];
|
||||
return <Table columns={columns}
|
||||
rowKey={record => record._id}
|
||||
dataSource={this.props.data}
|
||||
// pagination={this.state.pagination}
|
||||
onChange={this.handleTableChange}
|
||||
className='movie-table-list'
|
||||
/>
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Form, Input, Icon} from 'antd';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
class SearchByCodeInput extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
|
||||
// 初始状态
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
//同步信息
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
this.props.init(values.searchCode)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//处理回车按键
|
||||
search(e) {
|
||||
if (e.keyCode === 13) {
|
||||
this.handleSubmit(e)
|
||||
}
|
||||
}
|
||||
|
||||
keyPress() {
|
||||
let searchCode = this.props.form.getFieldValue('searchCode');
|
||||
this.props.changeCode && this.props.changeCode(searchCode);
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const {getFieldDecorator} = this.props.form;
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 3},
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 6},
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Form>
|
||||
<FormItem label="输入一卡通账号查询"
|
||||
{...formItemLayout}>
|
||||
{getFieldDecorator('searchCode', {
|
||||
rules: [{
|
||||
required: true, message: '请输入一卡通账号!',
|
||||
}, {
|
||||
validator(rule, values, callback) {
|
||||
if (values && values.length > 0) {
|
||||
let reg = /^[0-9]*$/g;
|
||||
if (!reg.test(values)) {
|
||||
callback(`id只能是数字`);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}],
|
||||
})(
|
||||
<Input onKeyUp={this.keyPress.bind(this)}
|
||||
prefix={<Icon type="link" style={{color: 'rgba(0,0,0,.25)'}}/>}
|
||||
placeholder="输入一卡通账号后按下回车键即查询" onKeyDown={this.search.bind(this)}/>
|
||||
)}
|
||||
</FormItem>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchByCodeInput = Form.create({})(SearchByCodeInput);
|
||||
|
@ -0,0 +1,67 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Form, Input} from 'antd';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const Search = Input.Search;
|
||||
|
||||
class SearchNoticeInput extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
|
||||
// 初始状态
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
//同步信息
|
||||
handleSubmit = (e) => {
|
||||
// e.preventDefault();
|
||||
this.props.form.validateFields((err, values) => {
|
||||
if (!err) {
|
||||
console.log(values);
|
||||
// this.props.init(values.searchCode)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
//处理回车按键
|
||||
search(e) {
|
||||
if (e.keyCode === 13) {
|
||||
this.handleSubmit(e)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {getFieldDecorator} = this.props.form;
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 3},
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 6},
|
||||
},
|
||||
};
|
||||
return (
|
||||
<Form>
|
||||
<FormItem label="输入文章标题或主体内容查询"
|
||||
{...formItemLayout}>
|
||||
{getFieldDecorator('searchCode', {
|
||||
rules: [{
|
||||
required: true, message: '该项不能为空!',
|
||||
}],
|
||||
})(
|
||||
<Search placeholder="input search text" enterButton="搜索"
|
||||
onSearch={this.handleSubmit}/>
|
||||
)}
|
||||
</FormItem>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchNoticeInput = Form.create({})(SearchNoticeInput);
|
||||
|
@ -0,0 +1,2 @@
|
||||
// 定义常量方便维护
|
||||
export const MENU_KEY_UPDATE = 'MENU_KEY_UPDATE';
|
@ -0,0 +1,2 @@
|
||||
// 定义常量方便维护
|
||||
export const MODAL_VISIBLE_UPDATE = 'MODAL_VISIBLE_UPDATE';
|
@ -0,0 +1,2 @@
|
||||
// 定义常量方便维护
|
||||
export const USER_INFO_UPDATE = 'USER_INFO_UPDATE';
|
@ -0,0 +1,88 @@
|
||||
import React, {Component} from 'react';
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Layout, BackTop} from 'antd'
|
||||
import moment from 'moment';
|
||||
import 'moment/locale/zh-cn';
|
||||
import {connect} from 'react-redux'
|
||||
import {bindActionCreators} from 'redux'
|
||||
import * as userInfoActionsFromOtherFile from '../actions/userInfo'
|
||||
import * as menuKeyActionsFromOtherFile from '../actions/menuKey'
|
||||
import * as modalVisibleActionsFromOtherFile from '../actions/modalVisible'
|
||||
import sessionStorage from '../until/sessionStorage'
|
||||
import LeftAside from "../components/LeftAside/LeftAside";
|
||||
import HomeHeader from '../components/HomeHeader/HomeHeader'
|
||||
import HomeFooter from "../components/HomeFooter/HomeFooter";
|
||||
import HomeBreadcrumb from "../components/HomeBreadcrumb/HomeBreadcrumb";
|
||||
import rocket from '../static/imgs/火箭.svg'
|
||||
|
||||
moment.locale('zh-cn');
|
||||
const {Content} = Layout;
|
||||
const layoutStyle = {minHeight: '100vh'},
|
||||
ContentStyle = {margin: '0 16px'},
|
||||
ContentDivStyle = {padding: 24, background: '#fff', minHeight: 360};
|
||||
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
export default class App extends Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
// 初始状态
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
let sessionUserInfo = JSON.parse(sessionStorage.getItem('userInfo'));
|
||||
//更新userInfo
|
||||
if (sessionUserInfo) {
|
||||
let {userInfo, userInfoActions} = this.props;
|
||||
Object.assign(userInfo, sessionUserInfo);
|
||||
userInfoActions.update(userInfo);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let {children, userInfo, modalVisible, userInfoActions, modalVisibleActions, menuKey, menuKeyActions} = this.props;
|
||||
return (
|
||||
<Layout style={layoutStyle}>
|
||||
<LeftAside userInfo={userInfo} menuKey={menuKey}
|
||||
menuKeyActions={menuKeyActions}
|
||||
style={{ overflow: 'auto', height: '100vh', position: 'fixed', left: 0 }}
|
||||
/>
|
||||
<Layout>
|
||||
<HomeHeader userInfo={userInfo} userInfoActions={userInfoActions}
|
||||
modalVisible={modalVisible}
|
||||
modalVisibleActions={modalVisibleActions} menuKey={menuKey}
|
||||
menuKeyActions={menuKeyActions}/>
|
||||
<Content style={ContentStyle}>
|
||||
<HomeBreadcrumb menuKey={menuKey}/>
|
||||
<div style={ContentDivStyle}>
|
||||
{children}
|
||||
</div>
|
||||
</Content>
|
||||
<HomeFooter content='Campus Card System ©2018 Created by Belief_RC'/>
|
||||
</Layout>
|
||||
<BackTop>
|
||||
<img src={rocket} alt="BackTop"/>
|
||||
</BackTop>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
userInfo: state.userInfo,
|
||||
menuKey: state.menuKey,
|
||||
modalVisible: state.modalVisible
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
userInfoActions: bindActionCreators(userInfoActionsFromOtherFile, dispatch),
|
||||
menuKeyActions: bindActionCreators(menuKeyActionsFromOtherFile, dispatch),
|
||||
modalVisibleActions: bindActionCreators(modalVisibleActionsFromOtherFile, dispatch)
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Tooltip} from 'antd';
|
||||
import NotFoundLogo from '../static/imgs/404.svg'
|
||||
import selectedKeyUntil from "../until/selectedKeyUntil";
|
||||
import * as menuKeyActionsFromOtherFile from "../actions/menuKey";
|
||||
import {bindActionCreators} from "redux";
|
||||
import {connect} from "react-redux";
|
||||
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
export default class NotFound extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
// 初始状态
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
let {menuKey, menuKeyActions} = this.props;
|
||||
selectedKeyUntil.update(menuKey, menuKeyActions, '/')
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{textAlign: 'center', marginTop: '20px'}}>
|
||||
<h1>404 Not Found Page</h1>
|
||||
|
||||
<Tooltip placement="bottom" title="点击图片返回首页">
|
||||
<img src={NotFoundLogo} alt="NotFoundLogo" onClick={this.handleClick.bind(this)}
|
||||
style={{cursor: 'pointer'}}/>
|
||||
</Tooltip>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
menuKey: state.menuKey,
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
menuKeyActions: bindActionCreators(menuKeyActionsFromOtherFile, dispatch),
|
||||
}
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Tabs, Icon, Spin, message, DatePicker, Row, Alert} from 'antd'
|
||||
import {connect} from 'react-redux'
|
||||
import moment from 'moment'
|
||||
import SearchByCodeInput from '../../components/SearchByCodeInput/SearchByCodeInput'
|
||||
import {post} from "../../fetch/post";
|
||||
import BillListTable from '../../components/BillListTable/BillListTable'
|
||||
import BillCalendar from '../../components/BillCalendar/BillCalendar'
|
||||
import DataAnalysis from '../../components/DataAnalysis/DataAnalysis'
|
||||
|
||||
const TabPane = Tabs.TabPane;
|
||||
const {RangePicker} = DatePicker;
|
||||
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
export default class BillListPage extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
|
||||
// 初始状态
|
||||
this.state = {
|
||||
activeKey: '1',
|
||||
loading: false,
|
||||
code: '',
|
||||
data: [],
|
||||
value: []
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
value: [moment().subtract(3, 'days'), moment()]
|
||||
});
|
||||
let {userInfo} = this.props;
|
||||
if (userInfo.isLogin && !userInfo.isAdmin && userInfo.code) {
|
||||
this.setState({code: userInfo.code});
|
||||
} else {
|
||||
let code = this.props.location.state ? this.props.location.state.code : null;
|
||||
if (code) {
|
||||
this.setState({code});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let {code, value, activeKey} = this.state;
|
||||
//时间不存在则不查询
|
||||
if (!code || value.length === 0 || activeKey !== '1') {
|
||||
return
|
||||
}
|
||||
this.init(code, value)
|
||||
}
|
||||
|
||||
tabOnChange(key) {
|
||||
this.setState({activeKey: key})
|
||||
}
|
||||
|
||||
onChange(date, dateString) {
|
||||
console.log(moment().subtract(3, 'years'));
|
||||
this.setState({value: date}, () => {
|
||||
let {code, value, activeKey} = this.state;
|
||||
//时间不存在则不查询
|
||||
if (value.length === 0 || activeKey !== '1') {
|
||||
return
|
||||
}
|
||||
console.log(value);
|
||||
this.init(code, value)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
disabledDate(current) {
|
||||
return current && current > moment().endOf('day');
|
||||
}
|
||||
|
||||
|
||||
//初始化(获取出卡人信息)
|
||||
init(code, value = this.state.value) {
|
||||
this.setState({loading: true});
|
||||
post('/card/billList', {code, date: value}, (data) => {
|
||||
this.setState({loading: false});
|
||||
if (data.success) {
|
||||
delete data.backData._id;
|
||||
this.setState({
|
||||
data: data.backData
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
data: {}
|
||||
});
|
||||
message.error(data.msg)
|
||||
}
|
||||
}, () => {
|
||||
this.setState({loading: false});
|
||||
});
|
||||
}
|
||||
|
||||
changeCode(code) {
|
||||
this.setState({code})
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
let {userInfo} = this.props;
|
||||
let {loading, data, value, activeKey, code} = this.state;
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
{userInfo.isAdmin ? <SearchByCodeInput init={this.init.bind(this)} code={code}
|
||||
changeCode={this.changeCode.bind(this)}/> : ''}
|
||||
<Tabs activeKey={activeKey} onChange={this.tabOnChange.bind(this)}>
|
||||
<TabPane tab={<span><Icon type="table"/>表格</span>} key="1">
|
||||
<Row>
|
||||
<RangePicker value={value}
|
||||
format="YYYY-MM-DD"
|
||||
onChange={this.onChange.bind(this)}
|
||||
disabledDate={this.disabledDate.bind(this)}
|
||||
ranges={{
|
||||
'今天': [moment(), moment()],
|
||||
'本星期': [moment().startOf('week'), moment()],
|
||||
'本月': [moment().startOf('month'), moment()]
|
||||
}}/>
|
||||
</Row>
|
||||
<Alert style={{margin: '10px 0 10px 0'}}
|
||||
message={`当前余额:¥${data.balance ? data.balance.toFixed(2) : 0.00}`} type="warning"
|
||||
showIcon/>
|
||||
<BillListTable data={data.bills}/>
|
||||
</TabPane>
|
||||
<TabPane tab={<span><Icon type="calendar"/>日历</span>} key="2">
|
||||
<BillCalendar userInfo={userInfo}/>
|
||||
</TabPane>
|
||||
<TabPane tab={<span><Icon type="line-chart"/>数据分析</span>} key="3">
|
||||
<DataAnalysis userInfo={userInfo}/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
userInfo: state.userInfo,
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {}
|
||||
}
|
||||
|
@ -0,0 +1,76 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import CardListTable from '../../components/CardListTable/CardListTable'
|
||||
import {get} from "../../fetch/get";
|
||||
import {Spin, message} from "antd";
|
||||
import {bindActionCreators} from "redux";
|
||||
import * as menuKeyActionsFromOtherFile from "../../actions/menuKey";
|
||||
import {connect} from "react-redux";
|
||||
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
|
||||
export default class CardholderListPage extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
|
||||
// 初始状态
|
||||
this.state = {
|
||||
loading: false,
|
||||
data: []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getDataSource()
|
||||
}
|
||||
|
||||
//获取表格数据
|
||||
getDataSource() {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
let _this = this;
|
||||
get('/card/list', {}, (data) => {
|
||||
if (data.success) {
|
||||
_this.setState({
|
||||
data: data.backData
|
||||
})
|
||||
} else {
|
||||
message.error(data.msg)
|
||||
}
|
||||
_this.setState({
|
||||
loading: false
|
||||
});
|
||||
}, () => {
|
||||
message.error('获取持卡人列表失败');
|
||||
_this.setState({
|
||||
loading: false
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
let {loading, data} = this.state;
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<CardListTable menuKey={this.props.menuKey} menuKeyActions={this.props.menuKeyActions} data={data}
|
||||
getDataSource={this.getDataSource.bind(this)}/>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
menuKey: state.menuKey,
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
menuKeyActions: bindActionCreators(menuKeyActionsFromOtherFile, dispatch),
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,79 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {get} from '../../fetch/get'
|
||||
import {post} from '../../fetch/post'
|
||||
import {Form, Input, Button, Spin, message} from 'antd';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const {TextArea} = Input;
|
||||
|
||||
class EditInstruction extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
|
||||
// 初始状态
|
||||
this.state = {loading: false};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({loading: true});
|
||||
get('/instruction/detail', {}, (data) => {
|
||||
this.setState({loading: false});
|
||||
if (data.success) {
|
||||
this.props.form.setFieldsValue({instruction: data.backData.instruction})
|
||||
} else {
|
||||
message.error(data.msg)
|
||||
}
|
||||
}, () => {
|
||||
this.setState({loading: false});
|
||||
})
|
||||
}
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.form.validateFieldsAndScroll((err, values) => {
|
||||
if (!err) {
|
||||
this.setState({loading: true});
|
||||
post('/instruction/save', values, (data) => {
|
||||
this.setState({loading: false});
|
||||
if (data.success) {
|
||||
message.success(data.msg)
|
||||
} else {
|
||||
message.error(data.msg)
|
||||
}
|
||||
}, () => {
|
||||
this.setState({loading: false});
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {getFieldDecorator} = this.props.form;
|
||||
let {loading} = this.state;
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
|
||||
<FormItem label={(<span>使用说明 </span>)}>
|
||||
{getFieldDecorator('instruction', {
|
||||
rules: [{required: true, message: '使用说明不能为空!', whitespace: true}],
|
||||
})(
|
||||
<TextArea placeholder="请输入使用说明具体信息" autosize={{minRows: 4, maxRows: 30}}/>
|
||||
)}
|
||||
</FormItem>
|
||||
|
||||
<FormItem>
|
||||
<Button type="primary" htmlType="submit">更新</Button>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default EditInstruction = Form.create({})(EditInstruction);
|
||||
|
@ -0,0 +1,75 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Tabs, Icon, Spin, message} from 'antd'
|
||||
import FileListTable from '../../components/FileListTable/FileListTable'
|
||||
import FileUpload from '../../components/FileUpload/FileUpload'
|
||||
import {get} from '../../fetch/get'
|
||||
import {post} from '../../fetch/post'
|
||||
|
||||
const TabPane = Tabs.TabPane;
|
||||
|
||||
export default class ExcelManagementPage extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
// 初始状态
|
||||
this.state = {loading: false, data: []};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.getFileList()
|
||||
}
|
||||
|
||||
getFileList() {
|
||||
get('/file/fileList', {}, (data) => {
|
||||
this.setState({loading: false});
|
||||
if (data.success) {
|
||||
this.setState({
|
||||
data: data.backData
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
data: {}
|
||||
});
|
||||
message.error(data.msg)
|
||||
}
|
||||
}, () => {
|
||||
this.setState({loading: false});
|
||||
});
|
||||
}
|
||||
|
||||
delFile(_id) {
|
||||
post(`/file/delete`, {_id}, (data) => {
|
||||
if (data.success) {
|
||||
message.success(data.msg);
|
||||
this.getFileList();
|
||||
} else {
|
||||
message.error(data.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//切换面板式查询文件列表
|
||||
onChangeHandle(key) {
|
||||
if (key === '1') {
|
||||
this.getFileList();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {loading, data} = this.state;
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<Tabs defaultActiveKey='1' onChange={this.onChangeHandle.bind(this)}>
|
||||
<TabPane tab={<span><Icon type="folder"/>文件管理</span>} key="1">
|
||||
<FileListTable data={data} delFile={this.delFile.bind(this)}/>
|
||||
</TabPane>
|
||||
<TabPane tab={<span><Icon type="file-add"/>新增文件</span>} key="2">
|
||||
<div><FileUpload/></div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {message, Spin} from "antd";
|
||||
import FileList from '../../components/FileList/FileList'
|
||||
import {get} from "../../fetch/get";
|
||||
export default class ExcelPage extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
|
||||
// 初始状态
|
||||
this.state = {loading: false, data: []};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.getFileList()
|
||||
}
|
||||
|
||||
getFileList() {
|
||||
get('/file/fileList', {}, (data) => {
|
||||
this.setState({loading: false});
|
||||
if (data.success) {
|
||||
this.setState({
|
||||
data: data.backData
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
data: {}
|
||||
});
|
||||
message.error(data.msg)
|
||||
}
|
||||
}, () => {
|
||||
this.setState({loading: false});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {loading, data} = this.state;
|
||||
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<FileList data={data} />
|
||||
</Spin>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
import React from 'react'
|
||||
import PureRenderMixin from 'react-addons-pure-render-mixin'
|
||||
import {Spin, message} from "antd";
|
||||
import {get} from "../../fetch/get";
|
||||
import './style.less'
|
||||
|
||||
export default class InstructionPage extends React.Component {
|
||||
// 构造
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
|
||||
|
||||
// 初始状态
|
||||
this.state = {
|
||||
loading: false,
|
||||
instruction: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({loading: true});
|
||||
get('/instruction/detail', {}, (data) => {
|
||||
this.setState({loading: false});
|
||||
if (data.success) {
|
||||
this.setState({
|
||||
instruction: data.backData.instruction
|
||||
})
|
||||
} else {
|
||||
message.error(data.msg)
|
||||
}
|
||||
}, () => {
|
||||
this.setState({loading: false});
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
let {loading ,instruction} = this.state;
|
||||
return (
|
||||
<Spin spinning={loading}>
|
||||
<h1 className='instruction-title'>校园卡使用说明</h1>
|
||||
<div className='bg-img clear-fix'>
|
||||
<article className='instruction-content'>
|
||||
{instruction}
|
||||
</article>
|
||||
</div>
|
||||
</Spin>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|