20241113-a

This commit is contained in:
unknown
2024-11-13 09:05:45 +08:00
commit 3bcaa20bc4
186 changed files with 100169 additions and 0 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
.history
node_modules
build
dist
README.md

36
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
on:
push:
tags:
- 'v*.*.*'
pull_request:
branches:
- 'master'
jobs:
call_workflow_build_node:
uses: infrastructure/reusing_workflows/.github/workflows/build_node.yml@main
with:
node-version: '16.x'
dist-dir: 'dist'
artifact-name: 'artifact'
secrets: inherit
call_workflow_publish_docker:
uses: infrastructure/reusing_workflows/.github/workflows/publish_docker.yml@main
with:
artifact-name: 'artifact'
docker_context: '.'
dockerfile_path: './docker/Dockerfile'
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
secrets: inherit
needs: call_workflow_build_node
call_workflow_deploy_compose:
uses: infrastructure/reusing_workflows/.github/workflows/deploy_compose.yml@main
with:
stack_file_name: compose.admin.yml
env_file_name: ${{ github.event_name == 'pull_request' && '.env.dev' || '.env.base' }},.env.version
args: up -d
pull_images_first: true
secrets: inherit
needs: call_workflow_publish_docker

37
.gitignore vendored Normal file
View File

@@ -0,0 +1,37 @@
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
/dist
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
.idea/
*.iml
# Local History for Visual Studio Code
.history/
yarn.lock
.eslintcache

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
**/dist

6
.prettierrc.yaml Normal file
View File

@@ -0,0 +1,6 @@
trailingComma: 'es5'
tabWidth: 2
semi: true
singleQuote: true
bracketSpacing: true
endOfLine: 'auto'

7
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib",
"CodeGPT.Autocomplete.suggestionDelay": 0,
"CodeGPT.Autocomplete.maxTokens": 1000,
"CodeGPT.Autocomplete.provider": "Ollama - llama3:instruct",
"CodeGPT.apiKey": "Ollama"
}

8
README.md Normal file
View File

@@ -0,0 +1,8 @@
REACT_APP_BASE=/tianmuhu PUBLIC_URL=/tianmuhu/admin npm run build
docker build -t ccr.ccs.tencentyun.com/olms/admin:1.1-subpath-changzhou -f Dockerfile.subpath --build-arg BASE_PATH=/changzhou .
系统配置
系统LOGO
微信服务
考勤客户端下载路径

38
babel.config.json Normal file
View File

@@ -0,0 +1,38 @@
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1"
},
"useBuiltIns": "usage",
"corejs": "3.6.5"
}
],
[
"@babel/preset-react",
{
"runtime": "classic"
}
],
[
"@babel/preset-typescript",
{
"isTSX": true,
"allExtensions": true
}
]
],
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
]
}

106
config/env.js Normal file
View File

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

70
config/getHttpsConfig.js Normal file
View File

@@ -0,0 +1,70 @@
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const chalk = require('react-dev-utils/chalk');
const paths = require('./paths');
// Ensure the certificate and key provided are valid and if not
// throw an easy to debug error
function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
let encrypted;
try {
// publicEncrypt will throw an error with an invalid cert
encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
} catch (err) {
throw new Error(
`The certificate "${chalk.yellow(crtFile)}" is invalid.\n${
err.message
}`
);
}
try {
// privateDecrypt will throw an error with an invalid key
crypto.privateDecrypt(key, encrypted);
} catch (err) {
throw new Error(
`The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${
err.message
}`
);
}
}
// Read file and throw an error if it doesn't exist
function readEnvFile(file, type) {
if (!fs.existsSync(file)) {
throw new Error(
`You specified ${chalk.cyan(
type
)} in your env, but the file "${chalk.yellow(
file
)}" can't be found.`
);
}
return fs.readFileSync(file);
}
// Get the https config
// Return cert files if provided in env, otherwise just true or false
function getHttpsConfig() {
const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
const isHttps = HTTPS === 'true';
if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
const config = {
cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
};
validateKeyAndCerts({ ...config, keyFile, crtFile });
return config;
}
return isHttps;
}
module.exports = getHttpsConfig;

134
config/modules.js Normal file
View File

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

74
config/paths.js Normal file
View File

@@ -0,0 +1,74 @@
'use strict';
const path = require('path');
const fs = require('fs');
const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
// Make sure any symlinks in the project folder are resolved:
// https://github.com/facebook/create-react-app/issues/637
const appDirectory = fs.realpathSync(process.cwd());
const resolveApp = (relativePath) => path.resolve(appDirectory, relativePath);
// 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.
const publicUrlOrPath = getPublicUrlOrPath(
process.env.NODE_ENV === 'development',
require(resolveApp('package.json')).homepage,
process.env.PUBLIC_URL
);
const buildPath = process.env.BUILD_PATH || 'dist';
const moduleFileExtensions = [
'web.mjs',
'mjs',
'web.js',
'js',
'web.ts',
'ts',
'web.tsx',
'tsx',
'json',
'web.jsx',
'jsx',
];
// Resolve file paths in the same order as webpack
const resolveModule = (resolveFn, filePath) => {
const extension = moduleFileExtensions.find((extension) =>
fs.existsSync(resolveFn(`${filePath}.${extension}`))
);
if (extension) {
return resolveFn(`${filePath}.${extension}`);
}
return resolveFn(`${filePath}.js`);
};
// config after eject: we're in ./config/
module.exports = {
dotenv: resolveApp('.env'),
appPath: resolveApp('.'),
appBuild: resolveApp(buildPath),
appPublic: resolveApp('public'),
appHtml: resolveApp('public/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/index'),
appEditorJs: resolveModule(resolveApp, 'src/editor'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
appTsConfig: resolveApp('tsconfig.json'),
appJsConfig: resolveApp('jsconfig.json'),
yarnLockFile: resolveApp('yarn.lock'),
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
proxySetup: resolveApp('src/setupProxy.js'),
appNodeModules: resolveApp('node_modules'),
swSrc: resolveModule(resolveApp, 'src/service-worker'),
publicUrlOrPath,
};
module.exports.moduleFileExtensions = moduleFileExtensions;

35
config/pnpTs.js Normal file
View File

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

621
config/webpack.config.js Normal file
View File

@@ -0,0 +1,621 @@
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const PnpWebpackPlugin = require('pnp-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
// const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
const apiMocker = require('mocker-api');
const paths = require('./paths');
const modules = require('./modules');
const getClientEnvironment = require('./env');
// Source maps are resource heavy and can cause out of memory issue for large source files.
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
const webpackDevClientEntry = require.resolve(
'react-dev-utils/webpackHotDevClient'
);
const reactRefreshOverlayEntry = require.resolve(
'react-dev-utils/refreshOverlayInterop'
);
const imageInlineSizeLimit = parseInt(
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
);
// Get the path to the uncompiled service worker (if it exists).
const swSrc = paths.swSrc;
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
webpackConfig = function (webpackEnv) {
const isEnvDevelopment = webpackEnv === 'development';
const isEnvProduction = webpackEnv === 'production';
// Variable used for enabling profiling in Production
// passed into alias object. Uses a flag if passed into the build command
const isEnvProductionProfile =
isEnvProduction && process.argv.includes('--profile');
// We will provide `paths.publicUrlOrPath` 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.
// Get environment variables to inject into our app.
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
const shouldUseReactRefresh = true;//env.raw.FAST_REFRESH;
const getStyleLoaders = (cssOptions, preProcessor) => {
const loaders = [
isEnvDevelopment && require.resolve('style-loader'),
isEnvProduction && {
loader: MiniCssExtractPlugin.loader,
// css is located in `static/css`, use '../../' to locate index.html folder
// in production `paths.publicUrlOrPath` can be a relative path
options: paths.publicUrlOrPath.startsWith('.')
? { publicPath: '../../' }
: {},
},
{
loader: require.resolve('css-loader'),
options: cssOptions,
},
{
// Options for PostCSS as we reference these options twice
// Adds vendor prefixing based on your specified browser support in
// package.json
loader: require.resolve('postcss-loader'),
},
].filter(Boolean);
if (preProcessor) {
loaders.push(
{
loader: require.resolve('resolve-url-loader'),
options: {
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
root: paths.appSrc,
},
},
{
loader: require.resolve(preProcessor),
options: {
sourceMap: true,
},
}
);
}
return loaders;
};
return {
mode: isEnvProduction
? 'production'
: isEnvDevelopment && 'development',
context: path.resolve(__dirname, '../'),
entry: {
main:
isEnvDevelopment && !shouldUseReactRefresh
? [
// 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'),
//
// When using the experimental react-refresh integration,
// the webpack plugin takes care of injecting the dev client for us.
webpackDevClientEntry,
// 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.
]
: paths.appIndexJs
// 'editor.worker': 'monaco-editor/esm/vs/editor/editor.worker.js',
// 'json.worker': 'monaco-editor/esm/vs/language/json/json.worker',
// 'css.worker': 'monaco-editor/esm/vs/language/css/css.worker',
// 'html.worker': 'monaco-editor/esm/vs/language/html/html.worker',
// 'ts.worker': 'monaco-editor/esm/vs/language/typescript/ts.worker',
},
output: {
// The build folder.
path: isEnvProduction ? paths.appBuild : undefined,
// Add /* filename */ comments to generated require()s in the output.
pathinfo: isEnvDevelopment,
// There will be one main bundle, and one file per asynchronous chunk.
// In development, it does not produce real files.
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/[name].bundle.js',
// There are also additional JS chunk files if you use code splitting.
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
// 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.
// We inferred the "public path" (such as / or /my-project) from homepage.
publicPath: paths.publicUrlOrPath,
// Point sourcemap entries to original disk location (format as URL on Windows)
devtoolModuleFilenameTemplate: isEnvProduction
? (info) =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
((info) =>
path
.resolve(info.absoluteResourcePath)
.replace(/\\/g, '/')),
},
optimization: {
minimize: true,
minimizer: [
// This is only used in production mode
new TerserPlugin({
terserOptions: {
parse: {
// We want terser to parse ecma 8 code. However, we don't want it
// to apply any minification steps that turns valid ecma 5 code
// into invalid ecma 5 code. This is why the 'compress' and 'output'
// sections only apply transformations that are ecma 5 safe
// https://github.com/facebook/create-react-app/pull/4234
ecma: 8,
},
compress: {
ecma: 5,
warnings: false,
// Disabled because of an issue with Uglify breaking seemingly valid code:
// https://github.com/facebook/create-react-app/issues/2376
// Pending further investigation:
// https://github.com/mishoo/UglifyJS2/issues/2011
comparisons: false,
// Disabled because of an issue with Terser breaking valid code:
// https://github.com/facebook/create-react-app/issues/5250
// Pending further investigation:
// https://github.com/terser-js/terser/issues/120
inline: 2,
},
mangle: {
safari10: true,
},
// Added for profiling in devtools
// keep_classnames: isEnvProductionProfile,
// keep_fnames: isEnvProductionProfile,
output: {
ecma: 5,
comments: false,
// Turned on because emoji and regex is not minified properly using default
// https://github.com/facebook/create-react-app/issues/2488
ascii_only: true,
},
},
}),
// This is only used in production mode
new CssMinimizerPlugin({
minimizerOptions: {
preset: [
'default',
{ minifyFontValues: { removeQuotes: false } },
],
},
}),
],
// Automatically split vendor and commons
// https://twitter.com/wSokra/status/969633336732905474
// https://medium.com/webpack/webpack-4-code-splitting-chunk-graph-and-the-splitchunks-optimization-be739a861366
splitChunks: {
chunks: 'all',
name: false,
},
// Keep the runtime chunk separated to enable long term caching
// https://twitter.com/wSokra/status/969679223278505985
// https://github.com/facebook/create-react-app/issues/5358
runtimeChunk: {
name: (entrypoint) => `runtime-${entrypoint.name}`,
},
},
resolve: {
extensions: ['.tsx', '.ts', '.js', '.html', '.mjs'],
alias: {
'@': path.resolve(__dirname, '..', 'src'),
},
fallback: {
"util": false,
"path": false,
"stream": false,
"url": require.resolve('url')
},
conditionNames: ['require', 'browser', 'web'],
// This allows you to set a fallback for where webpack should look for modules.
// We placed these paths second because we want `node_modules` to "win"
// if there are any conflicts. This matches Node resolution mechanism.
// https://github.com/facebook/create-react-app/issues/253
modules: ['node_modules', paths.appNodeModules].concat(
modules.additionalModulePaths || []
),
plugins: [
// Adds support for installing with Plug'n'Play, leading to faster installs and adding
// guards against forgotten dependencies and such.
PnpWebpackPlugin,
// Prevents users from importing files from outside of src/ (or node_modules/).
// This often causes confusion because we only process files within src/ with babel.
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
// please link the files into your node_modules/ and let module-resolution kick in.
// Make sure your source files are compiled, as they will not be processed in any way.
// new ModuleScopePlugin(paths.appSrc, [
// paths.appPackageJson,
// reactRefreshOverlayEntry,
// ]),
],
},
resolveLoader: {
plugins: [
// Also related to Plug'n'Play, but this time it tells webpack to load its loaders
// from the current package.
PnpWebpackPlugin.moduleLoader(module),
],
},
module: {
rules: [
// Disable require.ensure as it's not a standard language feature.
// { parser: { requireEnsure: false } },
{
oneOf: [
// {
// test: /froala-editor/,
// parser: {
// amd: false,
// },
// },
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve('url-loader'),
options: {
limit: imageInlineSizeLimit,
name: 'static/media/[name].[hash:8].[ext]',
},
},
// Process application JS with Babel.
// The preset includes JSX, Flow, TypeScript, and some ESnext features.
{
test: /\.(js|cjs|mjs|jsx|ts|tsx)$/,
include: paths.appSrc,
loader: require.resolve('babel-loader'),
options: {
plugins: [
// [
// require.resolve('babel-plugin-named-asset-import'),
// {
// loaderMap: {
// svg: {
// ReactComponent:
// '@svgr/webpack?-svgo,+titleProp,+ref![path]',
// },
// },
// },
// ],
isEnvDevelopment &&
shouldUseReactRefresh &&
require.resolve('react-refresh/babel'),
].filter(Boolean),
cacheDirectory: true,
compact: isEnvProduction,
},
},
// "postcss" loader applies autoprefixer to our CSS.
// "css" loader resolves paths in CSS and adds assets as dependencies.
// "style" loader turns CSS into JS modules that inject <style> tags.
// In production, we use MiniCSSExtractPlugin to extract that CSS
// to a file, but in development "style" loader enables hot editing
// of CSS.
// By default we support CSS Modules with the extension .module.css
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
}),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
}),
},
// Opt-in support for SASS (using .scss or .sass extensions).
// By default we support SASS Modules with the
// extensions .module.scss or .module.sass
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
},
'sass-loader'
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},
// Adds support for CSS Modules, but using SASS
// using the extension .module.scss or .module.sass
{
test: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 3,
sourceMap: isEnvProduction
? shouldUseSourceMap
: isEnvDevelopment,
modules: {
getLocalIdent: getCSSModuleLocalIdent,
},
},
'sass-loader'
),
},
// "file" loader makes sure those assets get served by WebpackDevServer.
// When you `import` an asset, you get its (virtual) filename.
// In production, they would get copied to the `build` folder.
// This loader doesn't use a "test" so it will catch all modules
// that fall through the other loaders.
{
loader: require.resolve('file-loader'),
// Exclude `js` files to keep "css" loader working as it injects
// its runtime that would otherwise be processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
exclude: [
/\.(js|cjs|mjs|jsx|ts|tsx)$/,
/\.html$/,
/\.json$/,
],
options: {
name: 'static/media/[name].[hash:8].[ext]',
},
},
],
},
],
},
plugins: [
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
// Inlines the webpack runtime script. This script is too small to warrant
// a network request.
// https://github.com/facebook/create-react-app/issues/5358
isEnvProduction &&
shouldInlineRuntimeChunk &&
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [
/runtime-.+[.]js/,
]),
// Makes some environment variables available in index.html.
// The public URL is available as %PUBLIC_URL% in index.html, e.g.:
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
// 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(HtmlWebpackPlugin, env.raw),
// This gives some necessary context to module not found errors, such as
// the requesting resource.
new ModuleNotFoundPlugin(paths.appPath),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
// It is absolutely essential that NODE_ENV is set to production
// during a production build.
// Otherwise React will be compiled in the very slow development mode.
new webpack.DefinePlugin(env.stringified),
// This is necessary to emit hot updates (CSS and Fast Refresh):
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
// Experimental hot reloading for React .
// https://github.com/facebook/react/tree/master/packages/react-refresh
isEnvDevelopment &&
shouldUseReactRefresh &&
new ReactRefreshWebpackPlugin({
overlay: {
entry: webpackDevClientEntry,
// The expected exports are slightly different from what the overlay exports,
// so an interop is included here to enable feedback on module-level errors.
module: reactRefreshOverlayEntry,
// Since we ship a custom dev client and overlay integration,
// the bundled socket handling logic can be eliminated.
sockIntegration: false,
},
}),
// Watcher doesn't work well if you mistype casing in a path so we use
// a plugin that prints an error when you attempt to do this.
// See https://github.com/facebook/create-react-app/issues/240
isEnvDevelopment && new CaseSensitivePathsPlugin(),
isEnvProduction &&
new MiniCssExtractPlugin({
// Options similar to the same options in webpackOptions.output
// both options are optional
filename: 'static/css/[name].[contenthash:8].css',
chunkFilename:
'static/css/[name].[contenthash:8].chunk.css',
}),
// Generate an asset manifest file with the following content:
// - "files" key: Mapping of all asset filenames to their corresponding
// output file so that tools can pick it up without having to parse
// `index.html`
// - "entrypoints" key: Array of files which are included in `index.html`,
// can be used to reconstruct the HTML if necessary
new WebpackManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: paths.publicUrlOrPath,
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
const entrypointFiles = entrypoints.main.filter(
(fileName) => !fileName.endsWith('.map')
);
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
// Moment.js is an extremely popular library that bundles large locale files
// by default due to how webpack interprets its code. This is a practical
// solution that requires the user to opt into importing specific locales.
// https://github.com/jmblog/how-to-optimize-momentjs-with-webpack
// You can remove this if you don't use Moment.js:
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
isEnvProduction &&
fs.existsSync(swSrc) &&
new WorkboxWebpackPlugin.InjectManifest({
swSrc,
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
// Bump up the default maximum size (2mb) that's precached,
// to make lazy-loading failure scenarios less likely.
// See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
}),
// useTypeScript &&
// new ForkTsCheckerWebpackPlugin({
// typescript: resolve.sync('typescript', {
// basedir: paths.appNodeModules,
// }),
// async: isEnvDevelopment,
// checkSyntacticErrors: true,
// resolveModuleNameModule: process.versions.pnp
// ? `${__dirname}/pnpTs.js`
// : undefined,
// resolveTypeReferenceDirectiveModule: process.versions.pnp
// ? `${__dirname}/pnpTs.js`
// : undefined,
// tsconfig: paths.appTsConfig,
// reportFiles: [
// // This one is specifically to match during CI tests,
// // as micromatch doesn't match
// // '../cra-template-typescript/template/src/App.tsx'
// // otherwise.
// '../**/src/**/*.{ts,tsx}',
// '**/src/**/*.{ts,tsx}',
// '!**/src/**/__tests__/**',
// '!**/src/**/?(*.)(spec|test).*',
// '!**/src/setupProxy.*',
// '!**/src/setupTests.*',
// ],
// silent: true,
// // The formatter is invoked directly in WebpackDevServerUtils during development
// formatter: isEnvProduction ? typescriptFormatter : undefined,
// }),
// !disableESLintPlugin &&
// new ESLintPlugin({
// // Plugin options
// extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
// formatter: require.resolve('react-dev-utils/eslintFormatter'),
// eslintPath: require.resolve('eslint'),
// failOnError: !(isEnvDevelopment && emitErrorsAsWarnings),
// context: paths.appSrc,
// cache: true,
// cacheLocation: path.resolve(
// paths.appNodeModules,
// '.cache/.eslintcache'
// ),
// // ESLint class options
// cwd: paths.appPath,
// resolvePluginsRelativeTo: __dirname,
// baseConfig: {
// extends: [require.resolve('eslint-config-react-app/base')],
// rules: {
// ...(!hasJsxRuntime && {
// 'react/react-in-jsx-scope': 'error',
// }),
// },
// },
// }),
isEnvProduction && new BundleAnalyzerPlugin({ analyzerMode: 'static', generateStatsFile: true })
].filter(Boolean),
// Turn off performance processing because we utilize
// our own hints via the FileSizeReporter
performance: false,
};
};
module.exports = webpackConfig;
// 打印每个模块的执行速度
// https://github.com/stephencookdev/speed-measure-webpack-plugin/issues/167
// module.exports = smp.wrap(webpackConfig);

View File

@@ -0,0 +1,79 @@
'use strict';
const fs = require('fs');
const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
const ignoredFiles = require('react-dev-utils/ignoredFiles');
const redirectServedPath = require('react-dev-utils/redirectServedPathMiddleware');
const paths = require('./paths');
const getHttpsConfig = require('./getHttpsConfig');
const host = process.env.HOST || '0.0.0.0';
const sockHost = process.env.WDS_SOCKET_HOST;
const sockPath = process.env.WDS_SOCKET_PATH; // default: '/sockjs-node'
const sockPort = process.env.WDS_SOCKET_PORT;
module.exports = function (proxy, allowedHost) {
const disableFirewall =
!proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === "true";
return {
allowedHosts: disableFirewall ? "all" : [allowedHost],
compress: true,
static: {
directory: paths.appPublic,
publicPath: paths.publicUrlOrPath,
watch: {
ignored: ignoredFiles(paths.appSrc),
}
},
client: {
webSocketURL: {
hostname: sockHost,
pathname: sockPath,
port: sockPort,
},
overlay: true,
},
devMiddleware: {
// It is important to tell WebpackDevServer to use the same "publicPath" path as
// we specified in the webpack config. When homepage is '.', default to serving
// from the root.
// remove last slash so user can land on `/test` instead of `/test/`
publicPath: paths.publicUrlOrPath.slice(0, -1),
},
https: getHttpsConfig(),
host,
historyApiFallback: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebook/create-react-app/issues/387.
disableDotRule: true,
index: paths.publicUrlOrPath,
},
// `proxy` is run between `before` and `after` `webpack-dev-server` hooks
proxy,
onBeforeSetupMiddleware(server) {
const app = server.app;
// Keep `evalSourceMapMiddleware`
// middlewares before `redirectServedPath` otherwise will not have any effect
// This lets us fetch source contents from webpack for the error overlay
app.use(evalSourceMapMiddleware(server));
if (fs.existsSync(paths.proxySetup)) {
// This registers user provided middleware for proxy reasons
require(paths.proxySetup)(app);
}
},
onAfterSetupMiddleware(server) {
const app = server.app;
// Redirect to `PUBLIC_URL` or `homepage` from `package.json` if url not match
app.use(redirectServedPath(paths.publicUrlOrPath));
// This service worker file is effectively a 'no-op' that will reset any
// previous service worker registered for the same host:port combination.
// We do this in development to avoid hitting the production cache if
// it used the same host and port.
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
app.use(noopServiceWorkerMiddleware(paths.publicUrlOrPath));
},
};
};

14
docker/Dockerfile Normal file
View File

@@ -0,0 +1,14 @@
FROM nginx:stable-alpine
ARG version=0.0.0
ENV VERSION=${version}
# ENV INTERCEPT_POST=true
COPY pub /etc/nginx/html
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY docker/docker-entrypoint.sh /docker-entrypoint.d/40-dynamic-subpath.sh
RUN chmod +x /docker-entrypoint.d/40-dynamic-subpath.sh \
&& echo ${version} > /etc/nginx/html/VERSION
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

25
docker/Dockerfile.build Normal file
View File

@@ -0,0 +1,25 @@
# build environment
FROM node:16 as builder
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY package.json ./
COPY package-lock.json ./
RUN npm install
COPY . ./
RUN npm run build
# production environment
FROM nginx:stable-alpine
ARG version=0.0.0
ENV VERSION=${version}
# ENV INTERCEPT_POST=true
COPY --from=builder /app/dist /etc/nginx/html
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY docker/docker-entrypoint.sh /docker-entrypoint.d/40-dynamic-subpath.sh
RUN chmod +x /docker-entrypoint.d/40-dynamic-subpath.sh \
&& echo ${version} > /etc/nginx/html/VERSION
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

19
docker/Dockerfile.subpath Normal file
View File

@@ -0,0 +1,19 @@
# build environment
FROM node:14 as builder
WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
ENV REACT_APP_BASE $BASE_PATH
ENV PUBLIC_URL $BASE_PATH/admin
COPY package.json ./
COPY package-lock.json ./
RUN npm install
COPY . ./
RUN REACT_APP_BASE=/changzhou PUBLIC_URL=/changzhou/admin GENERATE_SOURCEMAP=false node scripts/build.js
# production environment
FROM nginx:stable-alpine
COPY --from=builder /app/dist /etc/nginx/html
COPY nginx/nginx.conf /etc/nginx/nginx.conf
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,13 @@
#!/bin/sh
set -e
if [[ "$APP_SUB_PATH" ]]; then
sed -i s@/admin/@$APP_SUB_PATH/admin/@g /etc/nginx/html/index.html
sed -i s@basePath:\"@basePath:\"$APP_SUB_PATH@g /etc/nginx/html/static/js/main*
sed -i s@\"/api/report-api/@\"${APP_SUB_PATH}/api/report-api/@g /etc/nginx/html/static/js/*
sed -i s@'/download@'$APP_SUB_PATH/download@g /etc/nginx/html/static/js/*
sed -i s@/admin/preview@$APP_SUB_PATH/admin/preview@g /etc/nginx/html/static/js/*
sed -i s@baseURL=\"/api\"@baseURL=\"$APP_SUB_PATH/api\"@g /etc/nginx/html/static/js/main*
sed -i s@"/admin/"@"$APP_SUB_PATH/admin/"@g /etc/nginx/html/static/css/*
fi

19
docker/nginx/default.conf Normal file
View File

@@ -0,0 +1,19 @@
server {
listen 80;
location ^~ /admin {
alias /etc/nginx/html;
try_files $uri $uri/index.html @admin;
}
location @admin {
rewrite ^.*$ /admin/index.html last;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

98
docker/nginx/nginx.conf Normal file
View File

@@ -0,0 +1,98 @@
# user www-data;
worker_processes 2;
pid /run/nginx.pid;
events {
worker_connections 1024;
# multi_accept on;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 10m;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_min_length 1000;
gzip_proxied expired no-cache no-store private auth;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript application/x-ipynb+json;
##
# nginx-naxsi config
##
# Uncomment it if you installed nginx-naxsi
##
#include /etc/nginx/naxsi_core.rules;
##
# nginx-passenger config
##
# Uncomment it if you installed nginx-passenger
##
#passenger_root /usr;
#passenger_ruby /usr/bin/ruby;
##
# Virtual Host Configs
##
client_header_buffer_size 16k;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
#mail {
# # See sample authentication script at:
# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#
# # auth_http localhost/auth.php;
# # pop3_capabilities "TOP" "USER";
# # imap_capabilities "IMAP4rev1" "UIDPLUS";
#
# server {
# listen localhost:110;
# protocol pop3;
# proxy on;
# }
#
# server {
# listen localhost:143;
# protocol imap;
# proxy on;
# }
#}

6
mock/index.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = {
'/api/login': {
status: 0,
msg: '登录成功',
},
};

31116
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

130
package.json Normal file
View File

@@ -0,0 +1,130 @@
{
"name": "olms-admin",
"version": "1.2.0",
"homepage": "admin",
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"build-sub": "REACT_APP_BASE=/tianmuhu PUBLIC_URL=/tianmuhu/admin GENERATE_SOURCEMAP=false node scripts/build.js",
"test": "node scripts/test.js",
"analyze": "webpack-bundle-analyzer --port 8080 dist/stats.json",
"lint": "prettier --config ./.prettierrc.yaml --write ."
},
"dependencies": {
"@fluentui/react": "^7.161.0",
"@rjsf/core": "^2.4.2",
"@rjsf/fluent-ui": "^2.4.2",
"@types/crypto-js": "^4.1.1",
"amis": "^6.8.0",
"amis-core": "^6.8.0",
"amis-ui": "^6.8.0",
"antd": "^4.18.6",
"axios": "^0.24.0",
"bootstrap": "^3.4.1",
"classnames": "^2.2.6",
"copy-to-clipboard": "3.3.2",
"crypto-js": "^4.0.0",
"date-fns": "^2.16.1",
"font-awesome": "^4.7.0",
"history": "^4.7.2",
"js-cookie": "^2.2.1",
"lodash": "^4.17.15",
"mammoth": "^1.4.21",
"mini-css-extract-plugin": "^2.6.0",
"mobx": "4.15.4",
"mobx-react": "6.1.8",
"mobx-state-tree": "^3.17.3",
"promise": "^8.1.0",
"promise-polyfill": "8.2.0",
"qs": "6.5.1",
"react": "^16.10.2",
"react-big-calendar": "^0.40.0",
"react-dom": "^16.10.2",
"react-perfect-scrollbar": "^1.5.8",
"react-router": "5.1.2",
"react-router-dom": "5.1.2",
"resolve": "^1.22.0",
"resolve-url-loader": "^5.0.0",
"vditor": "^3.10.7",
"viewerjs": "^1.10.1",
"xlsx": "^0.16.9"
},
"devDependencies": {
"@babel/cli": "^7.17.6",
"@babel/core": "^7.17.9",
"@babel/plugin-proposal-decorators": "^7.17.9",
"@babel/preset-env": "^7.16.11",
"@babel/preset-react": "^7.16.7",
"@babel/preset-typescript": "^7.16.7",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
"@types/js-cookie": "^2.2.6",
"@types/lodash": "^4.14.123",
"@types/node": "^11.13.8",
"@types/qs": "^6.5.3",
"@types/react": "^16.8.4",
"@types/react-big-calendar": "^0.24.8",
"@types/react-dom": "^16.8.4",
"@types/react-router-dom": "^5.1.6",
"amis-editor": "^6.8.0",
"amis-editor-core": "^6.8.0",
"axios-mock-adapter": "1.16.0",
"babel-loader": "^8.2.4",
"bfj": "^7.0.2",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"clean-webpack-plugin": "^3.0.0",
"core-js": "^3.22.0",
"css-loader": "^3.4.2",
"css-minimizer-webpack-plugin": "^3.4.1",
"dotenv": "^16.0.0",
"dotenv-expand": "^8.0.3",
"file-loader": "^5.0.2",
"fs-walk": "0.0.2",
"html-minifier": "^4.0.0",
"html-webpack-plugin": "^5.5.0",
"http-proxy-middleware": "^2.0.4",
"mocker-api": "^1.11.2",
"pnp-webpack-plugin": "^1.7.0",
"postcss": "^8.4.12",
"postcss-custom-properties": "^12.1.7",
"postcss-flexbugs-fixes": "4.2.1",
"postcss-loader": "^4.3.0",
"postcss-normalize": "8.0.1",
"postcss-preset-env": "6.7.0",
"postcss-safe-parser": "5.0.2",
"prettier": "^2.6.2",
"react-dev-utils": "^12.0.1",
"react-error-overlay": "6.0.9",
"sass": "^1.45.2",
"sass-loader": "^8.0.2",
"speed-measure-webpack-plugin": "^1.5.0",
"style-loader": "^1.1.3",
"terser-webpack-plugin": "^5.3.1",
"ts-pnp": "^1.2.0",
"tslib": "^2.3.1",
"type-fest": "^2.12.1",
"typescript": "^4.4.4",
"url": "^0.11.0",
"url-loader": "^4.1.1",
"webpack": "^5.28.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.6.0",
"webpack-dev-server": "^4.8.1",
"webpack-manifest-plugin": "^5.0.0",
"workbox-webpack-plugin": "^6.5.3"
},
"overrides": {
"react-error-overlay": "6.0.9"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
">0.2%",
"not dead",
"not op_mini all"
]
}
}

17
postcss.config.js Normal file
View File

@@ -0,0 +1,17 @@
const postcssNormalize = require('postcss-normalize');
module.exports = {
plugins: () => [
require('postcss-flexbugs-fixes'),
require('postcss-preset-env')({
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
}),
// Adds PostCSS Normalize as the reset css with default options,
// so that it honors browserslist config in package.json
// which in turn let's users customize the target behavior as per their needs.
postcssNormalize(),
],
};

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

38
public/index.html Normal file
View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="智医教学实践平台" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
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" class="app-wrapper"></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>

25
public/manifest.json Normal file
View File

@@ -0,0 +1,25 @@
{
"short_name": "OLMS",
"name": "智医教学实践平台",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

215
scripts/build.js Normal file
View File

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

171
scripts/start.js Normal file
View File

@@ -0,0 +1,171 @@
'use strict';
// Do this as the first thing so that any code reading it knows the right env.
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';
// Makes the script crash on unhandled rejections instead of silently
// ignoring them. In the future, promise rejections that are not handled will
// terminate the Node.js process with a non-zero exit code.
process.on('unhandledRejection', (err) => {
throw err;
});
// Ensure environment variables are read.
require('../config/env');
const fs = require('fs');
const chalk = require('react-dev-utils/chalk');
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const clearConsole = require('react-dev-utils/clearConsole');
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
const {
choosePort,
createCompiler,
prepareProxy,
prepareUrls,
} = require('react-dev-utils/WebpackDevServerUtils');
const openBrowser = require('react-dev-utils/openBrowser');
const semver = require('semver');
const paths = require('../config/paths');
const configFactory = require('../config/webpack.config');
const createDevServerConfig = require('../config/webpackDevServer.config');
const getClientEnvironment = require('../config/env');
const react = require(require.resolve('react', { paths: [paths.appPath] }));
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
const useYarn = fs.existsSync(paths.yarnLockFile);
const isInteractive = process.stdout.isTTY;
// Warn and crash if required files are missing
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
process.exit(1);
}
// Tools like Cloud9 rely on this.
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000;
const HOST = process.env.HOST || '0.0.0.0';
if (process.env.HOST) {
console.log(
chalk.cyan(
`Attempting to bind to HOST environment variable: ${chalk.yellow(
chalk.bold(process.env.HOST)
)}`
)
);
console.log(
`If this was unintentional, check that you haven't mistakenly set it in your shell.`
);
console.log(
`Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}`
);
console.log();
}
// We require that you explicitly set browsers and do not fall back to
// browserslist defaults.
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
checkBrowsers(paths.appPath, isInteractive)
.then(() => {
// We attempt to use the default port but if it is busy, we offer the user to
// run on a different port. `choosePort()` Promise resolves to the next free port.
return choosePort(HOST, DEFAULT_PORT);
})
.then((port) => {
if (port == null) {
// We have not found a port.
return;
}
const config = configFactory("development");
const protocol = process.env.HTTPS === "true" ? "https" : "http";
const appName = require(paths.appPackageJson).name;
const useTypeScript = fs.existsSync(paths.appTsConfig);
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === "true";
const urls = prepareUrls(
protocol,
HOST,
port,
paths.publicUrlOrPath.slice(0, -1)
);
const devSocket = {
warnings: (warnings) =>
devServer.sockWrite(devServer.sockets, "warnings", warnings),
errors: (errors) =>
devServer.sockWrite(devServer.sockets, "errors", errors),
};
// Create a webpack compiler that is configured with custom messages.
const compiler = createCompiler({
appName,
config,
devSocket,
urls,
useYarn,
useTypeScript,
tscCompileOnError,
webpack,
});
// Load proxy config
const proxySetting = require(paths.appPackageJson).proxy;
const proxyConfig = prepareProxy(
proxySetting,
paths.appPublic,
paths.publicUrlOrPath
);
// Serve webpack assets generated by the compiler over a web server.
const serverConfig = createDevServerConfig(
proxyConfig,
urls.lanUrlForConfig
);
serverConfig.host = HOST;
serverConfig.port = port;
serverConfig.setupExitSignals = true;
const devServer = new WebpackDevServer(serverConfig, compiler);
// Launch WebpackDevServer.
devServer.startCallback((err) => {
if (err) {
return console.log(err);
}
if (isInteractive) {
clearConsole();
}
if (env.raw.FAST_REFRESH && semver.lt(react.version, "16.10.0")) {
console.log(
chalk.yellow(
`Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`
)
);
}
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();
});
});
if (process.env.CI !== 'true') {
// Gracefully exit when stdin ends
process.stdin.on('end', function () {
devServer.close();
process.exit();
});
}
})
.catch((err) => {
if (err && err.message) {
console.log(err.message);
}
process.exit(1);
});

51
scripts/test.js Normal file
View File

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

290
src/App.css Normal file
View File

@@ -0,0 +1,290 @@
.cxd-Layout-content {
background-color: #fff;
}
.user-page .cxd-Page-body {
padding: 0;
}
.cxd-DropDown-menu {
min-width: auto;
}
.cxd-Tree {
height: 84vh;
max-height: inherit!important;
}
.cxd-ListControl-item:hover:active:hover,
.cxd-ListControl-item.is-active:hover {
background-color: var(--ListControl-item-onActive-bg);
}
.plan-list .cxd-ListControl-items {
display: flex;
flex-direction: column;
}
.rbc-allday-cell {
display: none;
}
.rbc-month-row {
min-height: 128px;
border-color: rgba(0, 0, 0, 0.12);
}
.copy-list {
display: flex;
flex-flow: wrap;
}
.copy-list .cxd-GroupedSelection-item {
width: 50%;
}
/* https://github.com/rjsf-team/react-jsonschema-form/issues/2188 */
/* .rjsf .field-undefined> :nth-child(1) {
display: none;
} */
.rjsf [for='root']:first-child + #root__description {
display: none;
}
.rjsf .css-42:last-child {
display: none;
}
.rjsf .field-array span:last-child {
float: none !important;
}
.rjsf fieldset {
padding: 0;
border: 0;
}
.rjsf .btn-add::after {
content: '添加';
}
.rjsf.array-item-move-up::after {
content: '上移';
}
.rjsf.array-item-move-down::after {
content: '下移';
}
.rjsf.array-item-remove::after {
content: '删除';
}
.rjsf .form-group {
margin-bottom: 15px;
}
.rjsf .form-group label {
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
font-weight: 700;
}
.rjsf .form-control {
max-width: 100%;
width: 100%;
display: flex;
background: var(--Form-input-bg);
border: var(--Form-input-borderWidth) solid var(--Form-input-borderColor);
border-radius: var(--Form-input-borderRadius);
line-height: var(--Form-input-lineHeight);
padding: var(--Form-input-paddingY) var(--Form-input-paddingX);
font-size: var(--Form-input-fontSize);
flex-wrap: wrap;
justify-content: flex-start;
}
.preview table {
width: 100%;
border: 1px solid #000000;
border-collapse: separate;
border-left: 0;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
background-color: transparent;
border-spacing: 0;
}
.preview table thead:first-child tr:first-child > th:first-child,
.preview table tbody:first-child tr:first-child > td:first-child {
-webkit-border-top-left-radius: 4px;
border-top-left-radius: 4px;
-moz-border-radius-topleft: 4px;
}
.preview table thead th {
vertical-align: bottom;
}
.preview table th,
.preview table td {
padding: 4px 8px !important;
line-height: 14px;
text-align: left;
vertical-align: top;
border-left: 1px solid #000000;
border-top: 1px solid #000000;
}
.preview table th {
padding: 4px 8px;
line-height: 20px;
text-align: left;
}
/*custom*/
.cxd-Select-popover {
z-index: 1500;
}
.tox {
z-index: 1400 !important;
}
.amis-scope .cxd-PopOver {
z-index: 1400 !important;
}
#print-preview .cxd-Table-fixedTop {
display: none;
}
.common-padding {
padding: 0px 0 4px 10px !important;
}
/* 部门管理 */
.partment-grid .cxd-Grid-col--md3 {
padding-right: 5px;
}
.partment-grid .cxd-Grid-col--md9 {
padding-left: 0;
}
.partment-height {
height: 84vh;
max-height: 100vh;
overflow: hidden;
}
/* 报告模板 */
.rpt-tp-height {
height: 81vh;
}
/* 教学计划 */
.experiment {
display: inline-block;
margin-left: 0;
width: 230px;
height: 20px;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.experiment:hover {
display: inline-block;
width: auto;
height: 20px;
min-width: 230px;
vertical-align: middle;
white-space: none;
overflow: unset;
text-overflow: unset;
}
.class {
display: inline-block;
width: 140px;
height: 20px;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.class:hover {
display: inline-block;
width: auto;
height: 20px;
min-width: 140px;
vertical-align: middle;
white-space: none;
overflow: unset;
text-overflow: unset;
}
.course_select .cxd-Select-menu {
width: 200px;
}
.trash {
padding: 0 5px;
}
.wrapper {
display: inline-block;
padding: 0;
}
.wrapper .is-disabled {
background: grey;
color: white !important;
}
.tab .cxd-Tabs-content {
padding: 0;
}
.table {
width: auto;
min-width: auto;
}
.text-purple {
color: rgb(175, 167, 175);
}
.question img {
max-width: 100%;
}
.question-option p {
display: inline-block;
}
.question .cxd-Button {
display: inline-block;
width: 480px;
overflow: hidden;
white-space: nowrap;
text-align: left;
text-overflow: ellipsis;
}
.menu-permission-tree {
max-height: none;
}
.evaluation-height {
height: 75vh;
max-height: 75vh;
}
.cxd-Modal {
overflow-y: auto;
}
.gallery .cxd-Modal-content {
border: 0;
max-width: 100%;
}
.gallery .cxd-Modal-content.in,
.gallery .cxd-Modal-content.out {
animation-fill-mode: none;
}
.device-frame {
overflow-y: auto;
}
.root-41 {
display: none;
}

171
src/App.tsx Normal file
View File

@@ -0,0 +1,171 @@
import { alert, confirm, toast } from 'amis-ui';
import axios from 'axios';
import copy from 'copy-to-clipboard';
import _, { isString } from 'lodash';
import { Provider } from 'mobx-react';
import * as React from 'react';
import RootRoute from './routes';
import { MainStore } from './stores';
import './utils/icons';
import { request } from './utils/requestInterceptor';
import { resRequest } from './utils/resRequestInterceptor';
import './renderer/AttScoreRule';
import './renderer/Calendar';
import './renderer/CourseList';
import './renderer/DataPreview';
import './renderer/DataTrial';
import './renderer/ProjectScoreItem';
import './renderer/Scheduler';
import './renderer/TimeslotPicker';
import './renderer/TimeslotPicker/Copy';
import './renderer/Workload';
// import './renderer/DataPreview1';
import './renderer/BatchGroup';
import './renderer/CustomImageUpload';
import './renderer/CustomRichtext';
import './renderer/DataSet';
import './renderer/DataSetPicker';
import './renderer/DownloadReport';
import './renderer/DownloadReportSchedule';
import './renderer/ExamImport';
import './renderer/ExamPaperRule';
import './renderer/ExcelImport';
import './renderer/ExcelImportDeduction';
import './renderer/ExcelImportGroup';
import './renderer/ExcelImportSchedule';
import './renderer/ExcelImportStudent';
import './renderer/ExcelImportStudentOrg';
import './renderer/Group';
import './renderer/ImageGallery';
import './renderer/Option';
import './renderer/TextbookSubmit';
import './renderer/Vditor';
// css
import 'font-awesome/css/font-awesome.css';
// import 'amis/lib/themes/antd.css';
import 'amis-ui/lib/themes/cxd.css';
// import './scss/style.scss'
// import 'amis/sdk/iconfont.css'
import 'amis/lib/helper.css';
import './App.css';
export default function (): JSX.Element {
console.log(React);
const store = ((window as any).store = MainStore.create(
{},
{
fetcher: ({ url, method, data, config, headers }: any) => {
config = config || {};
config.headers = config.headers || {};
config.withCredentials = true;
config.baseURL = '/api';
// config.baseURL = '/changzhou/api';
// config.baseURL = prefix ? `${prefix}/api` : '/api';
if (config.cancelExecutor) {
config.cancelToken = new axios.CancelToken(config.cancelExecutor);
}
config.headers = headers || {};
config.method = method;
if (method === 'get' && data) {
config.params = data;
} else if (data && data instanceof FormData) {
// config.headers = config.headers || {};
// config.headers['Content-Type'] = 'multipart/form-data';
} else if (
data &&
typeof data !== 'string' &&
!(data instanceof Blob) &&
!(data instanceof ArrayBuffer)
) {
data = JSON.stringify(data);
// config.headers = config.headers || {};
config.headers['Content-Type'] = 'application/json';
}
let match = url.match(/\[(.*?)\]/);
while (match) {
url = url.replace(match[0], `.${match[1]}`);
match = url.match(/\[(.*?)\]/);
}
let apiPrefix = url.match(/^[^/]*\//);
if (
config.headers.post2rest !== false &&
(config.method === 'put' ||
config.method === 'patch' ||
config.method === 'delete')
) {
url = _.replace(
url,
/^[^/]*\//,
`${apiPrefix}post2rest/${config.method}/`
);
config.method = 'post';
}
let token = window.sessionStorage.getItem('authenticated');
if (token && url !== '/api/rpc/login') {
config.headers['Authorization'] = `Bearer ${token}`;
}
data && (config.data = data);
config.url = url;
return request(config);
},
resFetcher: ({ url, method, data, config, headers }): any => {
config = config || {};
config.headers = config.headers || {};
config.withCredentials = true;
config.baseURL = '';
if (config.cancelExecutor) {
config.cancelToken = new axios.CancelToken(config.cancelExecutor);
}
config.headers = headers || {};
config.method = method;
let auth = window.sessionStorage.getItem('auth');
if (auth) {
config.headers['X-Auth'] = auth;
}
data && (config.data = data);
config.url = url;
return resRequest(config);
},
isCancel: (e: any) => axios.isCancel(e),
notify: (type: 'success' | 'error' | 'info', msg: string) => {
if ((isString(msg) && msg) || msg?.message) {
toast[type]
? toast[type](msg, type === 'error' ? '系统错误' : '系统消息')
: console.warn('[Notify]', type, msg);
console.log('[notify]', type, msg);
}
},
alert,
confirm,
copy: (contents: string, options: any = {}) => {
const ret = copy(contents, options);
ret &&
(!options || options.shutup !== true) &&
toast.info('内容已拷贝到剪切板');
return ret;
},
}
));
return (
<Provider store={store}>
<RootRoute store={store} />
</Provider>
);
}

View File

@@ -0,0 +1,61 @@
import * as React from 'react';
import { render as renderSchema } from 'amis';
import { IMainStore } from '@/stores';
import { inject, observer } from 'mobx-react';
import { withRouter } from 'react-router';
import { Action } from 'amis/lib/types';
import renderOptions from '@/utils/renderOptions';
interface RendererProps {
schema?: any;
[propName: string]: any;
}
@inject('store')
// @ts-ignore
@withRouter
@observer
export default class AMisRenderer extends React.Component<RendererProps, any> {
env: any = null;
handleAction = (e: any, action: Action) => {
this.env.alert(`没有识别的动作:${JSON.stringify(action)}`);
};
constructor(props: RendererProps) {
super(props);
const store = props.store as IMainStore;
const history = props.history;
this.env = renderOptions(store, history);
}
render() {
const { schema, store, onAction, ...rest } = this.props;
return renderSchema(
schema,
{
data: {
user: store.user,
currentSemester: store.currentSemester?.id,
semester: store.currentSemester,
semesterSelect: store.currentSemester?.id,
periods: store.periods,
basePath: store.basePath,
},
onAction: (e: any, action: any) => {
console.log(action)
if (!action || action.actionType === 'url') {
return;
}
if (action.actionType === 'logout') {
store.logout();
}
},
theme: store && store.theme,
...rest,
},
this.env
);
}
}

215
src/components/UserInfo.tsx Normal file
View File

@@ -0,0 +1,215 @@
import React from 'react';
import AMisRenderer from './AMisRenderer';
interface UserInfoProps {
user: any;
}
interface UserInfoState {
open?: boolean;
}
export default class UserInfo extends React.Component<
UserInfoProps,
UserInfoState
> {
constructor(props: UserInfoProps) {
super(props);
this.state = {
open: false,
};
this.open = this.open.bind(this);
this.close = this.close.bind(this);
}
static defaultProps = {};
open() {
this.setState({
open: true,
});
}
close() {
this.setState({
open: false,
});
}
handleClickOutside() {
this.close();
}
render() {
const user = this.props.user;
const schema = {
type: 'page',
className: 'user-page',
body: [
{
type: 'button',
label: '工作台',
onEvent: {
click: {
actions: [
{
ignoreError: false,
actionType: 'url',
args: {
url: '/hub/login?jwt=$user.token',
params: {},
},
},
],
},
},
id: 'u:05fc5e4362bc',
themeCss: {
className: {
'padding-and-margin:default': {
marginRight: '5px',
},
},
},
},
// {
// type: 'button',
// label: '探索',
// onEvent: {
// click: {
// actions: [
// {
// ignoreError: false,
// actionType: 'url',
// args: {
// url: '/explore?token=$user.token',
// params: {},
// },
// },
// ],
// },
// },
// id: 'u:05fc5e4362bd',
// themeCss: {
// className: {
// 'padding-and-margin:default': {
// marginRight: '5px',
// },
// },
// },
// },
{
type: 'button',
label: '资源管理',
onEvent: {
click: {
actions: [
{
ignoreError: false,
actionType: 'url',
args: {
url: '/res',
params: {},
},
},
],
},
},
id: 'u:05fc5e4362bc',
themeCss: {
className: {
'padding-and-margin:default': {
marginRight: '5px',
},
},
},
},
{
type: 'dropdown-button',
label: user.name,
align: 'right',
buttons: [
{
type: 'button',
icon: 'fa fa-cog',
label: '系统信息 ',
actionType: 'dialog',
dialog: {
closeOnEsc: true,
title: '系统信息',
size: 'sm',
type: 'dialog',
body: [
{
type: 'tpl',
tpl: '<p>智医教学实践平台</p>\n<p>OLMS II</p>',
inline: false,
id: 'u:369cb33ce9d0',
},
],
showCloseButton: true,
showErrorMsg: true,
showLoading: true,
id: 'u:289af4d19c37',
actions: [],
},
},
{
type: 'button',
icon: 'fa fa-key',
label: '修改密码 ',
actionType: 'dialog',
dialog: {
closeOnEsc: true,
title: '修改登录密码',
size: 'sm',
body: [
{
type: 'form',
api: {
url: `rest/rpc/change_password`,
method: 'post',
data: {
password: '${password}',
},
},
messages: {
saveSuccess: '[密码修改成功] 请使用新密码重新登录',
saveFailed: '密码修改失败',
},
// redirect: '/login',
mode: 'horizontal',
horizontal: {
left: 'col-sm-3',
right: 'col-sm-9',
},
controls: [
{
type: 'password',
name: 'password',
required: true,
label: '新密码',
},
// {
// type: 'password',
// name: 'confirmPassword',
// required: true,
// label: '重复密码',
// },
],
},
],
},
},
{
type: 'button',
actionType: 'logout',
level: 'link',
icon: 'fa fa-reply',
label: '退出登录 ',
},
],
},
],
};
return <AMisRenderer schema={schema} />;
}
}

13
src/index.css Normal file
View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

29
src/index.tsx Normal file
View File

@@ -0,0 +1,29 @@
import * as React from 'react';
import { render } from 'react-dom';
import App from './App';
import 'react-perfect-scrollbar/dist/css/styles.css';
import './index.css';
export function bootstrap(mountTo: HTMLElement) {
render(<App />, mountTo);
}
(self as any).MonacoEnvironment = {
getWorkerUrl: function (moduleId: any, label: string) {
if (label === 'json') {
return '/json.worker.bundle.js';
}
if (label === 'css') {
return '/css.worker.bundle.js';
}
if (label === 'html') {
return '/html.worker.bundle.js';
}
if (label === 'typescript' || label === 'javascript') {
return '/ts.worker.bundle.js';
}
return '/editor.worker.bundle.js';
},
};
bootstrap(document.getElementById('root')!);

16
src/pages/404.tsx Normal file
View File

@@ -0,0 +1,16 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import { NotFound } from 'amis';
export default () => (
<NotFound
links={
<Link to="/" className="list-group-item">
<i className="fa fa-chevron-right text-muted" />
<i className="fa fa-fw fa-mail-forward m-r-xs" />
</Link>
}
footerText={''}
/>
);

116
src/pages/Editor.tsx Normal file
View File

@@ -0,0 +1,116 @@
import React, { useEffect } from 'react';
import { Editor, ShortcutKey } from 'amis-editor';
import { inject, observer } from 'mobx-react';
import { RouteComponentProps } from 'react-router-dom';
import 'amis/lib/themes/cxd.css';
import 'amis/lib/helper.css';
import 'amis/sdk/iconfont.css';
import 'amis-editor-core/lib/style.css';
// import 'amis/lib/themes/default.css';
import '../scss/editor.scss';
import { IMainStore } from '../stores';
import loadPageByPath from '../utils/loadPageByPath';
import renderOptions from '../utils/renderOptions';
export default inject('store')(
observer(function ({
store,
history,
match,
}: { store: IMainStore } & RouteComponentProps<{ id: string }>) {
const pageId: number = parseInt(match.params.id, 10);
useEffect(() => {
(async () => {
let nav: any = store.flat_navigations.find((x: any) => x.id === pageId);
let pagePath = nav ? nav.schema_path : '';
loadPageByPath(pagePath).then((page: any) => {
store.updateSchema(page.schema);
});
})();
}, [pageId]);
function save() {
console.log(store.schema);
store.copy(JSON.stringify(store.schema));
}
function exit() {
history.goBack();
}
return (
<div className="Editor-Demo">
<div className="Editor-header">
<div className="Editor-title"></div>
<div className="Editor-view-mode-group-container">
<div className="Editor-view-mode-group">
<div
className={`Editor-view-mode-btn editor-header-icon ${
!store.isMobile ? 'is-active' : ''
}`}
onClick={() => {
store.setIsMobile(false);
}}
>
PC
</div>
<div
className={`Editor-view-mode-btn editor-header-icon ${
store.isMobile ? 'is-active' : ''
}`}
onClick={() => {
store.setIsMobile(true);
}}
>
H5
</div>
</div>
</div>
<div className="Editor-header-actions">
<ShortcutKey />
<div
className={`header-action-btn m-1 ${
store.preview ? 'primary' : ''
}`}
onClick={() => {
store.setPreview(!store.preview);
}}
>
{store.preview ? '编辑' : '预览'}
</div>
{!store.preview && (
<>
<div className={`header-action-btn`} onClick={save}>
</div>
<div className={`header-action-btn exit-btn`} onClick={exit}>
退
</div>
</>
)}
</div>
</div>
<div className="Editor-inner">
<Editor
theme={store.theme}
preview={store.preview}
isMobile={store.isMobile}
value={store.schema}
onChange={(value: any) => store.updateSchema(value)}
onPreview={() => {
store.setPreview(true);
}}
onSave={save}
className="is-fixed"
showCustomRenderersPanel={true}
amisEnv={renderOptions(store, history)}
/>
</div>
</div>
);
})
);

113
src/pages/Login.tsx Normal file
View File

@@ -0,0 +1,113 @@
import { MD5 } from 'crypto-js';
import { inject, observer } from 'mobx-react';
import React from 'react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import AMisRenderer from '../components/AMisRenderer';
import { IMainStore } from '../stores/index';
interface LoginProps extends RouteComponentProps<any> {
store: IMainStore;
}
const schema = {
type: 'page',
body: [
{
type: 'wrapper',
body: [
{
type: 'plain',
tpl: '用户登录',
inline: false,
className: 'block m-t-xxl m-b-xl text-center text-2x text-info',
},
{
type: 'form',
submitText: '登录',
api: {
method: 'post',
url: 'rest/rpc/login',
data: {
code: '${username}',
password: '${password}',
},
requestAdaptor: (api: any) => {
api.body.password = MD5(api.body.password).toString();
return api;
},
},
wrapWithPanel: false,
canAccessSuperData: false,
messages: {
saveSuccess: '',
},
body: [
{
label: '',
type: 'input-text',
name: 'username',
required: true,
placeholder: '用户名',
},
{
type: 'input-password',
label: '',
name: 'password',
placeholder: '密码',
required: true,
},
{
type: 'submit',
label: '登录',
size: 'lg',
block: true,
level: 'primary',
},
],
},
],
className: 'bg-white w-xxl Modal-content',
},
],
className: 'bg-white h-full',
};
@inject('store')
// @ts-ignore
@withRouter
@observer
export default class LoginRoute extends React.Component<LoginProps> {
handleFormSaved = (value: any) => {
const store = this.props.store;
const history = this.props.history;
let user = null;
if (value.items) {
user = value.items[0].login;
} else {
user = value;
}
store.notify('登录成功!');
store.user.login(user);
history.push('/');
store
.resFetcher({
url: '/res/api/login',
method: 'post',
data: {
username: '',
password: '',
recaptcha: '',
},
})
.then((res: any) => {
console.log(res);
document.cookie = `auth=${res.data.data}; Path=/; SameSite=Strict;"`;
sessionStorage.setItem('auth', res.data.data);
});
};
render() {
return <AMisRenderer onFinished={this.handleFormSaved} schema={schema} />;
}
}

124
src/pages/Preview.tsx Normal file
View File

@@ -0,0 +1,124 @@
import React from 'react';
import { inject, observer } from 'mobx-react';
import { RouteComponentProps, withRouter } from 'react-router-dom';
import { IMainStore } from '../stores/index';
import AMisRenderer from '../components/AMisRenderer';
interface LoginProps extends RouteComponentProps<any> {
store: IMainStore;
}
const schema = {
type: 'page',
bodyClassName: 'w-4/5 m-auto',
body: [
{
type: 'plain',
tpl: '实验开放教学登记表',
inline: false,
className: 'text-center text-2x font-bold',
},
{
type: 'plain',
tpl: '实验名称:${project}',
inline: false,
className: 'm-t-xs',
},
{
type: 'plain',
tpl: '课堂编号:${id} 指导教师:${teacher} 地点:${location} \n 实验时间:${date} ${start_time}',
inline: false,
className: 'm-sm',
},
{
type: 'plain',
tpl: '备注:登记表请妥善保存,学期结束时必须交到实验室存档,不能遗失!',
inline: false,
className: '',
},
{
type: 'crud',
api: {
method: 'get',
url: 'rest/user2projects?select=id,student:users!user2project_student_id_fkey(id,code,name,user_orgs(orgs(name)))&schedule_id=eq.${id}&schedule_status=in.(elected,stopped)&order=id.asc',
adaptor:
'payload.data.items = payload.data.items.map((item, index) => {\r\n return {\r\n ...item,\r\n order_number: index + 1\r\n }\r\n});\r\nreturn payload;',
},
columns: [
{
name: 'order_number',
label: '序号',
type: 'text',
placeholder: '-',
},
{
name: 'student.code',
label: '学号',
type: 'text',
},
{
type: 'text',
label: '姓名',
name: 'student.name',
},
{
name: 'student.user_orgs[0].orgs.name',
label: '班级',
type: 'text',
},
{
type: 'text',
label: '考勤',
name: 'att',
placeholder: '',
},
{
type: 'text',
label: '预习',
name: 'prepare',
placeholder: '',
},
{
type: 'text',
label: '操作',
name: 'op',
placeholder: '',
},
{
type: 'text',
label: '报告',
name: 'report',
placeholder: '',
},
{
type: 'text',
label: '备注',
name: 'beizhu',
placeholder: '',
},
],
columnsTogglable: false,
affixHeader: false,
showHeader: true,
showFooter: true,
messages: {},
className: 'm-t-sm preview',
},
{
type: 'tpl',
tpl: '<p>教师签字:&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;</p>',
inline: false,
className: 'text-right',
},
],
};
@inject('store')
// @ts-ignore
@withRouter
@observer
export default class LoginRoute extends React.Component<LoginProps> {
render() {
return <AMisRenderer schema={schema} />;
}
}

41
src/pages/Schema.tsx Normal file
View File

@@ -0,0 +1,41 @@
import React, { useEffect, useState } from 'react';
import { inject, observer } from 'mobx-react';
import { RouteComponentProps, useParams } from 'react-router-dom';
import { IMainStore } from '../stores/index';
import AMisRenderer from '../components/AMisRenderer';
import loadPageByPath from '../utils/loadPageByPath';
export default inject('store')(
observer(function ({
store,
}: { store: IMainStore } & RouteComponentProps<{
id: string;
}>) {
let { centre_id, id } = useParams<any>();
const [schema, setSchema] = useState({
type: 'page',
body: {
type: 'spinner',
},
});
useEffect(() => {
setSchema({
type: 'page',
body: {
type: 'spinner',
},
});
let nav: any = store.flat_navigations
.filter((x: any) => x.path !== null)
.find((x: any) => x.id.toString() === id);
let pagePath = nav ? nav.schema_path : '';
loadPageByPath(pagePath).then((page: any) => {
setSchema(page.schema);
});
}, [id]);
// TODO: schema中的api有时没自动加载
return <AMisRenderer schema={schema} centre_id={centre_id} />;
})
);

35
src/pages/Student.tsx Normal file
View File

@@ -0,0 +1,35 @@
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { inject, observer } from 'mobx-react';
import AMisRenderer from '../components/AMisRenderer';
import { IMainStore } from '../stores';
export default inject('store')(
observer(function ({
page,
store,
location,
history,
match,
}: { page: number } & { store: IMainStore } & RouteComponentProps<{
id: string;
}>) {
const schema = {
type: 'page',
body: [
{
type: 'tpl',
tpl: '请使用微信选课',
},
],
aside: [],
messages: {},
// "initApi": {
// "method": "get",
// "url": "/api/user2schedules"
// }
};
return <AMisRenderer schema={schema} />;
})
);

View File

@@ -0,0 +1,9 @@
import schema2component from '@/utils/schema2component';
const schema = {
type: 'page',
title: 'Dashboard',
body: [],
};
export default schema2component(schema);

346
src/pages/admin/index.tsx Normal file
View File

@@ -0,0 +1,346 @@
import RouterGuard from '@/routes/RouterGuard';
import { IMainStore } from '@/stores';
import { AsideNav, Button, Layout, toast } from 'amis';
import { inject, observer } from 'mobx-react';
import * as React from 'react';
import {
Link,
Redirect,
Route,
RouteComponentProps,
Switch,
matchPath,
} from 'react-router-dom';
import UserInfo from '../../components/UserInfo';
import SchemaRoute from '../Schema';
import Student from '../Student';
import AMisRenderer from '@/components/AMisRenderer';
type NavItem = {
label: string;
children?: Array<NavItem>;
icon?: string;
path?: string;
component?: React.ReactType;
getComponent?: () => Promise<React.ReactType>;
};
let PATH_PREFIX = '';
function isActive(link: any, location: any) {
const ret = matchPath(location.pathname, {
path: link ? link.replace(/\?.*$/, '') : '',
exact: true,
strict: true,
});
return !!ret;
}
export interface AdminProps extends RouteComponentProps<any> {
store: IMainStore;
}
@inject('store')
@observer
export default class Admin extends React.Component<AdminProps, any> {
state = {
pathname: '',
hasLoadMenu: false,
navigations: [],
};
logout = () => {
const store = this.props.store;
store.user.logout();
const history = this.props.history;
history.replace(`/login`);
};
componentDidMount() {
const store = this.props.store;
const history = this.props.history;
console.log('componentDidMount, store.user:', store.user);
if (!store.user.isAuthenticated) {
// toast['error']('用户未登陆,请先登陆!', '消息');
history.replace(`/login`);
}
this.refreshMenu();
}
refreshMenu = () => {
const store = this.props.store;
let pathname = this.props.location.pathname;
console.log('location:', pathname);
console.log('store.user:', store.user);
if (
pathname !== 'login' &&
!this.state.hasLoadMenu &&
store.user.isAuthenticated
) {
store.updateCentres();
store.updateSemesters();
store.updatePeriods();
}
};
renderHeader() {
const store = this.props.store;
const theme = 'cxd';
return (
<div>
<div className={`${theme}-Layout-brandBar`}>
<button
onClick={store.toggleOffScreen}
className="pull-right visible-xs"
>
<i className="glyphicon glyphicon-align-justify"></i>
</button>
<div className={`${theme}-Layout-brand`}>
<i className="fa "></i>
<span className="hidden-folded">{store.name}</span>
</div>
</div>
<div
className={`${theme}-Layout-headerBar`}
style={{ justifyContent: 'space-between' }}
>
<div className="nav navbar-nav hidden-xs">
<Button
level="link"
className="no-shadow navbar-btn"
onClick={store.toggleAsideFolded}
tooltip="展开或收起侧边栏"
placement="bottom"
iconOnly
>
<i
className={store.asideFolded ? 'fa fa-indent' : 'fa fa-dedent'}
/>
</Button>
<Button
onClick={()=>{
window.open('/doc')
}}
></Button>
</div>
<div className="hidden-xs">
<UserInfo user={store.user} />
</div>
</div>
</div>
);
}
renderCentreAside(
prefix: string,
items: Array<NavItem>,
centre_id: number
): Array<NavItem> {
if (!items) {
return [];
}
return items.map((item: any) => {
if (item.children && item.children.length > 0) {
return {
...item,
children: this.renderCentreAside(prefix, item.children, centre_id),
};
}
// let centreSubPath = item.path && item.path.replace('centre/:id/', '');
return {
...item,
path: `${prefix}/${item.id}?centre_id=${centre_id}`,
children: undefined,
};
});
}
renderAside() {
const location = this.props.location;
const store = this.props.store;
let navigations = store.navigations.filter(
(item: any) => item.label !== '教学中心'
);
let centre_navigations = store.navigations.filter(
(item: any) => item.label === '教学中心'
);
let dynamic_navigations: Array<NavItem> = [];
this.props.store.centres?.forEach((item: any) => {
let centre_path = `centre/${item.id}`;
dynamic_navigations.push({
label: item.name,
icon: 'fa fa-institution',
// path: centre_path,
children: centre_navigations[0]
? this.renderCentreAside(
centre_path,
centre_navigations[0].children,
item.id
)
: [],
});
});
return (
<AsideNav
key={store.asideFolded ? 'folded-aside' : 'aside'}
navigations={[{ children: navigations.concat(dynamic_navigations) }]}
renderLink={({ link, toggleExpand, classnames: cx, depth }: any) => {
if (link.hidden) {
return null;
}
let children = [];
if (link.children && link.children.length > 0) {
children.push(
<span
key="expand-toggle"
className={cx('AsideNav-itemArrow')}
onClick={(e) => toggleExpand(link, e)}
></span>
);
} else {
if (
process.env.NODE_ENV === 'development' &&
store.user.role === 'dev'
) {
children.push(
<i
key="edit"
data-tooltip="编辑"
data-position="bottom"
className={'navbtn fa fa-pencil'}
onClick={(e: React.MouseEvent) => {
e.preventDefault();
this.props.history.push(`/edit/${link.page}`);
}}
/>
);
}
}
link.badge &&
children.push(
<b
key="badge"
className={cx(
`AsideNav-itemBadge`,
link.badgeClassName || 'bg-info'
)}
>
{link.badge}
</b>
);
if (link.icon) {
children.push(
<i key="icon" className={cx(`AsideNav-itemIcon`, link.icon)} />
);
} else if (store.asideFolded && depth === 1) {
children.push(
<i
key="icon"
className={cx(
`AsideNav-itemIcon`,
link.children ? 'fa fa-folder' : 'fa fa-info'
)}
/>
);
}
children.push(
<span key="label" className={cx('AsideNav-itemLabel')}>
{link.label}
</span>
);
return link.schema_path ? (
link.active ? (
<a>{children}</a>
) : (
<Link to={`/page/${link.path}`}>{children}</Link>
)
) : (
<a
onClick={
link.onClick
? link.onClick
: link.children
? () => toggleExpand(link)
: undefined
}
>
{children}
</a>
);
}}
// renderSubLinks={({link,renderLink, depth}:any) =>{
// return (link.children && link.children.length ? (
// <ul className={'AsideNav-subList'}>
// {link.label ? (
// <li key="subHeader" className={'AsideNav-subHeader'}>
// <a>{link.label}</a>
// </li>
// ) : null}
// {link.children.map((link:any, key:any) =>
// renderLink(link, key, {}, depth + 1)
// )}
// </ul>
// ) : link.label && depth === 1 ? (
// <div className={'AsideNav-tooltip'}>{link.label}</div>
// ) : null)
// }}
isActive={(link: any) =>
isActive(
link.path && link.path[0] === '/'
? link.path
: `${PATH_PREFIX}/page/${link.path}`,
location
)
}
/>
);
}
render() {
const store = this.props.store;
let pathname = this.props.location.pathname;
console.log('location:', pathname);
if (pathname === 'login') {
return (
<Switch>
<RouterGuard />
<Redirect to={`/404`} />
</Switch>
);
} else {
return (
<>
<Layout
theme="cxd"
aside={this.renderAside()}
header={this.renderHeader()}
folded={store.asideFolded}
offScreen={store.offScreen}
>
<Switch>
<Redirect to={`/${store.defaultNav}`} from={`/`} exact />
<Route
path="/page/centre/:centre_id/:id"
component={SchemaRoute}
/>
<Route path="/page/:id" component={SchemaRoute} />
<RouterGuard />
<Redirect to={`/404`} />
</Switch>
</Layout>
</>
);
}
}
}

View File

@@ -0,0 +1,66 @@
const schema = {
body: [
{
type: 'button',
label: '软件下载',
size: 'md',
level: 'primary',
block: false,
onEvent: {
click: {
actions: [
{
args: {
url: 'https://www.platosoft.org/download/ATT.zip',
},
actionType: 'url',
},
],
weight: 0,
},
},
},
{
type: 'button',
label: '使用说明',
actionType: 'url',
size: 'md',
level: 'primary',
className: 'm-l',
onEvent: {
click: {
actions: [
{
args: {
url: 'https://www.platosoft.org/doc/olms/manual/admin/att_qr.html',
},
actionType: 'url',
},
],
weight: 0,
},
},
},
{
type: 'formula',
name: 'api_url',
formula: 'data.api_url=location.origin + this.basePath + "/api"',
},
{
type: 'property',
title: '配置参数',
items: [
{ label: '接口地址', content: '$api_url', span: 1 },
{ label: '本地秘钥', content: 'olms', span: 1 },
],
column: 1,
mode: 'table',
className: 'w-xxl m-t',
},
],
title: '二维码考勤',
type: 'page',
messages: {},
};
export { schema };

View File

@@ -0,0 +1,28 @@
const schema = {
body: [{ type: 'html', html: '<h1>人脸考勤使用说明</h1>' }],
type: 'page',
title: '人脸考勤使用说明',
toolbar: [
{
type: 'button',
label: '下载客户端',
level: 'success',
blank: true,
onEvent: {
click: {
actions: [
{
args: {
url: '/download/faceApp.zip',
},
actionType: 'url',
},
],
weight: 0,
},
},
},
],
};
export { schema };

View File

@@ -0,0 +1,461 @@
const schema = {
body: [
{
type: 'form',
className: '',
name: 'filter',
title: '表单',
wrapWithPanel: false,
reload:
'adopt?organization_id=eq.$centre_id&project_id=$project_id&teacher_id=$teacher_id&location_id=$location_id&date=$date&class_list=$class_list&no_tch=$no_tch',
submitOnInit: true,
submitOnChange: true,
initApi: {
method: 'get',
url: 'rest/users?role=eq.teacher&id=eq.${user.id}&order=code',
adaptor:
"if (payload.data.items.length !== 0) {\r\n return {\r\n teacher_id: 'eq.'.concat(payload.data.items[0].id)\r\n }\r\n} else {\r\n return {}\r\n}",
},
body: [
{
type: 'group',
body: [
{
type: 'select',
name: 'project_id',
label: '实验',
mode: 'inline',
source: {
method: 'get',
url: 'rest/projects?organization_id=eq.$centre_id&semester_id=eq.$currentSemester&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
size: 'md',
checkAll: false,
clearable: true,
inputClassName: 'm-l-xs',
},
],
},
{
type: 'group',
body: [
{
type: 'input-date-range',
name: 'date',
mode: 'inline',
label: '日期',
clearable: true,
inputClassName: 'm-l-xs',
value: '',
format: 'YYYY-MM-DD',
ranges:
'yesterday,today,7daysago,prevweek,thismonth,prevmonth,prevquarter',
},
{
type: 'select',
label: '地点',
name: 'location_id',
mode: 'inline',
checkAll: false,
clearable: true,
source: {
method: 'get',
url: 'rest/locations?organization_id=eq.$centre_id&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
size: 'md',
className: 'm-l-sm',
inputClassName: 'm-l-xs',
},
{
type: 'select',
name: 'teacher_id',
label: '教师',
checkAll: false,
source: {
method: 'get',
url: 'rest/users?role=eq.teacher&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
mode: 'inline',
size: 'md',
clearable: true,
className: 'm-l-sm',
inputClassName: 'm-l-xs',
},
],
},
{
type: 'group',
body: [
{
type: 'tree-select',
label: '预留班级',
name: 'class_list',
mode: 'horizontal',
searchable: true,
clearable: true,
multiple: true,
joinValues: true,
onlyChildren: true,
source: {
method: 'get',
url: 'rest/orgs?select=*&path=cd.root&order=name',
adaptor:
"const recursive = id => {\r\n let nodes = payload.data.items.filter(x => {\r\n return x.parent === id\r\n })\r\n\r\n return nodes.map(item => {\r\n return {\r\n ...item,\r\n label: item.name,\r\n value: item.id,\r\n children: recursive(item.id)\r\n }\r\n })\r\n}\r\n\r\nlet school = payload.data.items.find(x => x.type === 'school')\r\n\r\nreturn {\r\n ...payload,\r\n data: recursive(school.id)\r\n}\r\n",
},
withChildren: true,
initiallyOpen: false,
heightAuto: true,
virtualThreshold: false,
},
],
},
{
type: 'group',
body: [
{
type: 'switch',
name: 'no_tch',
mode: 'inline',
value: 0,
option: '显示无教师场次',
optionAtLeft: false,
trueValue: 1,
falseValue: 0,
},
],
},
],
mode: 'inline',
},
{ type: 'divider', lineStyle: 'solid' },
{
type: 'crud',
messages: {},
api: {
method: 'get',
url: 'rest/schedule_stats?organization_id=eq.$centre_id&semester_id=eq.$currentSemester&is_publish=is.true&order=date.asc,start_time.asc',
data: { page: '$page', perPage: '$perPage', '&': '$$' },
adaptor:
"let supportDateTimeFormat = typeof(Intl.DateTimeFormat) === 'function';\r\npayload.data.items.forEach((item,index) => {\r\n item.day = supportDateTimeFormat ? `${item.date}${new Intl.DateTimeFormat('zh-CN', { weekday: 'short'}).format(new Date(item.date))}` : item.date;\r\n item.row_number = index + 1\r\n + (api.body.page - 1)\r\n * api.body.perPage\r\n let att_count = item.att_y_count\r\n + item.att_y_late_count\r\n + item.att_y_unclean_count\r\n item.att_num = \"\".concat(att_count, '/', item.current_student_number)\r\n item.att_num_status = att_count === item.current_student_number\r\n ? 1\r\n : 0\r\n item.stu_title = item.project_name.concat(\r\n ' :: ', item.date, ' ', item.period_name,\r\n ' :: ', item.location_name,\r\n ' :: ', item.teacher_name ? item.teacher_name : '无教师认领'\r\n )\r\n})\r\n\r\nreturn {\r\n ...payload\r\n}",
requestAdaptor:
"let url = api.url.replace(/project_id=&|teacher_id=&|date=&|location_id=&|no_tch=&|no_tch=0/g, '')\r\n\r\nif (api.body.class_list === '') {\r\n url = url.replace('&class_list=', '')\r\n} else {\r\n let class_list = api.body.class_list && api.body.class_list.replace(',', '%2C')\r\n url = url.replace(class_list, 'cs.{'.concat(class_list, '}'))\r\n}\r\n\r\nconst pattern = /(?:date=)(\\d+-\\d+-\\d+)%2C(\\d+-\\d+-\\d+)/g\r\nconst result = pattern.exec(url)\r\nif (result && result[1] === result[2]) {\r\n url = url.replace(result[0], 'date=eq.'.concat(result[1]))\r\n} else if (result) {\r\n url = url.replace(result[0], 'date=gte.'.concat(result[1], '&date=lte.', result[2]))\r\n} else {\r\n url = url.replace('&date=','')\r\n}\r\nconst is_no_tch = /no_tch=1/g.test(url)\r\nconst has_teacher = /teacher_id=.+?(?=&)/g.test(url)\r\nif (is_no_tch && has_teacher) {\r\n const teacher = /teacher_id=.+?(?=&)/g.exec(url)\r\n url = url.replace(teacher, 'or=('.concat(teacher, ',teacher_id.is.null)')).replace('teacher_id=', 'teacher_id.')\r\n url = url.replace(/no_tch=1/g, '')\r\n} else if (is_no_tch && !has_teacher) {\r\n url = url.replace(/no_tch=1/g, 'teacher_id=is.null')\r\n}\r\nreturn {\r\n ...api,\r\n url: url\r\n}",
},
syncLocation: false,
name: 'adopt',
columns: [
{
name: 'row_number',
type: 'plain',
label: '序号',
tpl: '',
inline: false,
},
{
name: '',
type: 'tpl',
placeholder: '-',
label: '实验',
tpl: '<% if (this.scheduled_class_name) { %>\n <%= this.project_name %><%= this.scheduled_class_name %>\n<% } else { %>\n <%= this.project_name %>\n<% } %>',
},
{ name: 'day', label: '日期', type: 'text', placeholder: '-' },
{ type: 'text', name: 'period_name', label: '节次', placeholder: '-' },
{
type: 'text',
name: 'location_name',
placeholder: '-',
label: '地点',
},
{
type: 'tpl',
label: '教师',
name: '',
placeholder: '-',
tpl: '<% if (this.teacher_name) { %>\n <span class="label label-info" style="font-size: smaller;"><%= this.teacher_name %></span>\n<% } else { %>\n <span class="label label-warning" style="font-size: smaller;">未认领</span>\n<% } %>',
inline: false,
},
{
type: 'tpl',
label: '考勤状态',
placeholder: '-',
tpl: '<% if(this.att_num_status) { %>\n <span class="label label-success"><%= this.att_num %></span>\n<% } else { %>\n <span class="label label-warning"><%= this.att_num %></span>\n<% } %>',
inline: false,
},
{
type: 'button-group',
label: '操作',
buttons: [
{
label: '认领场次',
type: 'button',
actionType: 'ajax',
level: 'success',
size: 'xs',
visibleOn:
"this.enableClaim && (this.user.role === 'teacher' && !this.teacher_id)",
api: {
method: 'patch',
url: 'rest/schedules/$id',
data: { teacher_id: '${user.id}' },
},
},
{
type: 'button',
label: '取消认领',
actionType: 'ajax',
level: 'danger',
size: 'xs',
visibleOn:
'this. enableClaim && this.teacher_code === this.user.code',
api: {
method: 'patch',
url: 'rest/schedules/$id',
data: { teacher_id: null },
},
},
{
label: '查看场次',
type: 'button',
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '学生信息',
body: [
{
type: 'crud',
api: {
method: 'get',
url: 'rest/user2projects?select=id,att_status,att_status_text:user2project_att_status_dict_fk(dictvalue),att_time,score,schedule_status:user2project_schedule_status_dict_fk(dictvalue),student:users!user2project_student_id_fkey(id,code,name,user_orgs(orgs(name)))&schedule_id=eq.${id}&schedule_status=in.(elected,stopped)&order=ordernumber_of_schedule',
data: null,
adaptor:
'payload.data.items.forEach((item,index) => {\r\n item.row_number=index+1;\r\n})\r\nreturn {\r\n ...payload\r\n}',
},
columns: [
{
label: '序号',
type: 'plain',
inline: false,
name: 'row_number',
tpl: '',
},
{
label: '选课状态',
type: 'plain',
name: 'schedule_status.dictvalue',
placeholder: '-',
inline: false,
},
{
type: 'text',
label: '学生',
name: 'student.name',
placeholder: '-',
},
{
type: 'text',
label: '学号',
name: 'student.code',
placeholder: '-',
},
{
type: 'text',
label: '班级',
name: 'student.user_orgs[0].orgs.name',
placeholder: '-',
},
{
type: 'date',
label: '出勤',
name: 'att_time',
placeholder: '-',
format: 'YYYY-MM-DD HH:mm:ss',
},
{
type: 'text',
label: '考勤状态',
name: 'att_status_text.dictvalue',
placeholder: '-',
groupName: '',
remark: '',
inline: true,
visibleOn:
"this.user.role !== 'teacher' || (this.user.role === 'teacher' && this.teacher_id === user.id)",
quickEdit: {
mode: 'popOver',
type: 'select',
label: '',
name: 'att_status',
checkAll: false,
source: {
method: 'get',
url: 'rest/dicts?typecode=eq.014',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.dictvalue,\r\n value: item.dictkey\r\n }\r\n })\r\n}',
},
size: 'sm',
saveImmediately: {
api: {
method: 'patch',
url: 'rest/user2projects/${id}',
data: { att_status: '${att_status}' },
dataType: 'json',
requestAdaptor:
"let temp = api.data.att_status.slice(0, 5);\r\n\r\nif (temp === 'att_y') {\r\n let date = new Date();\r\n api.data.att_time=`${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;\r\n} else {\r\n api.data.att_time = null;\r\n}\r\n\r\nreturn api;",
},
},
},
},
{
type: 'text',
label: '考勤状态',
name: 'att_status_text.dictvalue',
placeholder: '-',
groupName: '',
remark: '',
inline: true,
visibleOn:
"this.user.role === 'teacher' && this.teacher_id !== user.id",
},
],
messages: {},
syncLocation: false,
bodyClassName: '',
className: '',
headerToolbar: [
'bulkActions',
{
type: 'tpl',
tpl: '${stu_title}',
},
{
type: 'button',
label: '打印',
size: 'sm',
level: 'primary',
className: 'pull-right',
id: 'u:114eb8de43c8',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: true,
actionType: 'url',
args: {
url: '/admin/preview?id=${id}&project=${project_name}&location=${location_name}&teacher=${teacher_name}&date=${date}&start_time=${start_time}',
},
},
],
},
},
},
{
type: 'export-excel',
label: '全量导出 Excel',
api: {
method: 'get',
url: 'rest/user2projects?select=id,att_status,att_status_text:user2project_att_status_dict_fk(dictvalue),att_time,score,schedule_status:user2project_schedule_status_dict_fk(dictvalue),student:users!user2project_student_id_fkey(id,code,name,user_orgs(orgs(name)))&schedule_id=eq.${id}&schedule_status=in.(elected,stopped)&order=id.asc',
data: null,
adaptor:
'payload.data.items.forEach((item,index) => {\r\n item.row_number=index+1;\r\n})\r\nreturn {\r\n ...payload\r\n}',
},
filename: '考勤信息',
align: 'right',
id: 'u:d74fe0b275db',
},
],
bulkActions: [
{
label: '批量考勤',
actionType: 'dialog',
type: 'button',
dialog: {
type: 'dialog',
title: '批量考勤',
body: [
{
type: 'form',
title: '表单',
controls: [
{
type: 'select',
label: '选项',
name: 'att',
options: [],
checkAll: false,
source: {
method: 'get',
url: 'rest/dicts?typecode=eq.014',
adaptor:
'return {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n label: item.dictvalue,\r\n value: item.dictkey\r\n }\r\n })\r\n}',
},
},
{ type: 'hidden', name: 'id', value: '${ids}' },
],
wrapWithPanel: false,
initApi: '',
api: {
method: 'patch',
url: 'rest/user2projects?id=in.(${ids})',
data: { att_status: '$att' },
requestAdaptor:
"let temp = api.data.att_status.slice(0, 5);\r\n\r\nif (temp === 'att_y') {\r\n let date = new Date();\r\n api.data.att_time=`${date.getFullYear()}-${date.getMonth()+1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;\r\n} else {\r\n api.data.att_time = null;\r\n}\r\n\r\nreturn api;",
},
},
],
closeOnEsc: true,
showCloseButton: true,
},
visibleOn:
"this.user.role !== 'teacher' || (this.user.role === 'teacher' && this.teacher_id === user.id)",
},
],
footerToolbar: [],
perPageField: '',
pageField: '',
affixHeader: false,
perPageAvailable: [10],
hideQuickSaveBtn: true,
},
],
closeOnEsc: true,
closeOnOutside: false,
showCloseButton: true,
size: 'lg',
actions: [],
},
level: 'primary',
size: 'xs',
reload: 'adopt',
},
],
},
],
headerToolbar: [],
footerToolbar: [
{ type: 'pagination' },
{ type: 'switch-per-page' },
{ type: 'statistics' },
],
perPageAvailable: [10, 20, 30, 40, 50],
},
],
type: 'page',
messages: {},
bodyClassName: '',
title: '',
style: {},
initApi: {
method: 'get',
url: 'rest/config?key=eq.system&organization_id=eq.${centre_id}',
adaptor:
'let item = payload.data.items[0];\r\nreturn {\r\n ...payload,\r\n data: {\r\n enableClaim: payload.data.items[0].value.enableClaim\r\n }\r\n}',
},
};
export { schema };

View File

@@ -0,0 +1,305 @@
const schema = {
body: [
{
type: 'panel',
className: 'Panel--default',
title: '',
body: [
{
type: 'crud',
api: {
method: 'get',
url: 'rest/users_ex?select=organization_id:org_id,id,code,name,machine_locations(locations(id,name))&role=eq.machine&org_id=eq.$centre_id&order=code',
adaptor:
"return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n id: item.id,\r\n code: item.code,\r\n name: item.name,\r\n machine_locations: item.machine_locations,\r\n select_loc: item.machine_locations.map(item => {\r\n return ''.concat(item.locations.id)\r\n })\r\n }\r\n })\r\n}",
},
mode: 'cards',
messages: {},
card: {
type: 'card',
header: { title: '${code}', subTitle: '${name}' },
body: [
{ type: 'divider', label: '关联场地' },
{
type: 'button-group',
label: '',
buttons: [
{
type: 'button',
label: '',
actionType: 'dialog',
dialog: {
title: '关联场地',
body: [
{
type: 'form',
api: {
method: 'post',
url: 'rest/machine_locations',
dataType: 'json',
requestAdaptor:
'return {\r\n ...api,\r\n data: api.data.location_id.map(item => {\r\n return {\r\n user_id: api.data.id,\r\n location_id: item\r\n }\r\n })\r\n}',
adaptor: '',
},
title: '表单',
initApi: '',
body: [
{
type: 'checkboxes',
label: '场地',
mode: '',
name: 'location_id',
required: true,
options: [],
joinValues: false,
source: {
method: 'get',
url: 'rest/locations?id=not.in.(${select_loc})&organization_id=eq.$centre_id&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => ({\r\n label: item.name,\r\n value: item.id\r\n }))\r\n}',
requestAdaptor: '',
},
extractValue: true,
},
],
},
],
type: 'dialog',
closeOnEsc: true,
showCloseButton: true,
},
icon: 'fa fa-plus text-primary',
level: 'link',
size: 'md',
tooltip: '关联场地',
tooltipPlacement: 'top',
},
],
},
{
type: 'each',
name: 'machine_locations',
items: [
{
type: 'container',
body: [
{
type: 'tpl',
tpl: '${locations.name}',
inline: true,
},
{
type: 'button',
actionType: 'ajax',
confirmText: '确认取消关联"${locations.name}"',
api: {
method: 'delete',
url: 'rest/machine_locations?user_id=eq.$id&location_id=eq.${locations.id}',
},
size: 'xs',
icon: 'fa fa-times text-danger',
level: 'link',
tooltip: '取消场地关联',
tooltipPlacement: 'top',
},
],
className: 'wrapper',
},
],
placeholder: '暂无内容',
className: '',
},
],
actions: [
{
type: 'button',
label: '',
actionType: 'dialog',
dialog: {
body: [
{
type: 'form',
title: '表单',
api: {
method: 'patch',
url: 'rest/users/$id',
data: {
code: '$code',
name: '$name',
},
dataType: 'json',
},
body: [
{
type: 'input-text',
label: '账号',
name: 'code',
required: true,
mode: 'normal',
},
{
label: '名称',
type: 'input-text',
name: 'name',
required: true,
mode: 'normal',
},
],
},
],
type: 'dialog',
title: '修改考勤账号',
closeOnEsc: true,
showCloseButton: true,
},
icon: 'fa fa-edit text-info',
level: 'link',
tooltip: '修改考勤账号',
tooltipPlacement: 'top',
},
{
type: 'button',
label: '',
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '修改密码',
body: [
{
type: 'form',
title: '表单',
rules: [
{
rule: 'this.new_password === this.password',
message: '两次密码输入不一致',
},
],
api: {
method: 'post',
url: 'rest/rpc/change_password',
data: { password: '$password', user_id: '$id' },
},
body: [
{
label: '新密码',
type: 'input-password',
name: 'new_password',
required: true,
mode: 'normal',
},
{
type: 'input-password',
label: '确认密码',
name: 'password',
required: true,
mode: 'normal',
},
],
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'md',
},
icon: 'fa fa-cog text-warning',
tooltip: '修改密码',
tooltipPlacement: 'top',
},
{
type: 'button',
label: '',
actionType: 'ajax',
icon: 'fa fa-trash text-danger',
confirmText: '确认删除"${name}(${code})"',
api: { method: 'delete', url: 'rest/users/$id' },
level: 'link',
tooltip: '删除考勤账号',
tooltipPlacement: 'top',
},
],
},
footerToolbar: [],
bulkActions: [],
className: '',
},
],
header: [
{
type: 'button',
label: '添加考勤账号',
actionType: 'dialog',
dialog: {
title: '添加考勤账号',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'rest/rpc/create_user',
data: {
role: 'machine',
orgs_id: '$centre_id',
code: '$code',
name: '$name',
password: '$password',
},
dataType: 'json',
headers: { Prefer: 'params=single-object' },
requestAdaptor: '',
},
rules: [
{
rule: 'this.new_password === this.password',
message: '两次密码输入不一致',
},
],
messages: {},
body: [
{
type: 'input-text',
name: 'code',
label: '账号',
required: true,
mode: 'normal',
},
{
type: 'input-text',
label: '名称',
name: 'name',
required: true,
mode: 'normal',
},
{
label: '密码',
type: 'input-password',
name: 'new_password',
required: true,
mode: 'normal',
},
{
type: 'input-password',
label: '确认密码',
name: 'password',
required: true,
mode: 'normal',
},
],
},
],
type: 'dialog',
closeOnEsc: true,
showCloseButton: true,
},
level: 'primary',
align: 'left',
className: 'm-t-xs m-l',
},
],
headerClassName: 'panel-heading',
},
],
type: 'page',
};
export { schema };

View File

@@ -0,0 +1,316 @@
const schema = {
body: [
{
type: 'crud',
columns: [
{
type: 'text',
name: 'code',
label: '账号',
placeholder: '-',
id: 'u:c3eb8f9b177e',
},
{
type: 'text',
name: 'name',
label: '姓名',
placeholder: '-',
id: 'u:6f74a11657ef',
},
{
type: 'button-group',
label: '操作',
buttons: [
{
type: 'button',
level: 'link',
size: 'md',
icon: 'fa fa-pencil text-info',
visibleOn:
"this.user.role === 'dev' || this.user.role === 'admin'",
tooltip: '修改',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
id: 'u:13770aa6289a',
editorState: 'default',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
title: '修改账号',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'patch',
url: 'rest/users/$id',
data: {
code: '$code',
name: '$name',
},
dataType: 'json',
},
body: [
{
label: '账号',
type: 'input-text',
name: 'code',
required: true,
mode: 'normal',
id: 'u:a614008003f6',
},
{
type: 'input-text',
label: '姓名',
name: 'name',
required: true,
mode: 'normal',
id: 'u:5973b1eb26ab',
},
],
id: 'u:c59fb46c3665',
actions: [
{
type: 'submit',
label: 'Submit',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:366fea2f5de5',
ignoreError: false,
actionType: 'reload',
},
],
},
},
},
],
type: 'dialog',
closeOnEsc: true,
showCloseButton: true,
},
},
],
debounce: { wait: 100 },
},
},
},
{
type: 'button',
confirmText: '确认删除账号"${name}(${code})"',
level: 'link',
icon: 'fa fa-times text-danger',
size: 'md',
tooltip: '删除',
tooltipPlacement: 'bottom',
visibleOn:
"this.user.role === 'dev' || this.user.role === 'admin'",
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
id: 'u:221693061e9e',
editorState: 'default',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
outputVar: 'responseResult',
actionType: 'ajax',
options: {},
api: {
url: 'rest/users/$id',
method: 'delete',
requestAdaptor: '',
adaptor: '',
messages: {},
},
},
{
componentId: 'u:366fea2f5de5',
ignoreError: false,
actionType: 'reload',
},
],
debounce: { wait: 100 },
},
},
},
],
placeholder: '-',
width: 200,
id: 'u:96b309d3bf00',
},
],
messages: {},
api: {
method: 'get',
url: 'rest/users?select=*&role=eq.centreadmin&user_orgs.orgs.id=eq.$centre_id&order=code',
data: { page: '$page', perPage: '$perPage' },
adaptor:
'payload.data.items.sort(\r\n (a, b) => {\r\n if (a.name === b.name) {\r\n return a.code.localeCompare(b.code)\r\n } else {\r\n return a.name.localeCompare(b.name)\r\n }\r\n }\r\n)\r\n\r\nreturn {\r\n ...payload\r\n}',
},
headerToolbar: [
{
type: 'button',
icon: 'fa fa-plus text-primary',
size: 'lg',
level: 'link',
tooltip: '添加',
tooltipPlacement: 'left',
align: 'right',
label: '',
visibleOn: "this.user.role === 'dev' || this.user.role === 'admin'",
className: 'p-l-none',
id: 'u:76187972a571',
editorState: 'default',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '添加管理员',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'rest/rpc/create_user',
data: {
code: '$code',
name: '$name',
password: '$password',
new_password: '$new_password',
role: 'centreadmin',
orgs_id: '$centre_id',
},
dataType: 'json',
headers: { Prefer: 'params=single-object' },
},
body: [
{
type: 'input-text',
label: '账号',
name: 'code',
required: true,
mode: 'normal',
id: 'u:6dca4e78622c',
},
{
type: 'input-text',
name: 'name',
label: '姓名',
mode: 'normal',
required: true,
id: 'u:56021033191e',
},
{
type: 'input-password',
name: 'password',
label: '密码',
mode: 'normal',
required: true,
id: 'u:1b5ed3926797',
},
{
type: 'input-password',
name: 'new_password',
label: '确认密码',
mode: 'normal',
required: true,
id: 'u:7c1e9ce050c0',
},
],
rules: [
{
rule: 'this.password === this.new_password',
message: '两次密码输入不一致,请重新输入',
},
],
id: 'u:6ecfef8353a7',
actions: [
{ type: 'submit', label: '提交', primary: true },
],
feat: 'Insert',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:366fea2f5de5',
ignoreError: false,
actionType: 'reload',
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
id: 'u:6179e3a79d93',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:e62e24a0fc22',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:f7524893881b',
},
],
},
},
],
debounce: { wait: 100 },
},
},
},
],
syncLocation: false,
perPageAvailable: [10, 20, 30, 40, 50],
name: 'admin',
footerToolbar: [
{ type: 'pagination' },
{ type: 'switch-per-page' },
{ type: 'statistics' },
],
perPageField: 'perPage',
bulkActions: [],
itemActions: [],
pageField: 'page',
affixHeader: true,
id: 'u:366fea2f5de5',
},
],
type: 'page',
messages: {},
title: '',
perPageAvailable: [10, 20, 30, 40, 50],
id: 'u:4c19431ee5db',
};
export { schema };

View File

@@ -0,0 +1,474 @@
const schema = {
type: 'page',
body: [
{
type: 'crud',
columns: [
{
type: 'text',
name: 'code',
label: '账号',
sortable: true,
searchable: true,
id: 'u:91821b736555',
},
{
type: 'text',
name: 'name',
label: '姓名',
sortable: true,
searchable: true,
id: 'u:a81612f6d91c',
},
{
type: 'button-group',
label: '操作',
buttons: [
{
type: 'button',
level: 'link',
size: 'md',
icon: 'fa fa-pencil text-info',
tooltip: '修改',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
label: '',
api: { method: 'get', url: 'rest/users/$id' },
id: 'u:483a2e18377b',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
title: '修改账号',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'patch',
url: 'rest/users/$id',
data: { code: '$code', name: '$name' },
dataType: 'json',
},
mode: 'normal',
body: [
{
label: '账号',
type: 'input-text',
name: 'code',
required: true,
id: 'u:bcbd9ab0fd9e',
},
{
type: 'input-text',
label: '姓名',
name: 'name',
required: true,
id: 'u:3b7829fa16f8',
},
],
id: 'u:7a2ac32b9e2b',
actions: [
{ type: 'submit', label: '提交', primary: true },
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:f1b328630134',
ignoreError: false,
actionType: 'reload',
},
],
},
},
},
],
type: 'dialog',
closeOnEsc: true,
showCloseButton: true,
id: 'u:49ea67f4ccca',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:e22fba76fe33',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:46273e3a4090',
},
],
actionType: 'dialog',
},
},
],
},
},
},
{
type: 'button',
confirmText: '确认删除"${name}(${code})"',
level: 'link',
icon: 'fa fa-times text-danger',
size: 'md',
tooltip: '删除',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
label: '',
id: 'u:c3026515a2de',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
outputVar: 'responseResult',
actionType: 'ajax',
options: {},
api: {
url: 'rest/users/$id',
method: 'delete',
requestAdaptor: '',
adaptor: '',
messages: {},
},
},
{
componentId: 'u:f1b328630134',
ignoreError: false,
actionType: 'reload',
args: { resetPage: false },
},
],
},
},
},
],
placeholder: '-',
width: 200,
id: 'u:e8398dbbd899',
},
],
messages: {},
api: {
method: 'get',
url: 'rest/users?select=*,user_orgs(orgs(id))&code=${code}&name=${name}&role=eq.assistant&user_orgs.orgs.id=eq.$centre_id&order=code',
data: {
page: '${page}',
perPage: '${perPage}',
orderBy: '${orderBy}',
orderDir: '${orderDir}',
code: 'like.%${code}%',
name: 'like.%${name}%',
},
requestAdaptor:
"if (api.query.code === 'like.%%') {\r\n api.url = api.url.replace('&code=like.%25%25', '')\r\n}\r\n\r\nif (api.query.name === 'like.%%') {\r\n api.url = api.url.replace('&name=like.%25%25', '')\r\n}\r\n\r\nif (api.query.orderDir) {\r\n api.url = api.url.replace('&order=code%2Cname', '');\r\n api.url = api.url.replace('&orderBy=' + api.query.orderBy + '&orderDir=' + api.query.orderDir, '&order=' + api.query.orderBy + '.' + api.query.orderDir);\r\n} else if (api.query.orderBy) {\r\n api.url = api.url.replace('&order=code%2Cname', '');\r\n api.url = api.url.replace('&orderBy=' + api.query.orderBy + '&orderDir=', '&order=' + api.query.orderBy);\r\n} else {\r\n api.url = api.url.replace('&orderBy=&orderDir=', '');\r\n}\r\n\r\nreturn api;",
},
headerToolbar: [
{
type: 'button',
icon: 'fa fa-plus',
level: 'default',
align: 'right',
label: '添加',
className: 'm-l-xs',
id: 'u:ab332275c1fa',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '添加助教',
body: [
{
type: 'form',
title: '表单',
body: [
{
type: 'input-text',
name: 'code',
label: '账号',
mode: 'normal',
required: true,
id: 'u:487b8805e663',
},
{
type: 'input-text',
name: 'name',
label: '姓名',
mode: 'normal',
required: true,
id: 'u:89de05a91f91',
},
{
type: 'input-password',
name: 'password',
label: '密码',
mode: 'normal',
required: true,
id: 'u:0fb31870de06',
},
{
type: 'input-password',
name: 'new_password',
label: '确认密码',
mode: 'normal',
required: true,
id: 'u:c78103928eff',
},
],
api: {
method: 'post',
url: 'rest/rpc/create_user',
data: {
code: '$code',
name: '$name',
password: '$password',
new_password: '$new_password',
role: 'assistant',
orgs_id: '$centre_id',
},
requestAdaptor: '',
dataType: 'json',
headers: { Prefer: 'params=single-object' },
},
rules: [
{
rule: 'this.password === this.new_password',
message: '两次密码输入不一致,请重新输入',
},
],
id: 'u:b610d21f422f',
actions: [
{ type: 'submit', label: '提交', primary: true },
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:f1b328630134',
ignoreError: false,
actionType: 'reload',
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
closeOnOutside: false,
id: 'u:2fbf0844aca1',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:deb6bbf65621',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:bbdffe4baf61',
},
],
actionType: 'dialog',
},
},
],
},
},
tooltip: '',
tooltipPlacement: 'left',
},
{
type: 'button',
align: 'right',
label: '导入',
icon: 'fa fa-upload',
level: 'warning',
id: 'u:6581af7e35f0',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '助教导入',
body: [
{
type: 'form',
title: '表单',
body: [
{
type: 'input-excel',
name: 'assistants',
label: '',
mode: 'normal',
id: 'u:5f0eca8a64fc',
},
{
type: 'input-table',
name: 'assistants',
label: '',
columns: [
{
label: '账号',
name: '账号',
placeholder: '-',
quickEdit: {
mode: 'inline',
id: 'u:fcf8c77a0302',
},
id: 'u:328345552f87',
},
{
label: '姓名',
name: '姓名',
placeholder: '-',
quickEdit: {
mode: 'inline',
id: 'u:3801de195dcc',
},
id: 'u:e3d8eb8dae09',
},
],
mode: 'normal',
visibleOn: 'this.assistants',
strictMode: true,
removable: true,
editable: false,
id: 'u:d73c846629ed',
clearValueOnHidden: true,
},
],
api: {
method: 'post',
url: 'rest/rpc/create_user',
data: {
orgs_id: '${centre_id}',
assistants: '${assistants}',
},
requestAdaptor:
"return {\r\n ...api,\r\n data: api.data.assistants.map(item => {\r\n return {\r\n name: item.姓名,\r\n code: item.账号,\r\n role: 'assistant',\r\n orgs_id: api.data.orgs_id\r\n }\r\n })\r\n}",
dataType: 'json',
headers: { Prefer: 'params=single-object' },
},
visibleOn: '',
name: '',
id: 'u:feda7d9a6c8e',
actions: [
{ type: 'submit', label: '提交', primary: true },
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:f1b328630134',
ignoreError: false,
actionType: 'reload',
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'xl',
actionType: 'dialog',
id: 'u:7cc3f8103868',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:908813bc5040',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:280fe6e082d4',
},
],
closeOnOutside: false,
showErrorMsg: true,
showLoading: true,
draggable: false,
},
},
],
},
},
},
{
type: 'export-excel',
align: 'right',
label: '导出',
icon: 'fa fa-download',
level: 'primary',
api: 'rest/users?select=*,user_orgs(orgs(id))&role=eq.assistant&user_orgs.orgs.id=eq.$centre_id&order=code',
filename: '助教管理',
exportColumns: [
{ label: '账号', name: 'code' },
{ name: 'name', label: '姓名' },
],
id: 'u:556c3974c544',
},
],
syncLocation: false,
perPageAvailable: [10, 20, 30, 40, 50],
footerToolbar: [
{ type: 'pagination' },
{ type: 'switch-per-page' },
{ type: 'statistics' },
],
perPageField: 'perPage',
pageField: 'page',
affixHeader: true,
title: '',
initApi: '',
bodyClassName: '',
id: 'u:f1b328630134',
},
],
messages: {},
title: '',
id: 'u:31753e0403ea',
asideResizor: false,
pullRefresh: { disabled: true },
definitions: {},
};
export { schema };

View File

@@ -0,0 +1,493 @@
const schema = {
type: 'page',
body: [
{
type: 'crud',
columns: [
{
type: 'text',
name: 'code',
label: '编号',
sortable: true,
searchable: true,
id: 'u:f01745729d6f',
},
{
type: 'text',
name: 'name',
label: '名称',
sortable: true,
searchable: true,
id: 'u:0db1a83903b6',
},
{
type: 'text',
label: '类型',
id: 'u:00332fcfb6b6',
name: 'dicts.dictvalue',
searchable: true,
},
{
type: 'button-group',
label: '操作',
buttons: [
{
type: 'button',
actionType: 'dialog',
dialog: { $ref: 'dialog-ref-2' },
level: 'link',
size: 'md',
icon: 'fa fa-pencil text-info',
tooltip: '修改',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
id: 'u:2fbcae5dec1e',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: { $ref: 'dialog-ref-2' },
},
],
},
},
},
{
type: 'button',
confirmText: '确认删除"${name}(${code})"',
level: 'link',
icon: 'fa fa-times text-danger',
size: 'md',
tooltip: '删除',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
id: 'u:259ec9a26f44',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
outputVar: 'responseResult',
actionType: 'ajax',
options: {},
api: {
url: 'rest/locations/$id',
method: 'delete',
requestAdaptor: '',
adaptor: '',
messages: {},
},
},
{
componentId: 'u:96cb5c2c87a4',
ignoreError: false,
actionType: 'reload',
args: { resetPage: false },
},
],
},
},
},
],
placeholder: '-',
width: 200,
id: 'u:328bf6132313',
},
],
messages: {},
api: {
method: 'get',
url: 'rest/locations?select=id,code,name,type,dicts!inner(dictvalue)&code=${code}&name=${name}&dicts.dictvalue=${dicts.dictvalue}&order=code',
data: {
page: '${page}',
perPage: '${perPage}',
orderBy: '${orderBy}',
orderDir: '${orderDir}',
code: 'like.%${code}%',
name: 'like.%${name}%',
'dicts.dictvalue': 'like.%${dicts.dictvalue}%',
},
requestAdaptor:
"if (api.query.code === 'like.%%') {\r\n api.url = api.url.replace('&code=like.%25%25', '')\r\n}\r\n\r\nif (api.query.name === 'like.%%') {\r\n api.url = api.url.replace('&name=like.%25%25', '')\r\n}\r\n\r\nif (api.query.dicts.dictvalue === 'like.%%') {\r\n api.url = api.url.replace('&dicts[dictvalue]=like.%25%25', '')\r\n}\r\n\r\nif (api.query.orderDir) {\r\n api.url = api.url.replace('&order=code%2Cname', '');\r\n api.url = api.url.replace('&orderBy=' + api.query.orderBy + '&orderDir=' + api.query.orderDir, '&order=' + api.query.orderBy + '.' + api.query.orderDir);\r\n} else if (api.query.orderBy) {\r\n api.url = api.url.replace('&order=code%2Cname', '');\r\n api.url = api.url.replace('&orderBy=' + api.query.orderBy + '&orderDir=', '&order=' + api.query.orderBy);\r\n} else {\r\n api.url = api.url.replace('&orderBy=&orderDir=', '');\r\n}\r\n\r\nreturn api;",
},
headerToolbar: [
{
type: 'button',
icon: 'fa fa-plus',
level: 'default',
align: 'right',
label: '添加',
className: 'm-l-xs',
id: 'u:64fb6bec0fff',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: { $ref: 'dialog-ref-1' },
args: { fromCurrentDialog: true },
},
],
},
},
actionType: 'dialog',
dialog: { $ref: 'dialog-ref-1' },
},
{
type: 'button',
align: 'right',
label: '导入',
icon: 'fa fa-upload',
level: 'warning',
id: 'u:a2c775bee2a9',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '场地导入',
size: 'lg',
body: [
{
type: 'form',
id: 'u:2d97745994e5',
title: '表单',
body: [
{
type: 'input-excel',
name: 'locations',
label: '',
mode: 'normal',
id: 'u:5f0eca8a64fc',
},
{
type: 'input-table',
name: 'locations',
label: '',
columns: [
{
label: '编号',
name: '编号',
placeholder: '-',
quickEdit: {
mode: 'inline',
id: 'u:931a41e1011d',
},
id: 'u:0c6672aa1858',
},
{
label: '名称',
name: '名称',
placeholder: '-',
quickEdit: {
mode: 'inline',
id: 'u:6ebc04992e03',
},
id: 'u:a03852887668',
},
{
label: '类型',
name: '类型',
placeholder: '-',
quickEdit: {
mode: 'inline',
id: 'u:a75f5e419328',
},
id: 'u:efedd284d265',
},
],
mode: 'normal',
visibleOn: 'this.locations',
strictMode: true,
removable: true,
editable: false,
id: 'u:d73c846629ed',
clearValueOnHidden: true,
},
],
api: {
method: 'post',
url: 'rest/locations',
data: {
orgs_id: '${centre_id}',
locations: '${locations}',
},
requestAdaptor:
"return {\r\n ...api,\r\n data: api.data.locations.map(item => {\r\n let type;\r\n switch (item.类型)\r\n {\r\n case '实验室':\r\n type = 'laboratory';\r\n break;\r\n case '教室':\r\n type = 'classroom';\r\n break;\r\n case '办公室':\r\n type = 'office';\r\n break;\r\n case '仓库':\r\n typ = 'warehouse';\r\n break;\r\n }\r\n\r\n return {\r\n name: item.名称,\r\n code: item.编号,\r\n type: type,\r\n organization_id: api.data.orgs_id\r\n }\r\n })\r\n}",
dataType: 'json',
},
visibleOn: '',
name: '',
actions: [
{ type: 'submit', label: '提交', primary: true },
],
feat: 'Insert',
dsType: 'api',
labelAlign: 'left',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:96cb5c2c87a4',
ignoreError: false,
actionType: 'reload',
},
],
},
},
},
],
actionType: 'dialog',
id: 'u:438767684afb',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:c664b67b3e6b',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:f8fa25c49d82',
},
],
closeOnEsc: true,
showCloseButton: true,
},
},
],
},
},
},
{
type: 'export-excel',
align: 'right',
label: '导出',
icon: 'fa fa-download',
level: 'primary',
api: 'rest/locations?select=id,code,name,type,dicts(dictvalue)&order=code',
filename: '实验场地',
exportColumns: [
{ label: '编号', name: 'code' },
{ name: 'name', label: '名称' },
{ name: 'dicts.dictvalue', label: '类型' },
],
id: 'u:ab01cbc28399',
perPageAvailable: [10],
},
],
syncLocation: false,
perPageAvailable: [10, 20, 30, 40, 50],
footerToolbar: [
{ type: 'pagination' },
{ type: 'switch-per-page' },
{ type: 'statistics' },
],
perPageField: 'perPage',
pageField: 'page',
title: '',
initApi: '',
bodyClassName: '',
id: 'u:96cb5c2c87a4',
initFetch: '',
},
],
messages: {},
title: '',
id: 'u:4ffd31a5304f',
definitions: {
'dialog-ref-1': {
type: 'dialog',
title: '添加实验场地',
body: [
{
type: 'form',
title: '表单',
body: [
{
type: 'input-text',
name: 'code',
label: '编号',
mode: 'normal',
required: true,
id: 'u:9736564ec7fc',
},
{
type: 'input-text',
name: 'name',
label: '名称',
mode: 'normal',
required: true,
id: 'u:dcb5e881be78',
},
{
type: 'select',
name: 'type',
label: '类型',
mode: 'normal',
required: true,
checkAll: false,
source: {
url: 'rest/dicts?typecode=eq.006',
method: 'get',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map((item) => {\r\n return {\r\n label: item.dictvalue,\r\n value: item.dictkey\r\n };\r\n })\r\n}',
},
value: 'laboratory',
options: [{ label: '实验室', value: 'laboratory' }],
id: 'u:af3f658e33cf',
},
],
api: {
method: 'post',
url: 'rest/locations',
data: {
code: '$code',
name: '$name',
type: '$type',
organization_id: '${centre_id}',
},
dataType: 'json',
},
id: 'u:510d0236a371',
actions: [{ type: 'submit', label: '提交', primary: true }],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:96cb5c2c87a4',
ignoreError: false,
actionType: 'reload',
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
id: 'u:9cae782cb189',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:5217a0785f6c',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:1e10c45aa1a2',
},
],
$$ref: 'dialog-ref-1',
},
'dialog-ref-2': {
type: 'dialog',
title: '修改实验场地',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'patch',
url: 'rest/locations?id=eq.$id',
data: { code: '$code', name: '$name', type: '$type' },
dataType: 'json',
},
body: [
{
label: '编号',
type: 'input-text',
name: 'code',
required: true,
mode: 'normal',
id: 'u:339d0f396a34',
},
{
type: 'input-text',
label: '名称',
name: 'name',
required: true,
mode: 'normal',
id: 'u:3f1f8e6f2f13',
},
{
name: 'type',
type: 'select',
label: '类型',
source: {
url: 'rest/dicts?typecode=eq.006',
method: 'get',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map((item) => {\r\n return {\r\n label: item.dictvalue,\r\n value: item.dictkey,\r\n };\r\n }),\r\n};',
},
required: true,
checkAll: false,
mode: 'normal',
id: 'u:fda206c44f88',
},
],
id: 'u:3ab6c9e29cf6',
actions: [{ type: 'submit', label: '提交', primary: true }],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:96cb5c2c87a4',
ignoreError: false,
actionType: 'reload',
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
id: 'u:786fa30ccac8',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:d81e2b6865c1',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:e76ad8a24b48',
},
],
$$ref: 'dialog-ref-2',
},
},
};
export { schema };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,485 @@
const schema = {
type: 'page',
body: [
{
type: 'crud',
columns: [
{
type: 'text',
name: 'code',
label: '账号',
sortable: true,
searchable: true,
id: 'u:815ab28420f2',
},
{
type: 'text',
name: 'name',
label: '姓名',
sortable: true,
searchable: true,
id: 'u:14ae63b2e9b2',
},
{
type: 'button-group',
label: '操作',
id: 'u:70fc12c2dc93',
buttons: [
{
type: 'button',
level: 'link',
size: 'md',
icon: 'fa fa-pencil text-info',
tooltip: '修改',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
label: '',
api: { method: 'get', url: 'rest/users/$id' },
id: 'u:0e1e6868a15e',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
title: '修改账号',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'patch',
url: 'rest/users/$id',
data: { code: '$code', name: '$name' },
dataType: 'json',
},
mode: 'normal',
id: 'u:752cc90e2438',
body: [
{
label: '账号',
type: 'input-text',
name: 'code',
required: true,
id: 'u:dab5b6824746',
},
{
type: 'input-text',
label: '姓名',
name: 'name',
required: true,
id: 'u:95086c61998e',
},
],
actions: [
{ type: 'submit', label: '提交', primary: true },
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:de2533791a72',
ignoreError: false,
actionType: 'reload',
},
],
},
},
},
],
type: 'dialog',
closeOnEsc: true,
showCloseButton: true,
id: 'u:41bebd8f0aba',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:7aabbd45b9b9',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:d1ba4fdace4d',
},
],
actionType: 'dialog',
},
},
],
},
},
},
{
type: 'button',
confirmText: '确认删除"${name}(${code})"',
level: 'link',
icon: 'fa fa-times text-danger',
size: 'md',
tooltip: '删除',
tooltipPlacement: 'bottom',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
label: '',
id: 'u:b9864828c5ea',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
outputVar: 'responseResult',
actionType: 'ajax',
options: {},
api: {
url: 'rest/users/$id',
method: 'delete',
requestAdaptor: '',
adaptor: '',
messages: {},
},
},
{
componentId: 'u:de2533791a72',
ignoreError: false,
actionType: 'reload',
args: { resetPage: false },
},
],
},
},
},
{
type: 'button',
label: '',
actionType: 'ajax',
size: 'md',
level: 'link',
icon: 'fa fa-cog text-warning',
api: {
method: 'post',
url: 'rest/rpc/reset_password',
data: { user_id: '$id' },
},
confirmText: '确认重置"${name}"的密码?',
visibleOn: '',
tooltip: '重置密码',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
id: 'u:b7b4f65ce939',
},
],
placeholder: '-',
width: 200,
},
],
messages: {},
api: {
method: 'get',
url: 'rest/users?select=*,user_orgs(orgs(id))&code=${code}&name=${name}&role=eq.teacher&user_orgs.orgs.id=eq.$centre_id&order=code',
data: {
page: '${page}',
perPage: '${perPage}',
orderBy: '${orderBy}',
orderDir: '${orderDir}',
code: 'like.%${code}%',
name: 'like.%${name}%',
},
requestAdaptor:
"if (api.query.code === 'like.%%') {\r\n api.url = api.url.replace('&code=like.%25%25', '')\r\n}\r\n\r\nif (api.query.name === 'like.%%') {\r\n api.url = api.url.replace('&name=like.%25%25', '')\r\n}\r\n\r\nif (api.query.orderDir) {\r\n api.url = api.url.replace('&order=code%2Cname', '');\r\n api.url = api.url.replace('&orderBy=' + api.query.orderBy + '&orderDir=' + api.query.orderDir, '&order=' + api.query.orderBy + '.' + api.query.orderDir);\r\n} else if (api.query.orderBy) {\r\n api.url = api.url.replace('&order=code%2Cname', '');\r\n api.url = api.url.replace('&orderBy=' + api.query.orderBy + '&orderDir=', '&order=' + api.query.orderBy);\r\n} else {\r\n api.url = api.url.replace('&orderBy=&orderDir=', '');\r\n}\r\n\r\nreturn api;",
},
headerToolbar: [
{
type: 'button',
icon: 'fa fa-plus',
level: 'default',
align: 'right',
label: '添加',
className: 'm-l-xs',
id: 'u:f4a47bb1a6d3',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '添加任课教师',
body: [
{
type: 'form',
title: '表单',
body: [
{
type: 'input-text',
name: 'code',
label: '账号',
mode: 'normal',
required: true,
id: 'u:bda0263ab8b8',
},
{
type: 'input-text',
name: 'name',
label: '姓名',
mode: 'normal',
required: true,
id: 'u:36976961498d',
},
{
type: 'input-password',
name: 'password',
label: '密码',
mode: 'normal',
required: true,
id: 'u:ec15585cc409',
},
{
type: 'input-password',
name: 'new_password',
label: '确认密码',
mode: 'normal',
required: true,
id: 'u:e364b38586a8',
},
],
api: {
method: 'post',
url: 'rest/rpc/create_user',
data: {
code: '$code',
name: '$name',
password: '$password',
new_password: '$new_password',
role: 'teacher',
orgs_id: '$centre_id',
},
requestAdaptor: '',
dataType: 'json',
headers: { Prefer: 'params=single-object' },
},
rules: [
{
rule: 'this.password === this.new_password',
message: '两次密码输入不一致,请重新输入',
},
],
id: 'u:9b27303ee8c7',
actions: [
{ type: 'submit', label: '提交', primary: true },
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:de2533791a72',
ignoreError: false,
actionType: 'reload',
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
id: 'u:feafc93d557c',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:836089a67684',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:51adce35589e',
},
],
actionType: 'dialog',
},
},
],
},
},
},
{
type: 'button',
align: 'right',
label: '导入',
icon: 'fa fa-upload',
level: 'warning',
id: 'u:25df91a187d4',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '教师导入',
body: [
{
type: 'form',
title: '表单',
body: [
{
type: 'input-excel',
name: 'teachers',
label: '',
mode: 'normal',
id: 'u:5f0eca8a64fc',
},
{
type: 'input-table',
name: 'teachers',
label: '',
columns: [
{
label: '账号',
name: '账号',
placeholder: '-',
quickEdit: {
mode: 'inline',
id: 'u:e92d65ada484',
},
id: 'u:67eb5050eb4d',
},
{
label: '姓名',
name: '姓名',
placeholder: '-',
quickEdit: {
mode: 'inline',
id: 'u:2d4bdf8c7035',
},
id: 'u:74137b900acf',
},
],
mode: 'normal',
visibleOn: 'this.teachers',
strictMode: true,
removable: true,
editable: false,
id: 'u:d73c846629ed',
clearValueOnHidden: true,
},
],
api: {
method: 'post',
url: 'rest/rpc/create_user',
data: {
orgs_id: '${centre_id}',
teachers: '${teachers}',
},
requestAdaptor:
"return {\r\n ...api,\r\n data: api.data.teachers.map(item => {\r\n return {\r\n name: item.姓名,\r\n code: item.账号,\r\n role: 'teacher',\r\n orgs_id: api.data.orgs_id\r\n }\r\n })\r\n}",
dataType: 'json',
headers: { Prefer: 'params=single-object' },
},
id: 'u:e60b2ab481ec',
actions: [
{ type: 'submit', label: '提交', primary: true },
],
feat: 'Insert',
dsType: 'api',
labelAlign: 'left',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:de2533791a72',
ignoreError: false,
actionType: 'reload',
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'lg',
actionType: 'dialog',
id: 'u:249cb4918177',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:2d7cd1af378c',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:2dca052d9127',
},
],
},
},
],
},
},
},
{
type: 'export-excel',
align: 'right',
label: '导出',
icon: 'fa fa-download',
level: 'primary',
api: 'rest/users?select=*,user_orgs(orgs(id))&role=eq.teacher&user_orgs.orgs.id=eq.$centre_id&order=code',
filename: '任课教师',
exportColumns: [
{ label: '账号', name: 'code' },
{ name: 'name', label: '姓名' },
],
id: 'u:e2ad66d916a5',
},
],
syncLocation: false,
perPageAvailable: [10, 20, 30, 40, 50],
footerToolbar: [
{ type: 'pagination' },
{ type: 'switch-per-page' },
{ type: 'statistics' },
],
perPageField: 'perPage',
pageField: 'page',
title: '',
initApi: '',
bodyClassName: '',
id: 'u:de2533791a72',
affixHeader: true,
placeholder: '-',
},
],
messages: {},
title: '',
id: 'u:1d83fac554fc',
definitions: {},
};
export { schema };

View File

@@ -0,0 +1,485 @@
const schema = {
type: 'page',
body: [
{
type: 'crud',
messages: {},
api: {
method: 'get',
url: 'rest/teams?select=*,team_members(users(id,code,name))&organization_id=eq.$centre_id&order=code',
data: {
page: '$page',
perPage: '$perPage',
},
adaptor:
'return {\r\n ...payload.data,\r\n items: payload.data.items.map(item=> {\r\n return {\r\n ...item,\r\n selected_members: item.team_members.map(x => x.users.id)\r\n }\r\n })\r\n}\r\n',
},
headerToolbar: [
{
type: 'button',
icon: 'fa fa-plus',
level: 'primary',
align: 'right',
label: '添加',
className: 'm-l-xs',
id: 'u:3c4c79023e6d',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '添加教学团队',
body: [
{
type: 'form',
title: '表单',
body: [
{
type: 'input-text',
name: 'code',
label: '编号',
mode: 'normal',
required: true,
id: 'u:9c6265d6605a',
},
{
type: 'input-text',
name: 'name',
label: '名称',
mode: 'normal',
required: true,
id: 'u:fb25ddbf9ea3',
},
{
type: 'input-rich-text',
name: 'intro',
label: '描述',
mode: 'normal',
required: false,
options: {
menubar: false,
},
id: 'u:c88442289c84',
},
],
api: {
method: 'post',
url: 'rest/teams',
data: {
organization_id: '$centre_id',
code: '$code',
name: '$name',
intro: '$intro',
},
dataType: 'json',
},
id: 'u:5e5757012047',
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:6b104aa599d9',
ignoreError: false,
actionType: 'reload',
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
id: 'u:f020a946e699',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:d93f54d6db83',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:cc9454abf750',
},
],
},
},
],
},
},
},
],
syncLocation: false,
perPageAvailable: [9, 27, 54],
footerToolbar: [
{
type: 'pagination',
},
{
type: 'switch-per-page',
tpl: '内容',
},
],
perPageField: 'perPage',
pageField: 'page',
bulkActions: [],
itemActions: [],
mode: 'cards',
card: {
type: 'card',
header: {
title: '$name',
subTitle: '$code',
},
body: [
{
type: 'tpl',
tpl: '${intro|raw}',
id: 'u:e89fc74f9f0a',
},
{
type: 'each',
name: 'team_members',
placeholder: '未添加团队成员',
items: [
{
type: 'plain',
tpl: '${users.name}',
inline: true,
className: 'm-l-sm',
id: 'u:53d23e452416',
},
{
type: 'button',
label: '',
level: 'link',
icon: 'fa fa-times text-danger',
iconClassName: 'text-danger',
confirmText: '确认删除"${users.name}"',
tooltip: '删除成员',
tooltipPlacement: 'top',
id: 'u:13cfdb0af771',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
outputVar: 'responseResult',
actionType: 'ajax',
options: {},
api: {
url: 'rest/team_members?team_id=eq.${id}&teacher_id=eq.${users.id}',
method: 'delete',
requestAdaptor: '',
adaptor: '',
messages: {},
},
},
{
componentId: 'u:6b104aa599d9',
ignoreError: false,
actionType: 'reload',
},
],
},
},
},
],
id: 'u:c6b9bef8b674',
},
],
actions: [
{
label: '',
type: 'button',
icon: 'fa fa-edit text-info',
tooltip: '修改教学团队',
tooltipPlacement: 'top',
size: 'md',
id: 'u:15a30654e625',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
body: [
{
type: 'form',
title: '表单',
api: {
method: 'patch',
url: 'rest/teams/${id}',
data: {
code: '$code',
name: '$name',
intro: '$intro',
teacher_list: '{${teacher_list}}',
},
dataType: 'json',
},
body: [
{
type: 'input-text',
label: '编号',
name: 'code',
required: true,
mode: 'normal',
id: 'u:21b2f1785596',
},
{
label: '名称',
type: 'input-text',
name: 'name',
required: true,
mode: 'normal',
id: 'u:765ddf946178',
},
{
label: '描述',
type: 'input-rich-text',
name: 'intro',
options: {
menubar: false,
},
mode: 'normal',
id: 'u:454ab08215fc',
},
],
id: 'u:51ad45e350ee',
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:6b104aa599d9',
ignoreError: false,
actionType: 'reload',
data: {},
dataMergeMode: 'override',
args: {
resetPage: false,
},
},
],
},
},
},
],
type: 'dialog',
closeOnEsc: true,
showCloseButton: true,
title: '修改教学团队',
id: 'u:071e18a84182',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:07f56791977b',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:5e08c0b4a2c2',
},
],
},
},
],
},
},
},
{
type: 'button',
label: '',
icon: 'fa fa-trash text-danger',
confirmText: '确认删除"${name}(${code})"',
iconClassName: 'text-danger',
tooltip: '删除教学团队',
tooltipPlacement: 'top',
size: 'md',
id: 'u:481d5762073e',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
outputVar: 'responseResult',
actionType: 'ajax',
options: {},
api: {
url: 'rest/teams?id=eq.${id}',
method: 'delete',
requestAdaptor: '',
adaptor: '',
messages: {},
},
},
{
componentId: 'u:6b104aa599d9',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
{
type: 'button',
label: '',
icon: 'fa fa-group text-success',
size: 'md',
tooltip: '添加成员',
tooltipPlacement: 'top',
id: 'u:8ab4c946e195',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
title: '添加成员',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'rest/team_members',
data: {
select: '$select',
team_id: '${id}',
},
dataType: 'json',
requestAdaptor:
"return {\r\n ...api,\r\n data: api.data.select.split(',').map(item => {\r\n return {\r\n team_id: api.data.team_id,\r\n teacher_id: item\r\n }\r\n })\r\n}",
},
body: [
{
type: 'select',
label: '',
name: 'select',
required: true,
mode: 'normal',
checkAll: false,
defaultCheckAll: false,
checkAllLabel: '全选',
source: {
method: 'get',
url: 'rest/users?select=*,user_orgs(organization_id)&role=eq.teacher&user_orgs.organization_id=eq.${centre_id}&id=not.in.(${selected_members})&order=code',
adaptor:
'return {\r\n data: payload.data.items.map(x=> {\r\n return {\r\n label: x.name,\r\n value: x.id\r\n }\r\n })\r\n}',
},
multiple: true,
joinValues: true,
id: 'u:d8d80248a021',
},
],
id: 'u:518d0cab0bb2',
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:6b104aa599d9',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
type: 'dialog',
closeOnEsc: true,
showCloseButton: true,
id: 'u:ca9566592590',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:537639203d7b',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:bdee93cc8455',
},
],
},
},
],
},
},
},
],
id: 'u:c2cda94e1dbb',
},
placeholder: '暂无数据',
columnsCount: 3,
defaultParams: {
perPage: 9,
},
affixHeader: true,
id: 'u:6b104aa599d9',
},
],
messages: {},
title: '',
id: 'u:418dc967756c',
};
export { schema };

View File

@@ -0,0 +1,592 @@
const schema = {
body: [
{
type: 'form',
title: '表单',
wrapWithPanel: false,
submitOnChange: true,
reload: '',
messages: {},
target:
'teacher2?semesterSelect=${semesterSelect}&course_id=${course_id}&parent_id=${parent_id},teacher1?semesterSelect=${semesterSelect}&course_id=${course_id}',
body: [
{
type: 'group',
body: [
{
type: 'select',
label: '学期',
name: 'semesterSelect',
mode: 'inline',
size: 'lg',
clearable: false,
source: {
method: 'get',
url: 'rest/semesters',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: item.id\r\n }\r\n })\r\n}',
},
checkAll: false,
searchable: true,
selectFirst: false,
inputClassName: 'm-l-xs',
},
{
type: 'select',
label: '课程',
name: 'course_id',
mode: 'inline',
size: 'md',
clearable: false,
source: {
method: 'get',
url: 'rest/courses?organization_id=eq.${centre_id}&semester_id=eq.${semesterSelect}&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.length > 0 && payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: item.id\r\n }\r\n })\r\n}',
sendOn: 'this.semesterSelect',
},
checkAll: false,
searchable: true,
selectFirst: false,
visibleOn: '',
clearValueOnHidden: false,
inputClassName: 'm-l-xs',
className: 'm-l-sm',
},
],
},
{
type: 'group',
body: [
{
type: 'select',
label: '一级指标',
name: 'parent_id',
mode: 'inline',
size: 'md',
source: {
method: 'get',
url: 'rest/teaching_evaluation_items?organization_id=eq.${centre_id}&semester_id=eq.${semesterSelect}&parent=is.null&order=order',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: item.id\r\n }\r\n })\r\n }\r\n}',
sendOn: 'this.course_id',
},
checkAll: false,
searchable: true,
selectFirst: false,
visibleOn: 'this.course_id',
clearValueOnHidden: true,
inputClassName: 'm-l-xs',
},
],
},
],
},
{
type: 'grid',
columns: [
{
body: [
{
type: 'flex',
items: [
{
type: 'wrapper',
body: [
{
type: 'chart',
config: {
title: [{ text: '教师统计(一级指标)' }],
series: [
{
data: '${series_data}',
type: 'radar',
emphasis: { lineStyle: { width: 4 } },
tooltip: { trigger: 'item' },
},
],
tooltip: { trigger: 'axis' },
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20,
data: '${legend}',
},
radar: [
{
indicator: '${items}',
radius: 200,
startAngle: 90,
splitNumber: 5,
name: {
textStyle: {
color: '#fff',
backgroundColor: '#666',
borderRadius: 3,
padding: [3, 5],
},
},
center: ['35%', '55%'],
},
],
toolbox: {
show: true,
feature: {
dataView: {
show: true,
title: '数据视图',
lang: ['数据视图', '关闭', '刷新'],
},
saveAsImage: {
show: true,
title: '保存为图片',
type: 'png',
},
},
left: 170,
top: -2,
},
},
_mode: '2',
api: {
method: 'post',
url: 'rest/rpc/evaluation_stats_teacher',
adaptor:
'let legend_set = new Set()\r\n\r\npayload.data.rows && payload.data.rows.forEach(item => {\r\n if(item.teacher_id) {\r\n legend_set.add(item.teacher_name.concat("(", item.teacher_id, ")"));\r\n } else {\r\n legend_set.add(\'未指定\');\r\n }\r\n})\r\n\r\nlet legend = Array.from(legend_set)\r\n\r\nlet series_data = legend\r\n ? legend.map(legend => {\r\n return {\r\n value: payload.data.rows.filter(\r\n item => item.teacher_id ? item.teacher_name.concat("(", item.teacher_id, ")") === legend : legend === \'未指定\'\r\n ).map(item => item.score),\r\n name: legend,\r\n symbol: "rect",\r\n symbolSize: 12,\r\n lineStyle: {\r\n type: "dashed"\r\n }\r\n }\r\n })\r\n : []\r\n\r\nreturn {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items\r\n ? payload.data.items.sort(\r\n (a, b) => a.id - b.id\r\n ).map(item => {\r\n return {\r\n text: item.name,\r\n max: 5\r\n }\r\n })\r\n : [],\r\n legend: legend,\r\n series_data: series_data\r\n }\r\n}\r\n',
sendOn: 'this.course_id',
data: {
course_id: '${course_id}',
organization_id: '${centre_id}',
},
requestAdaptor: '',
},
dataFilter: '',
height: 480,
name: 'teacher1',
},
],
size: 'xs',
className: 'bg-white',
},
],
direction: 'column',
justify: 'start',
alignItems: 'stretch',
className: '',
__mode: true,
style: {},
},
],
},
{
body: [
{
type: 'flex',
items: [
{
type: 'wrapper',
body: [
{
type: 'chart',
config: {
title: [{ text: '教师统计(二级指标)' }],
series: [
{
data: '${series_data}',
type: 'radar',
emphasis: { lineStyle: { width: 4 } },
tooltip: { trigger: 'item' },
},
],
tooltip: { trigger: 'axis' },
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20,
data: '${legend}',
},
radar: [
{
indicator: '${items}',
radius: 200,
startAngle: 90,
splitNumber: 5,
name: {
textStyle: {
color: '#fff',
backgroundColor: '#666',
borderRadius: 3,
padding: [3, 5],
},
},
center: ['35%', '55%'],
},
],
toolbox: {
show: true,
feature: {
dataView: {
show: true,
title: '数据视图',
lang: ['数据视图', '关闭', '刷新'],
},
saveAsImage: {
show: true,
title: '保存为图片',
type: 'png',
},
},
left: 170,
top: -2,
},
},
_mode: '2',
api: {
method: 'post',
url: 'rest/rpc/evaluation_stats_teacher',
adaptor:
'let legend_set = new Set()\r\n\r\npayload.data.rows && payload.data.rows.forEach(item => {\r\n if(item.teacher_id) {\r\n legend_set.add(item.teacher_name.concat("(", item.teacher_id, ")"));\r\n } else {\r\n legend_set.add(\'未指定\');\r\n }\r\n})\r\n\r\nlet legend = Array.from(legend_set)\r\n\r\nlet series_data = legend\r\n ? legend.map(legend => {\r\n return {\r\n value: payload.data.rows.filter(\r\n item => item.teacher_id ? item.teacher_name.concat("(", item.teacher_id, ")") === legend : legend === \'未指定\'\r\n ).map(item => item.score),\r\n name: legend,\r\n symbol: "rect",\r\n symbolSize: 12,\r\n lineStyle: {\r\n type: "dashed"\r\n }\r\n }\r\n })\r\n : []\r\n\r\nreturn {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items\r\n ? payload.data.items.sort(\r\n (a, b) => a.id - b.id\r\n ).map(item => {\r\n return {\r\n text: item.name,\r\n max: 5\r\n }\r\n })\r\n : [],\r\n legend: legend,\r\n series_data: series_data\r\n }\r\n}\r\n',
sendOn: 'this.parent_id',
data: {
course_id: '${course_id}',
organization_id: '${centre_id}',
parent: '${parent_id}',
},
requestAdaptor: '',
},
dataFilter: '',
height: 480,
name: 'teacher2',
},
],
size: 'xs',
className: 'bg-white',
},
],
direction: 'column',
justify: 'start',
alignItems: 'stretch',
className: '',
style: {},
},
],
},
],
},
{ type: 'divider', lineStyle: 'solid' },
{
type: 'form',
title: '表单',
wrapWithPanel: false,
submitOnChange: true,
reload: '',
messages: {},
target:
'location2?semesterSelect=${semesterSelect}&course_id=${course_id}&group_id=${group_id}&parent_id=${parent_id},location1?semesterSelect=${semesterSelect}&course_id=${course_id}&group_id=${group_id}',
body: [
{
type: 'group',
body: [
{
type: 'select',
label: '学期',
name: 'semesterSelect',
mode: 'inline',
size: 'lg',
clearable: false,
source: {
method: 'get',
url: 'rest/semesters?order=is_open.desc,since.desc',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: item.id\r\n }\r\n })\r\n}',
},
checkAll: false,
searchable: true,
selectFirst: false,
inputClassName: 'm-l-xs',
},
{
type: 'select',
label: '课程',
name: 'course_id',
mode: 'inline',
size: 'md',
clearable: false,
source: {
method: 'get',
url: 'rest/courses?organization_id=eq.${centre_id}&semester_id=eq.${semesterSelect}&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.length > 0 && payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: item.id\r\n }\r\n })\r\n}',
sendOn: 'this.semesterSelect',
},
checkAll: false,
searchable: true,
selectFirst: false,
visibleOn: '',
clearValueOnHidden: false,
inputClassName: 'm-l-xs',
className: 'm-l-sm',
},
{
type: 'select',
label: '场地组合',
name: 'group_id',
mode: 'inline',
size: 'lg',
clearable: true,
source: {
method: 'get',
url: 'rest/locations?organization_id=eq.${centre_id}&type=eq.laboratory&is_valid=is.true&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.length > 0 && payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: item.id\r\n }\r\n })\r\n}',
},
checkAll: true,
searchable: true,
selectFirst: false,
visibleOn: '',
clearValueOnHidden: false,
inputClassName: 'm-l-xs',
className: 'm-l-sm',
multiple: true,
joinValues: true,
valuesNoWrap: false,
defaultCheckAll: false,
checkAllLabel: '全选',
},
],
},
{
type: 'group',
body: [
{
type: 'select',
label: '一级指标',
name: 'parent_id',
mode: 'inline',
size: 'md',
source: {
method: 'get',
url: 'rest/teaching_evaluation_items?organization_id=eq.${centre_id}&semester_id=eq.${semesterSelect}&parent=is.null&order=order',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: item.id\r\n }\r\n })\r\n }\r\n}',
sendOn: 'this.course_id',
},
checkAll: false,
searchable: true,
selectFirst: false,
visibleOn: 'this.course_id',
clearValueOnHidden: true,
inputClassName: 'm-l-xs',
},
],
},
],
},
{
type: 'grid',
columns: [
{
body: [
{
type: 'flex',
items: [
{
type: 'wrapper',
body: [
{
type: 'chart',
config: {
title: [{ text: '场地统计(一级指标)' }],
series: [
{
data: '${series_data}',
type: 'radar',
emphasis: { lineStyle: { width: 4 } },
tooltip: { trigger: 'item' },
},
],
tooltip: { trigger: 'axis' },
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20,
data: '${legend}',
},
radar: [
{
indicator: '${items}',
radius: 200,
startAngle: 90,
splitNumber: 5,
name: {
textStyle: {
color: '#fff',
backgroundColor: '#666',
borderRadius: 3,
padding: [3, 5],
},
},
center: ['35%', '55%'],
},
],
toolbox: {
show: true,
feature: {
dataView: {
show: true,
title: '数据视图',
lang: ['数据视图', '关闭', '刷新'],
},
saveAsImage: {
show: true,
title: '保存为图片',
type: 'png',
},
},
left: 170,
top: -2,
},
},
_mode: '2',
api: {
method: 'post',
url: 'rest/rpc/evaluation_stats_location',
adaptor:
'let legend_set = new Set()\r\n\r\npayload.data.rows && payload.data.rows.forEach(item => {\r\n legend_set.add(item.location_name)\r\n})\r\n\r\nlet legend = Array.from(legend_set)\r\n\r\nlet series_data = legend\r\n ? legend.map(legend => {\r\n return {\r\n value: payload.data.rows.filter(\r\n item => item.location_name === legend\r\n ).map(item => item.score),\r\n name: legend,\r\n symbol: "rect",\r\n symbolSize: 12,\r\n lineStyle: {\r\n type: "dashed"\r\n }\r\n }\r\n })\r\n : []\r\n\r\nreturn {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items\r\n ? payload.data.items.sort(\r\n (a, b) => a.id - b.id\r\n ).map(item => {\r\n return {\r\n text: item.name,\r\n max: 5\r\n }\r\n })\r\n : [],\r\n legend: legend,\r\n series_data: series_data\r\n }\r\n}\r\n',
sendOn: 'this.course_id',
data: {
course_id: '${course_id}',
organization_id: '${centre_id}',
group: '${group_id}',
},
requestAdaptor:
"if (api.body.group === '') {\r\n delete api.body.group\r\n}\r\n\r\nreturn {\r\n ...api\r\n}",
},
dataFilter: '',
height: 480,
name: 'location1',
},
],
size: 'xs',
className: 'bg-white',
},
],
direction: 'column',
justify: 'start',
alignItems: 'stretch',
className: '',
__mode: true,
style: {},
},
],
},
{
body: [
{
type: 'flex',
items: [
{
type: 'wrapper',
body: [
{
type: 'chart',
config: {
title: [{ text: '场地统计(二级指标)' }],
series: [
{
data: '${series_data}',
type: 'radar',
emphasis: { lineStyle: { width: 4 } },
tooltip: { trigger: 'item' },
},
],
tooltip: { trigger: 'axis' },
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20,
data: '${legend}',
},
radar: [
{
indicator: '${items}',
radius: 200,
startAngle: 90,
splitNumber: 5,
name: {
textStyle: {
color: '#fff',
backgroundColor: '#666',
borderRadius: 3,
padding: [3, 5],
},
},
center: ['35%', '55%'],
},
],
toolbox: {
show: true,
feature: {
dataView: {
show: true,
title: '数据视图',
lang: ['数据视图', '关闭', '刷新'],
},
saveAsImage: {
show: true,
title: '保存为图片',
type: 'png',
},
},
left: 170,
top: -2,
},
},
_mode: '2',
api: {
method: 'post',
url: 'rest/rpc/evaluation_stats_location',
adaptor:
'let legend_set = new Set()\r\n\r\npayload.data.rows && payload.data.rows.forEach(item => {\r\n legend_set.add(item.location_name)\r\n})\r\n\r\nlet legend = Array.from(legend_set)\r\n\r\nlet series_data = legend\r\n ? legend.map(legend => {\r\n return {\r\n value: payload.data.rows.filter(\r\n item => item.location_name === legend\r\n ).map(item => item.score),\r\n name: legend,\r\n symbol: "rect",\r\n symbolSize: 12,\r\n lineStyle: {\r\n type: "dashed"\r\n }\r\n }\r\n })\r\n : []\r\n\r\nreturn {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items\r\n ? payload.data.items.sort(\r\n (a, b) => a.id - b.id\r\n ).map(item => {\r\n return {\r\n text: item.name,\r\n max: 5\r\n }\r\n })\r\n : [],\r\n legend: legend,\r\n series_data: series_data\r\n }\r\n}\r\n',
sendOn: 'this.parent_id',
data: {
course_id: '${course_id}',
organization_id: '${centre_id}',
parent: '${parent_id}',
group: '${group_id}',
},
requestAdaptor:
"if (api.body.group === '') {\r\n delete api.body.group\r\n}\r\n\r\nreturn {\r\n ...api\r\n}",
},
dataFilter: '',
height: 480,
name: 'location2',
},
],
size: 'xs',
className: 'bg-white',
},
],
direction: 'column',
justify: 'start',
alignItems: 'stretch',
className: '',
style: {},
},
],
},
],
},
],
type: 'page',
};
export { schema };

View File

@@ -0,0 +1,209 @@
const schema = {
type: 'page',
body: [
{
type: 'form',
title: '表单',
submitOnChange: true,
reload: 'statistic?course_id=$course_id&org_id=$org_id',
wrapWithPanel: false,
messages: {
fetchFailed: '初始化失败',
saveSuccess: '统计成功',
saveFailed: '保存失败',
},
body: [
{
label: '学期',
type: 'select',
name: 'semesterSelect',
mode: 'inline',
clearable: false,
source: {
method: 'get',
url: 'rest/semesters?order=is_open.desc,since.desc',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: item.id\r\n }\r\n })\r\n}',
},
checkAll: false,
size: 'lg',
searchable: true,
selectFirst: false,
id: 'u:b1122bff1df9',
},
{
type: 'select',
label: '课程',
name: 'course_id',
mode: 'inline',
clearable: false,
source: {
method: 'get',
url: 'rest/courses?organization_id=eq.$centre_id&semester_id=eq.$semesterSelect&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.length > 0 && payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: item.id\r\n }\r\n })\r\n}',
sendOn: 'this.semesterSelect',
},
checkAll: false,
size: 'lg',
searchable: true,
selectFirst: false,
visibleOn: '',
clearValueOnHidden: false,
id: 'u:2e2358141406',
},
{
type: 'select',
label: '班级',
name: 'org_id',
mode: 'inline',
clearable: true,
size: 'lg',
source: {
method: 'get',
url: 'rest/orgs?select=*,course2orgs!inner(*)&path=cd.root&course2orgs.course_id=eq.$course_id&type=in.(school,centre,faculty,class,reelectclass)&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => ({\r\n label: item.name,\r\n value: item.id\r\n }))\r\n }\r\n}',
sendOn: 'this.course_id',
requestAdaptor: '',
messages: {},
},
searchable: true,
visibleOn: 'this.course_id',
clearValueOnHidden: true,
multiple: true,
joinValues: true,
delimiter: ',',
cascade: false,
initiallyOpen: false,
unfoldedLevel: 2,
onlyChildren: true,
withChildren: false,
extractValue: true,
id: 'u:84b51bcafda6',
autoCheckChildren: true,
enableNodePath: false,
showIcon: true,
checkAll: false,
},
{
type: 'control',
label: '',
remark: null,
mode: 'inline',
visibleOn: 'this.is_cursem && this.course_id',
body: [
{
type: 'button',
level: 'primary',
actionType: 'ajax',
api: {
method: 'post',
url: 'rest/rpc/calculate_grade',
data: {
course_id: '$course_id',
org_id: '$org_id',
},
requestAdaptor:
'const { course_id, org_id } = api.data\r\n\r\nif (org_id) {\r\n return {\r\n ...api\r\n }\r\n} else {\r\n return {\r\n ...api,\r\n data: { course_id }\r\n }\r\n}',
},
reload: 'statistic',
messages: {},
label: '统计',
id: 'u:a147092cff8f',
},
],
id: 'u:4944fd7962d5',
},
{
type: 'service',
messages: {},
api: {
method: 'get',
url: 'rest/semesters?id=eq.$semesterSelect',
sendOn: 'this.semesterSelect',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n is_cursem: payload.data.items.length > 0 ? payload.data.items[0].is_open : false\r\n }\r\n}',
},
body: [
{
type: 'hidden',
name: 'is_cursem',
id: 'u:39ca4db8501a',
},
],
id: 'u:3e13ab87d15b',
dsType: 'api',
},
],
id: 'u:970b1b98cc6a',
feat: 'Insert',
},
{
type: 'crud',
api: {
method: 'post',
url: 'rest/rpc/grade_stat',
data: {
page: '$page',
course_id: '$course_id',
perpage: '$perPage',
org_id: '$org_id',
},
adaptor: '',
requestAdaptor:
'const { course_id, org_id, page, perpage } = api.data\r\n\r\nif (course_id && org_id) {\r\n return {\r\n ...api\r\n }\r\n} else if (course_id) {\r\n return {\r\n ...api,\r\n data: { course_id, page, perpage }\r\n }\r\n} else {\r\n return {\r\n ...api,\r\n data: {}\r\n }\r\n}',
dataType: 'form',
sendOn: '',
},
columns: [],
messages: {},
syncLocation: false,
pageField: 'page',
perPageField: 'perPage',
headerToolbar: [
{
type: 'export-excel',
label: '全量导出 Excel',
api: {
method: 'post',
url: 'rest/rpc/grade_stat',
data: {
course_id: '$course_id',
org_id: '$org_id',
},
dataType: 'form',
requestAdaptor:
'const { course_id, org_id } = api.data\r\n\r\nif (course_id && org_id) {\r\n return {\r\n ...api\r\n }\r\n} else if (course_id) {\r\n return {\r\n ...api,\r\n data: { course_id }\r\n }\r\n} else {\r\n return {\r\n ...api,\r\n data: {}\r\n }\r\n}',
},
filename: '$filename',
id: 'u:8d80c123da31',
},
],
footerToolbar: [
{
type: 'pagination',
tpl: '内容',
},
{
type: 'switch-per-page',
tpl: '内容',
},
],
name: 'statistic',
title: '',
bodyClassName: 'common-height',
body: [],
toolbar: [],
alwaysShowPagination: false,
perPageAvailable: [10, 20, 30, 40, 50],
affixHeader: false,
id: 'u:ec1754a6f5a9',
},
],
title: '',
messages: {},
id: 'u:e3bc67e03bea',
};
export { schema };

View File

@@ -0,0 +1,411 @@
const schema = {
type: 'page',
body: [
{
type: 'grid',
columns: [
{
md: 8,
body: [
{
type: 'chart',
config: {
title: [
{
text: '实验项目开课次数统计',
},
],
series: [
{
data: '$yAxis_les',
type: 'pie',
name: '项目',
radius: '55%',
center: ['40%', '50%'],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)',
},
legend: {
type: 'scroll',
orient: 'vertical',
top: 20,
bottom: 20,
data: '$xAxis_exp',
right: 10,
},
toolbox: {
show: true,
feature: {
dataView: {
show: true,
title: '数据视图',
lang: ['数据视图', '关闭', '刷新'],
},
saveAsImage: {
show: true,
title: '保存为图片',
type: 'png',
},
},
left: 190,
top: -2,
},
},
_mode: '2',
api: {
method: 'get',
url: 'rest/schedules?select=*,projects:projects!schedule_project_id_fkey(id,name)&semester_id=eq.${semesterSelect}&teacher_id=in.(${teacherSelect})',
requestAdaptor:
"api.url = api.url.replace('&teacher_id=in.%28%29', '')\r\n\r\nreturn api",
adaptor:
'let xAxis_exp = []\r\nlet yAxis_les = []\r\npayload.data.items.forEach(item => {\r\n const index_exp = xAxis_exp.findIndex(i => i === item.projects.name)\r\n if (index_exp === -1) {\r\n xAxis_exp.push(item.projects.name)\r\n const index_exp = xAxis_exp.findIndex(i => i === item.projects.name)\r\n yAxis_les[index_exp] = {\r\n name: item.projects.name,\r\n value: 1\r\n }\r\n } else {\r\n yAxis_les[index_exp].value += 1\r\n }\r\n})\r\nreturn {\r\n ...payload,\r\n data: {\r\n xAxis_exp: xAxis_exp,\r\n yAxis_les: yAxis_les\r\n }\r\n}',
error: {
message: '',
},
},
dataFilter: '',
height: 450,
name: 'chart1',
id: 'u:4f3d6cd9e92a',
},
],
id: 'u:3a343388441f',
},
{
body: [
{
type: 'chart',
config: {
title: [
{
text: '教师课堂次数统计',
},
],
series: [
{
data: '$yAxis_les',
type: 'pie',
name: '教师',
radius: '55%',
center: ['40%', '50%'],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
},
],
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)',
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20,
data: '$xAxis_exp',
},
toolbox: {
show: true,
feature: {
dataView: {
show: true,
title: '数据视图',
lang: ['数据视图', '关闭', '刷新'],
},
saveAsImage: {
show: true,
title: '保存为图片',
type: 'png',
},
},
left: 155,
top: -2,
},
},
_mode: '2',
dataFilter: '',
api: {
method: 'get',
url: 'rest/users?select=*,schedules:schedules!schedule_teacher_id_fkey(*,projects:projects!schedule_project_id_fkey(id,name))&role=eq.teacher&schedules.semester_id=eq.${semesterSelect}&id=in.(${teacherSelect})&order=code',
requestAdaptor:
"api.url = api.url.replace('&id=in.%28%29', '')\r\n\r\nreturn api",
adaptor:
'let xAxis_tch = []\r\nlet yAxis_les = []\r\npayload.data.items.forEach(payload_item => {\r\n let elected = 0\r\n xAxis_tch.push(payload_item.name)\r\n yAxis_les.push({\r\n name: payload_item.name,\r\n value: payload_item.schedules.length ? payload_item.schedules.length : 0\r\n })\r\n})\r\nreturn {\r\n ...payload,\r\n data: {\r\n xAxis_tch: xAxis_tch,\r\n yAxis_les: yAxis_les\r\n }\r\n}',
},
height: 450,
name: 'chart2',
id: 'u:ed31d767d8d5',
},
],
id: 'u:989c40ed8233',
},
],
id: 'u:63c06d3a12fc',
},
{
type: 'divider',
lineStyle: 'solid',
id: 'u:b0c8cc501851',
},
{
type: 'chart',
config: {
title: [
{
text: '实验项目相关人次统计',
},
],
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#283b56',
},
},
},
legend: {
data: ['可选人数', '已选人数', '签到人数', '未签到人数'],
},
xAxis: [
{
type: 'category',
data: '$xAxis_exp',
axisLabel: {
rotate: -10,
interval: 0,
},
},
],
yAxis: {
type: 'value',
name: '人数',
},
series: [
{
data: '$yAxis_unsel',
type: 'bar',
name: '可选人数',
barWidth: 20,
},
{
data: '$yAxis_sel',
type: 'bar',
name: '已选人数',
barWidth: 20,
},
{
data: '$yAxis_fin',
type: 'bar',
name: '签到人数',
barWidth: 10,
stack: '已选人数',
},
{
data: '$yAxis_unfin',
type: 'bar',
name: '未签到人数',
barWidth: 10,
stack: '已选人数',
},
],
toolbox: {
show: true,
feature: {
dataView: {
show: true,
title: '数据视图',
lang: ['数据视图', '关闭', '刷新'],
},
saveAsImage: {
show: true,
title: '保存为图片',
type: 'png',
},
},
left: 190,
top: -2,
},
},
_mode: '2',
api: {
method: 'get',
url: 'rest/workload_stats?select=*,projects:projects!schedule_project_id_fkey(id,name)&semester_id=eq.$semesterSelect&teacher_id=in.(${teacherSelect})&organization_id=eq.$centre_id',
requestAdaptor:
"api.url = api.url.replace('&teacher_id=in.%28%29', '')\r\n\r\nreturn api",
adaptor:
'let xAxis_exp = []\r\nlet yAxis_sel = []\r\nlet yAxis_unsel = []\r\nlet yAxis_fin = []\r\nlet yAxis_unfin = []\r\n\r\npayload.data.items.forEach(item => {\r\n const index_exp = xAxis_exp.findIndex(i => i === item.projects.name)\r\n\r\n if (index_exp === -1) {\r\n xAxis_exp.push(item.projects.name)\r\n const index_exp = xAxis_exp.findIndex(i => i === item.projects.name)\r\n yAxis_sel[index_exp] = item.current_student_number\r\n yAxis_unsel[index_exp] = item.max_student_number - item.current_student_number\r\n yAxis_fin[index_exp] = item.finished_count\r\n yAxis_unfin[index_exp] = item.unfinished_count\r\n } else {\r\n yAxis_sel[index_exp] += item.current_student_number\r\n yAxis_unsel[index_exp] += (item.max_student_number - item.current_student_number)\r\n yAxis_fin[index_exp] += item.finished_count\r\n yAxis_unfin[index_exp] += item.unfinished_count\r\n }\r\n})\r\n\r\nreturn {\r\n ...payload,\r\n data: {\r\n xAxis_exp: xAxis_exp,\r\n yAxis_sel: yAxis_sel,\r\n yAxis_unsel: yAxis_unsel,\r\n yAxis_fin: yAxis_fin,\r\n yAxis_unfin: yAxis_unfin\r\n }\r\n}',
},
replaceChartOption: false,
dataFilter: '',
height: 450,
name: 'chart3',
id: 'u:5357767552e5',
},
{
type: 'divider',
lineStyle: 'solid',
id: 'u:01c9e40a887d',
},
{
type: 'chart',
config: {
title: [
{
text: '教师课堂相关人次统计',
},
],
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#283b56',
},
},
},
legend: {
data: ['已选人数', '签到人数', '未签到人数'],
},
yAxis: {
type: 'value',
name: '人数',
},
series: [
{
data: '$yAxis_sel',
type: 'bar',
name: '已选人数',
barWidth: 20,
},
{
data: '$yAxis_fin',
type: 'bar',
name: '签到人数',
barWidth: 10,
stack: '已选人数',
},
{
data: '$yAxis_unfin',
type: 'bar',
name: '未签到人数',
barWidth: 10,
stack: '已选人数',
},
],
xAxis: [
{
type: 'category',
data: '$xAxis_tch',
},
],
toolbox: {
show: true,
feature: {
dataView: {
show: true,
title: '数据视图',
lang: ['数据视图', '关闭', '刷新'],
},
saveAsImage: {
show: true,
title: '保存为图片',
type: 'png',
},
},
left: 190,
top: -2,
},
},
_mode: '2',
api: {
method: 'get',
url: 'rest/users?select=*,workload_stats:workload_stats!schedule_teacher_id_fkey(*,projects:projects!schedule_project_id_fkey(id,name))&role=eq.teacher&workload_stats.semester_id=eq.$semesterSelect&id=in.(${teacherSelect})&workload_stats.organization_id=eq.$centre_id&order=code',
requestAdaptor:
"api.url = api.url.replace('&id=in.%28%29', '')\r\n\r\nreturn api",
adaptor:
'let xAxis_tch = []\r\nlet yAxis_sel = []\r\nlet yAxis_fin = []\r\nlet yAxis_unfin = []\r\n\r\npayload.data.items.forEach(payload_item => {\r\n let elected = 0\r\n let finished = 0\r\n let unfinished = 0\r\n\r\n payload_item.workload_stats.length > 0 && payload_item.workload_stats.forEach(workload_item => {\r\n elected += workload_item.current_student_number\r\n finished += workload_item.finished_count\r\n unfinished += workload_item.unfinished_count\r\n })\r\n\r\n xAxis_tch.push(payload_item.name)\r\n yAxis_sel.push(elected)\r\n yAxis_fin.push(finished)\r\n yAxis_unfin.push(unfinished)\r\n})\r\n\r\nreturn {\r\n ...payload,\r\n data: {\r\n xAxis_tch: xAxis_tch,\r\n yAxis_sel: yAxis_sel,\r\n yAxis_fin: yAxis_fin,\r\n yAxis_unfin: yAxis_unfin\r\n }\r\n}',
},
replaceChartOption: false,
dataFilter: '',
height: 450,
name: 'chart4',
id: 'u:9c6e8236a44f',
},
{
type: 'divider',
lineStyle: 'solid',
id: 'u:e448f96e6b83',
},
],
toolbar: [
{
type: 'form',
title: '表单',
wrapWithPanel: false,
submitOnChange: true,
target: 'chart1,chart2,chart3,chart4',
canAccessSuperData: true,
body: [
{
label: '学期',
type: 'select',
name: 'semesterSelect',
mode: 'inline',
clearable: false,
source: {
method: 'get',
url: 'rest/semesters?order=is_open.desc,since.desc',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: item.id\r\n }\r\n })\r\n}',
},
checkAll: false,
size: 'lg',
searchable: true,
selectFirst: false,
className: 'm',
id: 'u:bdeea7faf818',
},
{
label: '教师',
type: 'select',
name: 'teacherSelect',
mode: 'inline',
clearable: true,
source: {
method: 'get',
url: 'rest/users?role=eq.teacher&order=name,code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: item.id\r\n }\r\n })\r\n}',
},
checkAll: true,
size: 'lg',
searchable: true,
selectFirst: false,
multiple: true,
className: 'm',
id: 'u:bdeea7faf818',
},
],
id: 'u:5003bb8ac5e6',
},
],
id: 'u:b5376a388009',
};
export { schema };

View File

@@ -0,0 +1,148 @@
const schema = {
type: 'page',
body: [
{
type: 'dataset-renderer',
name: 'storage',
bucket: 'dataset',
addFolder: {
type: 'button',
actionType: 'dialog',
reload: 'storage',
dialog: {
type: 'dialog',
title: '新建文件夹',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'storage/v1/object/dataset/$path/$name/.empty',
requestAdaptor:
"api.url = api.url.replace('//', '/');return api",
},
body: [
{
type: 'input-text',
label: '文件夹名称',
name: 'name',
mode: 'normal',
required: true,
},
],
},
],
closeOnEsc: true,
showCloseButton: true,
},
},
moveFile: {
type: 'button',
actionType: 'dialog',
reload: 'storage',
dialog: {
type: 'dialog',
title: '重命名',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'storage/v1/object/move',
data: {
bucketId: 'dataset',
sourceKey: '$sourceKey',
dest: '$dest',
path: '$path',
},
requestAdaptor:
"console.log(api)\r\nreturn {\r\n ...api,\r\n data: {\r\n bucketId: api.data.bucketId,\r\n sourceKey: api.data.sourceKey,\r\n destinationKey: api.data.path === '' ? api.data.dest : api.data.path + '/' + api.data.dest\r\n }\r\n}",
},
body: [
{
name: 'dest',
type: 'input-text',
label: '名称',
},
],
},
],
closeOnEsc: true,
showCloseButton: true,
},
},
delFile: {
type: 'button',
actionType: 'ajax',
reload: 'storage',
label: false,
api: {
method: 'delete',
url: '/storage/v1/object/dataset',
data: {
prefixes: '$prefixes',
},
headers: {
post2rest: false,
},
},
confirmText: '确认删除吗?',
},
delFiles: {
type: 'button',
actionType: 'dialog',
reload: 'storage',
dialog: {
type: 'dialog',
title: '批量删除文件',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'delete',
url: 'storage/v1/object/dataset',
data: { prefixes: '$prefixes', path: '$path' },
headers: { post2rest: false },
requestAdaptor:
"return {\r\n ...api,\r\n data: {\r\n prefixes: api.data.prefixes.map(item => api.data.path === '' ? item : api.data.path + '/' + item)\r\n }\r\n}",
},
body: [
{
name: 'prefixes',
type: 'checkboxes',
label: false,
inline: false,
checkAll: true,
joinValues: false,
extractValue: true,
required: true,
source: {
method: 'post',
url: '/storage/v1/object/list/dataset',
data: {
prefix: '$path',
sortBy: {
column: 'name',
order: 'asc',
},
},
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.filter(item => item.id && item.name.charAt(0) !== ".").map(item => {\r\n return {\r\n label: item.name,\r\n value: item.name\r\n }\r\n })\r\n}',
},
},
],
},
],
closeOnEsc: true,
showCloseButton: true,
},
},
},
,
],
};
export { schema };

View File

@@ -0,0 +1,667 @@
const schema = {
type: 'page',
body: [
{
type: 'container',
body: [
{
type: 'form',
title: '表单',
className: '',
submitOnChange: true,
reload: 'notebooks?author=$author&type=$type&publish_at=$publish_at',
wrapWithPanel: false,
canAccessSuperData: false,
mode: 'inline',
name: 'filter',
body: [
{
type: 'group',
body: [
{
type: 'select',
mode: 'inline',
label: '作者',
name: 'author',
source: {
method: 'get',
url: 'rest/users?order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.name + "(" + item.code + ")",\r\n value: "eq." + item.code\r\n }\r\n })\r\n}',
},
checkAll: false,
size: 'md',
searchable: true,
clearable: true,
className: 'm-l-sm',
inputClassName: 'm-l-xs',
id: 'u:f2ef198e2dcf',
},
{
type: 'select',
label: '类型',
name: 'type',
source: {
method: 'get',
url: 'rest/dicts?typecode=eq.028&order=id',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.dictvalue,\r\n value: "eq." + item.dictkey\r\n }\r\n })\r\n}',
},
mode: 'inline',
inputClassName: 'm-l-xs',
size: 'md',
clearable: true,
id: 'u:28cd5a8d0d4f',
},
{
type: 'input-date-range',
name: 'publish_at',
mode: 'inline',
label: '发布日期',
clearable: true,
inputClassName: 'm-l-xs',
value: '',
format: 'YYYY-MM-DD',
ranges:
'yesterday,today,7daysago,prevweek,thismonth,prevmonth,prevquarter',
id: 'u:0b09573c1ed9',
},
],
id: 'u:e19f8840c465',
},
],
id: 'u:83f2045dc8aa',
feat: 'Insert',
},
{ type: 'divider', lineStyle: 'solid', id: 'u:b12bc5cfec5e' },
{
type: 'crud',
name: 'notebooks',
syncLocation: false,
api: {
method: 'get',
url: 'rest/notebooks?select=*,type:dicts!notebook_type_fkey(*),status:dicts!notebook_status_fkey(*)&name=like.*$keywords*&isprivate=is.false&author=$author&type=$type&order=publish_at.desc.nullslast',
data: {
page: '$page',
perPage: 8,
orderBy: '$orderBy',
orderDir: '$orderDir',
publish_at: '$publish_at',
},
requestAdaptor:
"if (api.query.author === '') api.url = api.url.replace('&author=', '')\r\n\r\nif (api.query.type === '') api.url = api.url.replace('&type=', '')\r\n\r\nif (api.body.publish_at === '') {\r\n api.url = api.url.replace('&publish_at=', '')\r\n} else {\r\n let date_former = api.body.publish_at && api.body.publish_at.split(',')[0]\r\n let date_latter = api.body.publish_at && api.body.publish_at.split(',')[1]\r\n\r\n if (date_former === date_latter) {\r\n api.url = api.url.replace(\r\n '&publish_at=' + date_former + '%2C' + date_latter,\r\n '&publish_at=gte.' + date_former + ' 00:00:00&publish_at=lte.' + date_former + ' 23:59:59'\r\n )\r\n } else {\r\n api.url = api.url.replace(\r\n '&publish_at=' + date_former + '%2C' + date_latter,\r\n '&publish_at=gte.' + date_former + ' 00:00:00&publish_at=lte.' + date_latter + ' 23:59:59'\r\n )\r\n }\r\n}\r\n\r\nif (api.query.orderBy && api.query.orderDir) {\r\n api.url = api.url.replace('order=publish_at.desc.nullslast', '');\r\n api.url = api.url.replace('&orderBy=' + api.query.orderBy + '&orderDir=' + api.query.orderDir, '&order=' + api.query.orderBy + '.' + api.query.orderDir);\r\n} else {\r\n api.url = api.url.replace('&orderBy=' + api.query.orderBy + '&orderDir=' + api.query.orderDir, '');\r\n}\r\n\r\nreturn api;",
},
footerToolbar: [
{ type: 'pagination' },
{ type: 'switch-per-page' },
{ type: 'statistics' },
],
perPageAvailable: [8,],
bulkActions: [],
itemActions: [],
id: 'u:46a2b5faa785',
filter: {
title: '',
body: [
{
type: 'input-text',
name: 'keywords',
label: '名称关键字',
id: 'u:a9a2048f6f68',
},
{
type: 'submit',
label: '搜索',
actionType: 'submit',
id: 'u:decd16bff04d',
level: 'primary',
},
],
reload: 'notebooks?keywords=$keywords',
canAccessSuperData: false,
wrapWithPanel: false,
autoFocus: false,
mode: 'inline',
id: 'u:1888123468b3',
feat: 'Insert',
},
messages: {},
card: {
type: 'card',
header: {
title: '',
subTitle: '',
avatar: '/olms/notebook/${pid}/cover.png',
desc: '',
},
body: [
{
name: 'name',
label: '',
type: 'text',
sortable: true,
id: 'u:02b589f37127',
inline: true,
tpl: '名称:${name}',
placeholder: '-',
},
{
name: 'author',
label: '',
type: 'text',
sortable: true,
id: 'u:3c98eb214a1b',
inline: true,
tpl: '创建者:${author}'
},
{
type: 'input-tag',
label: '',
name: 'tag',
id: 'u:7302818b79ff',
source: {
method: 'get',
url: 'rest/tags?order=name',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map((item) => {\r\n return {\r\n ...item,\r\n label: item.name,\r\n value: item.name,\r\n };\r\n }),\r\n};',
},
multiple: true,
joinValues: false,
extractValue: true,
mode: 'normal',
clearable: false,
creatable: true,
createBtnLabel: '新增标签',
addControls: [
{
label: '名称',
type: 'text',
name: 'name',
value: '',
required: true,
mode: 'normal',
},
],
addApi: {
method: 'post',
url: 'rest/tags',
data: { name: '${name}' },
dataType: 'json',
},
editable: false,
editControls: [
{
label: '名称',
type: 'text',
name: 'name',
value: '',
required: true,
mode: 'normal',
},
],
editApi: {
method: 'patch',
url: 'rest/tags?id=eq.${id}',
data: { name: '${name}' },
dataType: 'json',
},
removable: true,
deleteApi: { method: 'delete', url: 'rest/tags?id=eq.${id}' },
optionsTip: '最近您使用的标签',
className: 'm-b-sm m-t-sm',
},
{
name: 'description',
label: '描述',
type: 'text',
sortable: true,
id: 'u:67cf5873f23e',
inline: true,
hidden: true,
},
{
name: 'type.dictvalue',
label: '类型',
type: 'text',
sortable: true,
id: 'u:c3eb052e801c',
inline: true,
hidden: true,
},
{
name: 'publish_at',
label: '发布日期',
type: 'date',
format: 'YYYY-MM-DD',
sortable: true,
id: 'u:fdb56224ba1b',
hidden: true,
},
{
type: 'text',
name: 'tag',
label: '标签',
placeholder: '-',
id: 'u:d3e2a072de3c',
inline: true,
hidden: true,
},
{
type: 'mapping',
map: {
false: '<span class="label label-danger">否</span>',
true: '<span class="label label-success">是</span>',
},
label: '首页展示',
name: 'isrecommend',
sortable: true,
id: 'u:c717e7e057a9',
hidden: true,
},
{
name: 'recommendOrder',
label: '展示次序',
type: 'text',
sortable: true,
id: 'u:77fb26a7f92e',
inline: true,
hidden: true,
},
// {
// type: 'button-group',
// buttons: [
// {
// type: 'button',
// actionType: 'dialog',
// dialog: {
// title: '修改标签',
// body: [
// {
// type: 'form',
// title: '表单',
// api: {
// method: 'patch',
// url: 'rest/notebooks?id=eq.${id}',
// data: { tag: '${tag}' },
// dataType: 'json',
// },
// body: [
// {
// label: '标签',
// type: 'select',
// name: 'tag',
// checkAll: true,
// source: {
// method: 'get',
// url: 'rest/tags?order=name',
// adaptor:
// 'return {\r\n ...payload,\r\n data: payload.data.items.map((item) => {\r\n return {\r\n ...item,\r\n label: item.name,\r\n value: item.name,\r\n };\r\n }),\r\n};',
// },
// multiple: true,
// joinValues: false,
// extractValue: true,
// mode: 'normal',
// clearable: true,
// creatable: true,
// createBtnLabel: '新增标签',
// addControls: [
// {
// label: '名称',
// type: 'text',
// name: 'name',
// value: '',
// required: true,
// mode: 'normal',
// },
// ],
// addApi: {
// method: 'post',
// url: 'rest/tags',
// data: { name: '${name}' },
// dataType: 'json',
// },
// editable: true,
// editControls: [
// {
// label: '名称',
// type: 'text',
// name: 'name',
// value: '',
// required: true,
// mode: 'normal',
// },
// ],
// editApi: {
// method: 'patch',
// url: 'rest/tags?id=eq.${id}',
// data: { name: '${name}' },
// dataType: 'json',
// },
// removable: true,
// deleteApi: {
// method: 'delete',
// url: 'rest/tags?id=eq.${id}',
// },
// },
// ],
// },
// ],
// type: 'dialog',
// closeOnEsc: true,
// showCloseButton: true,
// },
// level: 'link',
// icon: 'fa fa-pencil text-info',
// size: 'md',
// tooltip: '修改标签',
// tooltipPlacement: 'top',
// iconClassName: 'pull-left',
// className: 'p-r-none p-l-none',
// id: 'u:39235263c3cb',
// },
// {
// type: 'button',
// label: '',
// level: 'link',
// size: 'md',
// icon: 'fa fa-eye text-warning',
// iconClassName: 'pull-left',
// actionType: 'dialog',
// tooltip: '预览',
// tooltipPlacement: 'top',
// className: 'p-r-none p-l-none',
// dialog: {
// title: '预览',
// body: [
// {
// type: 'form',
// title: '表单',
// body: [
// {
// type: 'formula',
// name: 'iframeSrc',
// formula:
// '"http://192.168.71.19"+ "/notebook/" + pid + "/" + post',
// },
// {
// type: 'iframe',
// src: '${iframeSrc}',
// height: '700px',
// },
// ],
// },
// ],
// type: 'dialog',
// closeOnEsc: false,
// closeOnOutside: false,
// showCloseButton: true,
// size: 'xl',
// actions: [],
// },
// id: 'u:1aa082ee65f0',
// },
// {
// type: 'button',
// label: '',
// actionType: 'dialog',
// size: 'md',
// tooltip: '首页展示',
// tooltipPlacement: 'top',
// iconClassName: 'pull-left',
// className: 'p-r-none p-l-none',
// icon: 'fa fa-thumbs-up text-info',
// level: 'link',
// visibleOn: 'this.type.dictkey === "notebook"',
// dialog: {
// type: 'dialog',
// title: '首页展示',
// body: [
// {
// type: 'form',
// title: '表单',
// api: {
// method: 'patch',
// url: 'rest/notebooks/$id',
// data: { '&': '$$' },
// requestAdaptor:
// 'if (api.data.recommendOrder) api.data.isrecommend = true;\r\nelse api.data.isrecommend = false;\r\n\r\nreturn api;',
// },
// reload: 'notebooks',
// body: [
// {
// type: 'select',
// label: '序号',
// name: 'recommendOrder',
// clearable: true,
// options: [
// { label: '1', value: 1 },
// { label: '2', value: 2 },
// { label: '3', value: 3 },
// { label: '4', value: 4 },
// { label: '5', value: 5 },
// { label: '6', value: 6 },
// { label: '7', value: 7 },
// { label: '8', value: 8 },
// ],
// },
// ],
// },
// ],
// closeOnEsc: true,
// showCloseButton: true,
// closeOnOutside: false,
// size: 'sm',
// },
// id: 'u:98e68e904df9',
// },
// {
// type: 'button',
// label: '',
// level: 'link',
// icon: 'fa fa-times text-danger',
// size: 'md',
// tooltip: '删除',
// tooltipPlacement: 'top',
// iconClassName: 'pull-left',
// className: 'p-r-none p-l-none',
// actionType: 'ajax',
// api: { method: 'post', url: 'nb/cancel_notebook/$id/$pid' },
// id: 'u:713ccf6aa5fc',
// },
// ],
// id: 'u:2408808996c7',
// label: '',
// },
{
type: 'button-group',
buttons: [
{
type: 'button',
label: '预览',
onEvent: {
click: {
actions: [
{
actionType: 'hidden',
componentId: 'u:5fe9a2ffdbe1',
ignoreError: false,
},
{
componentId: 'u:16d9090a3803',
ignoreError: false,
actionType: 'show',
},
{
componentId: 'my_service1',
actionType: 'setValue',
args: { value: '${event.data}' },
},
],
},
},
level: 'light',
size: 'sm',
id: 'u:1aa082ee65f0',
themeCss: {
className: {
'border:default': {
'top-border-style': 'solid',
'left-border-style': 'solid',
'right-border-style': 'solid',
'bottom-border-style': 'solid',
'top-border-width': 'none',
'left-border-width': 'none',
'right-border-width': 'none',
'bottom-border-width': 'none',
},
},
},
},
],
},
// {
// type: 'button',
// label: '预览',
// onEvent: {
// click: {
// actions: [
// {
// actionType: 'hidden',
// componentId: 'u:5fe9a2ffdbe1',
// ignoreError: false,
// },
// {
// componentId: 'u:16d9090a3803',
// ignoreError: false,
// actionType: 'show',
// },
// {
// componentId: 'my_service1',
// actionType: 'setValue',
// args: { value: '${event.data}' },
// },
// ],
// },
// },
// id: 'u:60093cd4c74f',
// level: 'light',
// size: 'xs',
// },
],
actions: [],
id: 'u:8f1fae00368b',
imageClassName: 'w-full h-sm',
avatarClassName: 'w-full',
},
mode: 'cards',
},
],
style: {
position: 'relative',
display: 'flex',
inset: 'auto',
flexWrap: 'nowrap',
flexDirection: 'column',
alignItems: 'flex-start',
},
size: 'none',
wrapperBody: false,
id: 'u:5fe9a2ffdbe1',
},
{
type: 'container',
body: [
{
type: 'button',
label: '返回',
onEvent: {
click: {
actions: [
{
componentId: 'u:5fe9a2ffdbe1',
ignoreError: false,
actionType: 'show',
},
{
componentId: 'u:16d9090a3803',
ignoreError: false,
actionType: 'hidden',
},
],
},
},
id: 'u:a0f4fc5477c0',
themeCss: {
className: {
'border:default': {
'top-border-width': 'none',
'left-border-width': 'none',
'right-border-width': 'none',
'bottom-border-width': 'none',
},
'font:default': {
color: '#36586b',
fontWeight: '500',
fontSize: '24px',
},
},
},
size: 'lg',
block: false,
level: 'link',
},
{
type: 'page',
body: [
{
type: 'service',
data: {
pid: '490f5308c4bd486fb2225f1e70198768',
post: '00_实践总览.ipynb',
},
body: [
{
type: 'formula',
name: 'iframeSrc',
formula:
'location.origin + "/notebook/" + this.pid + "/" + this.post',
id: 'u:479855e2164e',
},
{
type: 'iframe',
src: '${iframeSrc}/?token=$user.token',
height: '700px',
id: 'u:d2171dd0883e',
},
],
id: 'my_service1',
dsType: 'api',
},
],
id: 'u:d57881a83b7f',
},
],
style: {
position: 'relative',
display: 'flex',
inset: 'auto',
flexWrap: 'nowrap',
flexDirection: 'column',
alignItems: 'flex-start',
},
size: 'none',
wrapperBody: false,
id: 'u:16d9090a3803',
isFixedHeight: false,
isFixedWidth: false,
hidden: true,
visible: false,
},
],
id: 'u:4b1560f3b2db',
asideResizor: false,
pullRefresh: { disabled: true },
definitions: {},
};
export { schema };

View File

@@ -0,0 +1,295 @@
const schema = {
type: 'page',
body: [
{
type: 'form',
title: '表单',
className: '',
submitOnChange: true,
submitOnInit: true,
reload:
'manage_loc?semesterSelect=$semesterSelect&course_id=$course_id&project_id=$project_id&organization_id=$org_id',
wrapWithPanel: false,
canAccessSuperData: false,
mode: 'inline',
name: 'filter',
body: [
{
type: 'group',
body: [
{
type: 'select',
label: '学期',
name: 'semesterSelect',
source: {
method: 'get',
url: 'rest/semesters?order=is_open.desc,since.desc',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map((item) => {\r\n return {\r\n label: item.name,\r\n value: item.id,\r\n };\r\n }),\r\n};',
},
submitOnChange: true,
mode: 'inline',
inputClassName: 'm-l-xs',
size: 'md',
searchable: true,
selectFirst: true,
},
{
type: 'select',
mode: 'inline',
label: '课程',
name: 'course_id',
source: {
method: 'get',
url: 'rest/courses?organization_id=eq.$centre_id&semester_id=eq.$semesterSelect&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
size: 'md',
searchable: true,
selectFirst: true,
className: 'm-l-sm',
inputClassName: 'm-l-xs',
},
{
type: 'select',
label: '项目',
name: 'project_id',
mode: 'inline',
clearable: true,
source: {
method: 'get',
url: 'rest/course2projects?select=projects(id,name)&projects.organization_id=eq.${centre_id}&projects.semester_id=eq.${semesterSelect}&projects.free_schedule=eq.false&course_id=${course_id}',
adaptor:
'let projects = [];\r\npayload.data.items.filter(item => item.projects !== null).forEach(item => {\r\n if (projects.findIndex(x => x.value === item.projects.id) === -1) {\r\n projects.push({\r\n label: item.projects.name,\r\n value: "eq.".concat(item.projects.id)\r\n });\r\n }\r\n});\r\nreturn {\r\n data: projects\r\n}',
requestAdaptor:
"if (api.query.course_id === '') {\r\n api.url = api.url.replace('&course_id=', '');\r\n}\r\n\r\nreturn api;",
sendOn: 'this.course_id',
},
checkAll: false,
size: 'md',
searchable: true,
inputClassName: 'm-l-xs',
className: 'm-l-sm',
},
{
type: 'select',
label: '班级',
name: 'org_id',
mode: 'inline',
size: 'md',
clearable: true,
searchable: true,
source: {
method: 'get',
url: 'rest/orgs?select=*,course2orgs!inner(*)&type=eq.class&path=cd.root.1&course2orgs.course_id=${course_id}&order=name,code',
requestAdaptor:
'if (api.query.course2orgs.course_id === "eq.") {\r\n api.url = api.url.replace("&course2orgs[course_id]=eq.", "")\r\n}\r\nif (api.query.course2orgs.course_id === "eq.undefined") {\r\n api.url = api.url.replace("&course2orgs[course_id]=eq.undefined", "")\r\n}',
adaptor:
'return {\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: "eq." + item.id\r\n }\r\n })\r\n}',
sendOn: 'this.course_id',
},
},
],
},
],
},
{
type: 'divider',
lineStyle: 'solid',
},
{
type: 'crud',
api: {
method: 'get',
url: 'rest/users_ex1?semester_id=eq.$semesterSelect&name=like.*$keywords*&role=eq.student&course_id=$course_id&project_id=$project_id&organization_id=$organization_id&organization_type=eq.class&order=name,code',
data: {
page: '$page',
perPage: '$perPage',
orderBy: '$orderBy',
orderDir: '$orderDir',
},
adaptor: '',
requestAdaptor:
"api.url = api.url.replace('&semesterSelect=', '&semester_id=eq.')\r\napi.url = api.url.replace('&keywords=' + api.query.keywords, '')\r\n\r\nif (api.query.project_id === '') {\r\n api.url = api.url.replace('&project_id=', '')\r\n}\r\n\r\nif (api.query.organization_id === '') {\r\n api.url = api.url.replace('&organization_id=', '')\r\n}\r\n\r\nif (api.query.id === '') {\r\n api.url = api.url.replace('&id=', '')\r\n}\r\n\r\nif (api.query.orderBy && api.query.orderDir) {\r\n api.url = api.url.replace('&order=name%2Ccode', '');\r\n api.url = api.url.replace('&orderBy=' + api.query.orderBy + '&orderDir=' + api.query.orderDir, '&order=' + api.query.orderBy + '.' + api.query.orderDir);\r\n} else {\r\n api.url = api.url.replace('&orderBy=' + api.query.orderBy + '&orderDir=' + api.query.orderDir, '');\r\n}\r\n\r\nreturn {\r\n ...api\r\n}",
sendOn: 'this.course_id',
},
columns: [
{
type: 'text',
label: '学生',
name: 'name',
placeholder: '-',
sortable: true,
},
{
type: 'text',
label: '学号',
name: 'code',
placeholder: '-',
sortable: true,
},
{
type: 'text',
label: '班级',
name: 'organization_name',
placeholder: '-',
sortable: true,
},
{
type: 'text',
label: '课程',
name: 'course_name',
placeholder: '-',
sortable: true,
},
{
type: 'text',
label: '项目',
name: 'project_name',
placeholder: '-',
sortable: true,
},
{
type: 'text',
label: '作业',
name: 'notebook_name',
placeholder: '-',
sortable: true,
},
{
type: 'text',
label: '作业状态',
name: 'status_name',
placeholder: '-',
sortable: true,
},
{
type: 'text',
label: '作业分数',
name: 'score',
placeholder: '-',
sortable: true,
},
{
type: 'button-group',
buttons: [
{
type: 'button',
label: '批阅',
level: 'primary',
size: 'xs',
actionType: 'dialog',
visibleOn:
'this.status === "notebook_submit" || this.status === "notebook_checked"',
dialog: {
title: '批阅作业',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'rest/project_scores?on_conflict=user_id,project_id,item',
data: {
score: '$score',
user_id: '$id',
project_id: '$project_id',
item: 'exp_notebook',
},
headers: {
Prefer: 'resolution=merge-duplicates',
},
},
initApi: {
method: 'get',
url: 'rest/project_scores?user_id=eq.$id&project_id=eq.$project_id&item=eq.exp_notebook',
},
body: [
{
type: 'formula',
name: 'iframeSrc',
formula:
'location.origin + "/notebook/" + notebook_pid + "/" + notebook_post',
},
{
type: 'iframe',
src: '${iframeSrc}',
height: '700px',
},
{
type: 'input-number',
label: '分数',
name: 'score',
required: true,
mode: '',
value: 0,
min: '0',
max: '100',
step: 1,
},
],
},
],
type: 'dialog',
closeOnEsc: false,
closeOnOutside: false,
showCloseButton: true,
size: 'xl',
},
placeholder: '-',
},
],
id: 'u:aab0a651813d',
label: '操作',
placeholder: '-',
},
],
messages: {},
syncLocation: false,
pageField: 'page',
perPageField: 'perPage',
headerToolbar: [],
footerToolbar: [
{
type: 'pagination',
},
{
type: 'switch-per-page',
},
{
type: 'statistics',
},
],
name: 'manage_loc',
perPageAvailable: [10, 20, 30, 40, 50],
filter: {
title: '',
body: [
{
type: 'input-text',
name: 'keywords',
label: '学生关键字',
},
{
type: 'submit',
label: '搜索',
actionType: 'submit',
id: 'u:decd16bff04d',
level: 'primary',
},
],
reload: 'manage_loc?keywords=$keywords',
canAccessSuperData: false,
wrapWithPanel: false,
autoFocus: false,
mode: 'inline',
},
},
],
messages: {},
bodyClassName: '',
title: '',
};
export { schema };

View File

@@ -0,0 +1,329 @@
const schema = {
type: 'page',
body: [
{
type: 'service',
body: [
{
type: 'flex',
items: [
{
type: 'container',
id: 'u:cf018a54788b',
style: {
flexGrow: 1,
flexBasis: '0px',
flex: '1 1 auto',
display: 'flex',
position: 'static',
flexWrap: 'nowrap',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
},
isFixedHeight: false,
body: [
{
type: 'progress',
id: 'u:9ae23711aada',
value: '$cpu',
placeholder: '-',
progressClassName: '',
strokeWidth: 10,
map: [
{ color: '#28a745', value: 30 },
{ color: '#fad733', value: 70 },
{ color: '#dc3545', value: 100 },
],
gapDegree: 75,
gapPosition: 'bottom',
mode: 'dashboard',
},
{
type: 'tpl',
tpl: 'CPU',
inline: false,
wrapperComponent: '',
id: 'u:44a9ee228557',
themeCss: {
baseControlClassName: {
'font:default': { 'text-align': 'center' },
},
},
},
],
},
{
type: 'container',
body: [
{
type: 'progress',
id: 'u:2fc35c429a6c',
value: '$memory',
placeholder: '-',
progressClassName: '',
strokeWidth: 10,
map: [
{ color: '#28a745', value: 30 },
{ color: '#fad733', value: 70 },
{ color: '#dc3545', value: 100 },
],
gapDegree: 75,
gapPosition: 'bottom',
mode: 'dashboard',
},
{
type: 'tpl',
tpl: '内存',
inline: false,
wrapperComponent: '',
id: 'u:fd25e194e7b0',
themeCss: {
baseControlClassName: {
'font:default': { 'text-align': 'center' },
},
},
},
],
size: 'none',
style: {
position: 'static',
display: 'flex',
flex: '1 1 auto',
flexGrow: 1,
flexWrap: 'nowrap',
alignItems: 'center',
flexBasis: '0px',
justifyContent: 'center',
flexDirection: 'column',
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false,
id: 'u:10e16e97e026',
},
{
type: 'container',
body: [
{
type: 'progress',
id: 'u:c1f3e318e1d2',
value: '$disk',
placeholder: '-',
progressClassName: '',
strokeWidth: 10,
map: [
{ color: '#28a745', value: 30 },
{ color: '#fad733', value: 70 },
{ color: '#dc3545', value: 100 },
],
gapDegree: 75,
gapPosition: 'bottom',
mode: 'dashboard',
},
{
type: 'tpl',
tpl: '磁盘',
inline: false,
wrapperComponent: '',
id: 'u:11b1d296b866',
themeCss: {
baseControlClassName: {
'font:default': { 'text-align': 'center' },
},
},
},
],
size: 'none',
style: {
position: 'static',
display: 'flex',
flex: '1 1 auto',
flexGrow: 1,
flexWrap: 'nowrap',
alignItems: 'center',
flexBasis: '0px',
justifyContent: 'center',
overflowY: 'visible',
flexDirection: 'column',
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false,
id: 'u:d63bc21af893',
},
{
type: 'container',
body: [
{
type: 'tpl',
tpl: '当前驱动版本:${gpu.driver}',
inline: true,
wrapperComponent: '',
id: 'u:b66aa4e0241d',
},
{
type: 'cards',
columnsCount: 1,
card: {
type: 'container',
body: [
{
type: 'tpl',
tpl: '${name}',
inline: true,
wrapperComponent: '',
id: 'u:702c8fcd1378',
},
{
type: 'progress',
value: '$percent',
placeholder: '-',
progressClassName: '',
strokeWidth: 10,
map: [
{ color: '#28a745', value: 30 },
{ color: '#fad733', value: 70 },
{ color: '#dc3545', value: 100 },
],
gapDegree: 75,
gapPosition: 'bottom',
mode: 'dashboard',
id: 'u:376de1b837de',
},
],
wrapperBody: false,
style: {
position: 'relative',
display: 'flex',
width: '100%',
flexWrap: 'nowrap',
inset: 'auto',
justifyContent: 'space-evenly',
alignItems: 'center',
},
themeCss: {
baseControlClassName: {
'radius:default': {
'top-left-border-radius': '6px',
'top-right-border-radius': '6px',
'bottom-left-border-radius': '6px',
'bottom-right-border-radius': '6px',
},
'boxShadow:default':
' 0px 0px 10px 0px var(--colors-neutral-line-8)',
'border:default': {
'top-border-width': 'var(--borders-width-1)',
'left-border-width': 'var(--borders-width-1)',
'right-border-width': 'var(--borders-width-1)',
'bottom-border-width': 'var(--borders-width-1)',
'top-border-style': 'var(--borders-style-1)',
'left-border-style': 'var(--borders-style-1)',
'right-border-style': 'var(--borders-style-1)',
'bottom-border-style': 'var(--borders-style-1)',
'top-border-color': '#3be157',
'left-border-color': '#3be157',
'right-border-color': '#3be157',
'bottom-border-color': '#3be157',
},
'padding-and-margin:default': {
paddingTop: '10px',
paddingRight: '10px',
paddingBottom: '10px',
paddingLeft: '10px',
},
},
},
id: 'u:537200fb743f',
isFixedHeight: false,
isFixedWidth: false,
},
placeholder: '',
style: { gutterY: 10 },
id: 'u:f886cae9f2af',
name: 'gpu.list',
className: 'mt-5',
},
],
size: 'none',
style: {
position: 'static',
display: 'block',
flex: '1 1 auto',
flexGrow: 2,
flexBasis: 0,
},
wrapperBody: false,
isFixedHeight: false,
isFixedWidth: false,
id: 'u:6915a8d9638a',
},
],
style: { position: 'relative', rowGap: '10px', columnGap: '10px' },
id: 'u:e4d85d1072cf',
},
],
id: 'u:6399a46b9a99',
dsType: 'api',
ws: { url: 'ws://peiyun.host.platosoft.org:7080/api/nb/ws/metrics' },
},
{
type: 'crud',
syncLocation: false,
api: {
method: 'get',
url: '/hub/users',
data: {},
messages: {},
requestAdaptor: '',
adaptor: '',
dataType: 'json',
},
bulkActions: [],
itemActions: [],
id: 'u:bd445262be74',
perPageAvailable: [5, 10, 20, 50, 100],
messages: {},
listItem: {
body: [
{
name: 'name',
label: '用户',
type: 'text',
id: 'u:83674005c420',
inline: true,
},
{
type: 'datetime',
format: 'YYYY-MM-DD HH:mm:ss',
value: 1729753248,
name: 'last_activity',
id: 'u:92420a25ff12',
},
],
actions: [
{
type: 'button',
label: '停止',
onEvent: { click: { actions: [] } },
id: 'u:2384629a386e',
level: 'danger',
icon: 'fa fa-power-off',
},
],
id: 'u:aaf60b5849b5',
},
mode: 'list',
loadDataOnce: true,
matchFunc: '',
className: 'mt-10',
title: '用户列表',
showHeader: false,
},
],
id: 'u:4b1560f3b2db',
asideResizor: false,
pullRefresh: { disabled: true },
};
export { schema };

View File

@@ -0,0 +1,148 @@
const schema = {
type: 'page',
body: [
{
type: 'dataset-renderer',
name: 'storage',
bucket: 'model',
addFolder: {
type: 'button',
actionType: 'dialog',
reload: 'storage',
dialog: {
type: 'dialog',
title: '新建文件夹',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'storage/v1/object/model/$path/$name/.empty',
requestAdaptor:
"api.url = api.url.replace('//', '/');return api",
},
body: [
{
type: 'input-text',
label: '文件夹名称',
name: 'name',
mode: 'normal',
required: true,
},
],
},
],
closeOnEsc: true,
showCloseButton: true,
},
},
moveFile: {
type: 'button',
actionType: 'dialog',
reload: 'storage',
dialog: {
type: 'dialog',
title: '重命名',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'storage/v1/object/move',
data: {
bucketId: 'model',
sourceKey: '$sourceKey',
dest: '$dest',
path: '$path',
},
requestAdaptor:
"console.log(api)\r\nreturn {\r\n ...api,\r\n data: {\r\n bucketId: api.data.bucketId,\r\n sourceKey: api.data.sourceKey,\r\n destinationKey: api.data.path === '' ? api.data.dest : api.data.path + '/' + api.data.dest\r\n }\r\n}",
},
body: [
{
name: 'dest',
type: 'input-text',
label: '名称',
},
],
},
],
closeOnEsc: true,
showCloseButton: true,
},
},
delFile: {
type: 'button',
actionType: 'ajax',
reload: 'storage',
label: false,
api: {
method: 'delete',
url: '/storage/v1/object/model',
data: {
prefixes: '$prefixes',
},
headers: {
post2rest: false,
},
},
confirmText: '确认删除吗?',
},
delFiles: {
type: 'button',
actionType: 'dialog',
reload: 'storage',
dialog: {
type: 'dialog',
title: '批量删除文件',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'delete',
url: 'storage/v1/object/model',
data: { prefixes: '$prefixes', path: '$path' },
headers: { post2rest: false },
requestAdaptor:
"return {\r\n ...api,\r\n data: {\r\n prefixes: api.data.prefixes.map(item => api.data.path === '' ? item : api.data.path + '/' + item)\r\n }\r\n}",
},
body: [
{
name: 'prefixes',
type: 'checkboxes',
label: false,
inline: false,
checkAll: true,
joinValues: false,
extractValue: true,
required: true,
source: {
method: 'post',
url: '/storage/v1/object/list/model',
data: {
prefix: '$path',
sortBy: {
column: 'name',
order: 'asc',
},
},
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.filter(item => item.id && item.name.charAt(0) !== ".").map(item => {\r\n return {\r\n label: item.name,\r\n value: item.name\r\n }\r\n })\r\n}',
},
},
],
},
],
closeOnEsc: true,
showCloseButton: true,
},
},
},
,
],
};
export { schema };

View File

@@ -0,0 +1,657 @@
const schema = {
type: 'page',
body: [
{
type: 'crud',
messages: {},
api: {
method: 'get',
url: 'rest/subjects?order=name',
data: {
name: 'like.%${name}%',
},
requestAdaptor:
"if (api.query.name === 'like.%%') {\r\n api.url = api.url.replace('&name=like.%25%25', '')\r\n}\r\n\r\nreturn api;",
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n ...item,\r\n sname: item.name,\r\n temp: item.content.map(item => {\r\n return {\r\n ...item,\r\n mooc: item.session.filter(item => item.split("-")[1] === "mooc"),\r\n notebook: item.session.filter(item => item.split("-")[1] === "notebook")\r\n }\r\n })\r\n }\r\n })\r\n }\r\n}',
},
headerToolbar: [
{
type: 'button',
align: 'right',
label: '添加',
icon: 'fa fa-plus',
level: 'default',
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '添加专题',
body: [
{
type: 'form',
title: '表单',
body: [
{
type: 'control',
label: '',
mode: 'normal',
body: [
{
type: 'grid',
columns: [
{
body: [
{
label: '名称',
name: 'sname',
type: 'input-text',
mode: 'normal',
required: true,
id: 'u:f72c2d77e3a4',
},
],
id: 'u:8c6f142936c2',
},
],
id: 'u:c60d1c6c17ae',
},
],
id: 'u:1aab6d05c2f2',
},
{
type: 'button',
label: '内容管理',
onEvent: {
click: {
actions: [
{
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '内容管理',
body: [
{
type: 'form',
title: '表单',
reload: 'subject?content=$content',
body: [
{
label: '内容',
type: 'combo',
name: 'temp',
multiple: true,
multiLine: true,
items: [
{
label: '描述',
type: 'textarea',
name: 'intro',
mode: 'normal',
id: 'u:86fa1674235c',
},
{
type: 'picker',
label: '课程',
name: 'mooc',
valueField: 'value',
labelField: 'name',
modalClassName: 'app-popover',
id: 'u:c8e1e52ea0b1',
modalMode: 'dialog',
strictMode: false,
multiple: true,
source: {
method: 'get',
url: 'rest/mooc?name=${name}&order=name',
data: {
page: '${page}',
perPage: '${perPage}',
name: 'like.%${name}%',
},
requestAdaptor:
"if (api.query.name === 'like.%%') {\r\n api.url = api.url.replace('&name=like.%25%25', '')\r\n}\r\n\r\nreturn api;",
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n ...item,\r\n value: item.id + "-" + item.type + "-" + item.name\r\n }\r\n })\r\n }\r\n}',
},
joinValues: false,
extractValue: true,
embed: true,
pickerSchema: {
mode: 'table',
id: 'u:ae755822f652',
perPageAvailable: [10],
messages: {},
syncLocation: false,
columns: [
{
type: 'text',
label: '编号',
name: 'code',
id: 'u:636f3f0a2ed3',
},
{
type: 'text',
label: '名称',
name: 'name',
searchable: true,
id: 'u:636f3f0a2ed3',
},
],
},
},
{
type: 'picker',
label: '文档',
name: 'notebook',
valueField: 'value',
labelField: 'name',
modalClassName: 'app-popover',
id: 'u:0a92044686ee',
modalMode: 'dialog',
strictMode: false,
multiple: true,
source: {
method: 'get',
url: 'rest/notebooks?name=${name}&author=${author}&order=name',
data: {
page: '${page}',
perPage: '${perPage}',
name: 'like.%${name}%',
author: 'like.%${author}%',
},
requestAdaptor:
"if (api.query.name === 'like.%%') {\r\n api.url = api.url.replace('&name=like.%25%25', '')\r\n}\r\n\r\nif (api.query.author === 'like.%%') {\r\n api.url = api.url.replace('&author=like.%25%25', '')\r\n}\r\n\r\nreturn api;",
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n ...item,\r\n value: item.id + "-" + item.type + "-" + item.name\r\n }\r\n })\r\n }\r\n}',
},
joinValues: false,
extractValue: true,
embed: true,
pickerSchema: {
mode: 'table',
id: 'u:ae755822f652',
perPageAvailable: [10],
messages: {},
syncLocation: false,
columns: [
{
type: 'text',
label: '名称',
name: 'name',
searchable: true,
id: 'u:636f3f0a2ed3',
},
{
type: 'text',
label: '作者',
name: 'author',
searchable: true,
id: 'u:636f3f0a2ed3',
},
],
},
},
],
},
{
type: 'formula',
name: 'content',
formula:
'data.temp.map(item => { return { intro: item.intro, session: item.mooc.concat(item.notebook).filter(item => item) } })',
},
],
id: 'u:d2e6bd28f354',
},
],
showCloseButton: true,
showErrorMsg: true,
showLoading: true,
className: 'app-popover',
id: 'u:b6ff11656496',
closeOnEsc: true,
size: 'lg',
withDefaultData: false,
dataMapSwitch: true,
data: {
'&': '$$',
},
},
},
],
},
},
id: 'u:739469c288af',
level: 'primary',
},
{
type: 'input-array',
label: '内容',
name: 'content',
id: 'u:561926ea3025',
items: {
type: 'combo',
label: '',
controls: [
{
type: 'tpl',
label: '介绍',
tpl: '$intro',
},
{
type: 'input-array',
label: '内容',
name: 'session',
id: 'u:561926ea3025',
items: {
type: 'tpl',
id: 'u:f95cfcfcc0fb',
},
addable: false,
removable: true,
draggable: true,
isSlot: true,
},
],
id: 'u:f95cfcfcc0fb',
multiLine: false,
multiple: false,
messages: {},
},
addable: false,
removable: false,
},
],
api: {
method: 'post',
url: 'rest/subjects',
data: {
name: '$sname',
content: '$content',
},
dataType: 'json',
},
name: 'subject',
id: 'u:6ddf84e7e06c',
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'lg',
id: 'u:2f4c08ecc48e',
},
name: 'semesterSelect',
options: [],
checkAll: false,
submitOnChange: true,
autoComplete: '',
mode: 'inline',
className: '',
id: 'u:8eee4a809327',
},
],
syncLocation: false,
name: 'plan',
footerToolbar: [],
affixHeader: true,
placeholder: '暂无数据',
mode: 'cards',
card: {
type: 'card',
header: {
title: '$name',
},
body: [
{
label: '',
type: 'button-group',
buttons: [
{
type: 'button',
label: '',
actionType: 'dialog',
dialog: {
title: '修改专题',
body: [
{
type: 'form',
title: '表单',
data: { '&': '$$', name: '__undefined' },
body: [
{
type: 'control',
label: '',
mode: 'normal',
body: [
{
type: 'grid',
columns: [
{
body: [
{
label: '名称',
name: 'sname',
type: 'input-text',
mode: 'normal',
required: true,
id: 'u:f72c2d77e3a4',
},
],
id: 'u:8c6f142936c2',
},
],
id: 'u:c60d1c6c17ae',
},
],
id: 'u:1aab6d05c2f2',
},
{
type: 'button',
label: '内容管理',
onEvent: {
click: {
actions: [
{
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '内容管理',
data: { '&': '$$', name: '__undefined' },
body: [
{
type: 'form',
title: '表单',
reload: 'subject?content=$content',
body: [
{
label: '内容',
type: 'combo',
name: 'temp',
multiple: true,
multiLine: true,
items: [
{
label: '描述',
type: 'textarea',
name: 'intro',
mode: 'normal',
id: 'u:86fa1674235c',
},
{
type: 'picker',
label: '课程',
name: 'mooc',
valueField: 'value',
labelField: 'name',
modalClassName: 'app-popover',
id: 'u:c8e1e52ea0b1',
modalMode: 'dialog',
strictMode: false,
multiple: true,
source: {
method: 'get',
url: 'rest/mooc?name=${name}&order=name',
data: {
page: '${page}',
perPage: '${perPage}',
name: 'like.%${name}%',
},
requestAdaptor:
"if (api.query.name === 'like.%%') {\r\n api.url = api.url.replace('&name=like.%25%25', '')\r\n}\r\n\r\nreturn api;",
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n ...item,\r\n value: item.id + "-" + item.type + "-" + item.name\r\n }\r\n })\r\n }\r\n}',
},
joinValues: false,
extractValue: true,
embed: true,
pickerSchema: {
mode: 'table',
id: 'u:ae755822f652',
perPageAvailable: [10],
messages: {},
syncLocation: false,
columns: [
{
type: 'text',
label: '编号',
name: 'code',
id: 'u:636f3f0a2ed3',
},
{
type: 'text',
label: '名称',
name: 'name',
searchable: true,
id: 'u:636f3f0a2ed3',
},
],
},
},
{
type: 'picker',
label: '文档',
name: 'notebook',
valueField: 'value',
labelField: 'name',
modalClassName: 'app-popover',
id: 'u:0a92044686ee',
modalMode: 'dialog',
strictMode: false,
multiple: true,
source: {
method: 'get',
url: 'rest/notebooks?name=${name}&author=${author}&order=name',
data: {
page: '${page}',
perPage: '${perPage}',
name: 'like.%${name}%',
author: 'like.%${author}%',
},
requestAdaptor:
"if (api.query.name === 'like.%%') {\r\n api.url = api.url.replace('&name=like.%25%25', '')\r\n}\r\n\r\nif (api.query.author === 'like.%%') {\r\n api.url = api.url.replace('&author=like.%25%25', '')\r\n}\r\n\r\nreturn api;",
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n ...item,\r\n value: item.id + "-" + item.type + "-" + item.name\r\n }\r\n })\r\n }\r\n}',
},
joinValues: false,
extractValue: true,
embed: true,
pickerSchema: {
mode: 'table',
id: 'u:ae755822f652',
perPageAvailable: [10],
messages: {},
syncLocation: false,
columns: [
{
type: 'text',
label: '名称',
name: 'name',
searchable: true,
id: 'u:636f3f0a2ed3',
},
{
type: 'text',
label: '作者',
name: 'author',
searchable: true,
id: 'u:636f3f0a2ed3',
},
],
},
},
],
},
{
type: 'formula',
name: 'content',
formula:
'data.temp.map(item => { return { intro: item.intro, session: item.mooc.concat(item.notebook).filter(item => item) } })',
},
],
id: 'u:d2e6bd28f354',
},
],
showCloseButton: true,
showErrorMsg: true,
showLoading: true,
className: 'app-popover',
id: 'u:b6ff11656496',
closeOnEsc: true,
size: 'lg',
withDefaultData: false,
dataMapSwitch: false,
},
},
],
},
},
id: 'u:739469c288af',
level: 'primary',
},
{
type: 'input-array',
label: '内容',
name: 'content',
id: 'u:561926ea3025',
items: {
type: 'combo',
label: '',
controls: [
{
type: 'tpl',
label: '介绍',
tpl: '$intro',
},
{
type: 'input-array',
label: '内容',
name: 'session',
id: 'u:561926ea3025',
items: {
type: 'tpl',
id: 'u:f95cfcfcc0fb',
},
addable: false,
removable: true,
draggable: true,
isSlot: true,
},
],
id: 'u:f95cfcfcc0fb',
multiLine: false,
multiple: false,
messages: {},
},
addable: false,
removable: false,
},
],
api: {
method: 'patch',
url: 'rest/subjects/${id}',
data: {
name: '$sname',
content: '$content',
},
dataType: 'json',
},
name: 'subject',
id: 'u:6ddf84e7e06c',
},
],
type: 'dialog',
size: 'md',
closeOnEsc: true,
showCloseButton: true,
},
icon: 'fa fa-pencil text-info',
level: 'link',
size: 'md',
target: '',
block: false,
tooltip: '修改专题',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
},
{
type: 'button',
label: '',
actionType: 'ajax',
icon: 'fa fa-trash text-danger',
iconClassName: 'pull-left',
confirmText: '确认删除"${name}"',
api: {
method: 'delete',
url: 'rest/subjects/${id}',
},
level: 'link',
size: 'md',
tooltip: '删除专题',
tooltipPlacement: 'top',
className: 'p-r-none p-l-none',
},
],
},
{
type: 'divider',
label: '',
},
{
type: 'tabs',
label: '',
tabs: [
{
title: '内容',
body: [
{
type: 'list',
listItem: {
body: [
{
type: 'html',
html: '$intro',
wrapperComponent: '',
id: 'u:d6c7aa76d24f',
},
{
type: 'input-array',
name: 'session',
items: {
type: 'html',
},
addable: false,
removable: false,
},
],
actions: [],
id: 'u:1ddb9863eba8',
},
id: 'u:6a6d7608b012',
source: '$content',
placeholder: '没有数据',
},
],
},
],
className: 'tab',
},
],
actions: [],
actionsCount: 4,
},
columnsCount: 1,
loadDataOnce: true,
masonryLayout: false,
itemClassName: '',
checkAll: false,
id: 'u:f2d78d4b8af6',
},
],
cssVars: {
'--Form-item-gap': 0,
},
headerToolbar: [],
id: 'u:9891040f8810',
};
export { schema };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,850 @@
const schema = {
type: 'page',
body: [
{
type: 'tabs',
className: '',
tabs: [
{
title: '日程一览',
body: [
{
type: 'form',
title: '表单',
className: '',
submitOnChange: true,
submitOnInit: true,
reload:
'manage_loc?course_id=$course_id&project_id=$project_id&teacher_id=$teacher_id&location_id=$location_id&date=$date&no_tch=$no_tch&public=$public&semesterSelect=$semesterSelect',
initApi: {
method: 'get',
url: 'rest/users?role=eq.teacher&id=eq.${user.id}&order=code',
requestAdaptor: '',
adaptor:
"if (payload.data.items.length !== 0) {\r\n return {\r\n teacher_id: 'eq.'.concat(payload.data.items[0].id)\r\n }\r\n} else {\r\n return {}\r\n}",
},
wrapWithPanel: false,
mode: 'inline',
name: 'filter',
body: [
{
type: 'group',
body: [
{
type: 'select',
label: '历史',
name: 'semesterSelect',
mode: 'inline',
clearable: false,
source: {
method: 'get',
url: 'rest/semesters?order=is_open.desc,since.desc',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: item.id\r\n }\r\n })\r\n }\r\n}',
},
checkAll: false,
size: 'md',
searchable: true,
inputClassName: 'm-l-xs',
options: [],
id: 'u:66e672b9a4eb',
},
{
type: 'select',
label: '课程',
name: 'course_id',
mode: 'inline',
clearable: true,
source: {
method: 'get',
url: 'rest/courses?organization_id=eq.$centre_id&semester_id=eq.$semesterSelect&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
checkAll: false,
size: 'md',
searchable: true,
inputClassName: 'm-l-xs',
id: 'u:834c027e3f4a',
},
{
type: 'select',
mode: 'inline',
label: '项目',
name: 'project_id',
clearable: true,
source: {
method: 'get',
url: 'rest/projects?organization_id=eq.$centre_id&semester_id=eq.$semesterSelect&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
checkAll: false,
size: 'md',
searchable: true,
className: 'm-l-sm',
inputClassName: 'm-l-xs',
id: 'u:ce7be11036a7',
},
],
id: 'u:5014fc9e9daf',
},
{
type: 'group',
body: [
{
type: 'input-date-range',
label: '日期',
name: 'date',
mode: 'inline',
clearable: true,
value: '',
format: 'YYYY-MM-DD',
ranges:
'yesterday,today,7daysago,prevweek,thismonth,prevmonth,prevquarter',
inputClassName: 'm-l-xs',
id: 'u:105a2c1566be',
},
{
label: '地点',
type: 'select',
name: 'location_id',
mode: 'inline',
clearable: true,
checkAll: false,
source: {
method: 'get',
url: 'rest/locations?organization_id=eq.$centre_id&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
size: 'md',
searchable: true,
inputClassName: 'm-l-xs',
className: 'm-l-sm',
id: 'u:da7fb6289bbf',
},
{
type: 'select',
name: 'teacher_id',
mode: 'inline',
label: '教师',
clearable: true,
checkAll: false,
source: {
method: 'get',
url: 'rest/users?role=eq.teacher&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
size: 'md',
searchable: true,
inputClassName: 'm-l-xs',
className: 'm-l-sm',
id: 'u:9945a410d682',
},
],
id: 'u:7ef4cbfbbc4c',
},
{
type: 'group',
body: [
{
type: 'switch',
name: 'no_tch',
option: '显示无教师场次',
mode: 'inline',
optionAtLeft: false,
trueValue: 1,
falseValue: 0,
value: 0,
inputClassName: '',
id: 'u:e9b5ab665ddf',
},
{
type: 'switch',
name: 'public',
option: '显示公共场次',
mode: 'inline',
optionAtLeft: false,
trueValue: 1,
falseValue: 0,
value: 0,
inputClassName: 'm-l-sm',
id: 'u:984816e1593e',
},
],
id: 'u:1de38c7bb180',
},
{
type: 'divider',
lineStyle: 'solid',
id: 'u:f99b4d53b5dc',
},
],
id: 'u:ca42a8e96c02',
},
{
type: 'crud',
api: {
method: 'get',
url: 'rest/schedule_stats?organization_id=eq.$centre_id&semester_id=eq.$semesterSelect&is_publish=is.true&order=project_name,date,period_name,location_name,teacher_name',
data: {
page: '$page',
perPage: '$perPage',
'&': '$$',
},
adaptor:
"let supportDateTimeFormat = typeof(Intl.DateTimeFormat) === 'function';\r\npayload.data.items.forEach(item => {\r\n item.day = supportDateTimeFormat ? `${item.date}${new Intl.DateTimeFormat('zh-CN', { weekday: 'short'}).format(new Date(item.date))}` : item.date;\r\n let att_count = item.att_y_count\r\n + item.att_y_late_count\r\n + item.att_y_unclean_count\r\n item.att_num = \"\".concat(\r\n att_count, \"/\", item.current_student_number\r\n )\r\n item.data_num = \"\".concat(\r\n item.data_count, '/', att_count\r\n )\r\n item.data_num_status =\r\n item.data_count === att_count\r\n ? 1\r\n : 0\r\n item.stu_title = item.project_name.concat(\r\n ' :: ', item.date, ' ', item.period_name,\r\n ' :: ', item.location_name,\r\n ' :: ', item.teacher_name ? item.teacher_name : '无教师认领'\r\n )\r\n})\r\n\r\nreturn {\r\n ...payload\r\n}",
requestAdaptor:
"if (api.query.semesterSelect) {\r\n api.url = api.url.replace(/&semesterSelect=\\d*/g, '')\r\n api.url = api.url.replace(/(?<=semester_id=eq\\.)\\d*(?=&)/g, api.query.semesterSelect)\r\n} else {\r\n api.url = api.url.replace(/&semesterSelect=/g, '')\r\n}\r\nif (api.body.course_id === '') {\r\n api.url = api.url.replace(/&course_id=/g, '')\r\n}\r\nif (api.body.project_id === '') {\r\n api.url = api.url.replace(/&project_id=/g, '')\r\n}\r\nif (api.body.date === '') {\r\n api.url = api.url.replace(/&date=/g, '')\r\n}\r\nif (api.body.location_id === '') {\r\n api.url = api.url.replace(/&location_id=/g, '')\r\n}\r\nif (api.body.teacher_id === '') {\r\n api.url = api.url.replace(/&teacher_id=/g, '')\r\n}\r\nif (api.body.no_tch === 0) {\r\n api.url = api.url.replace(/&no_tch=0/g, '')\r\n}\r\nif (api.body.public === 0) {\r\n api.url = api.url.replace(/&public=0/g, '')\r\n}\r\nconst pattern = /(?:date=)(\\d+-\\d+-\\d+)%2C(\\d+-\\d+-\\d+)/g\r\nconst result = pattern.exec(api.url)\r\nif (result && result[1] === result[2]) {\r\n api.url = api.url.replace(result[0], 'date=eq.'.concat(result[1]))\r\n} else if (result) {\r\n api.url = api.url.replace(result[0], 'date=gte.'.concat(result[1], '&date=lte.', result[2]))\r\n}\r\nconst is_no_tch = /no_tch=1/g.test(api.url)\r\nconst has_teacher = /teacher_id=.+?(?=&)/g.test(api.url)\r\nif (is_no_tch && has_teacher) {\r\n const teacher = /teacher_id=.+?(?=&)/g.exec(api.url)\r\n api.url = api.url.replace(teacher, 'or=('.concat(teacher, ',teacher_id.is.null)')).replace('teacher_id=', 'teacher_id.')\r\n api.url = api.url.replace(/no_tch=1/g, '')\r\n} else if (is_no_tch && !has_teacher) {\r\n api.url = api.url.replace(/no_tch=1/g, 'teacher_id=is.null')\r\n}\r\nconst is_public = /public=1/g.test(api.url)\r\nconst has_course = /course_id=.+?(?=&)/g.test(api.url)\r\nif (is_public && has_course) {\r\n const course = /course_id=.+?(?=&)/g.exec(api.url)\r\n api.url = api.url.replace(course, 'or=('.concat(course, ',course_id.is.null)')).replace('course_id=', 'course_id.')\r\n api.url = api.url.replace(/public=1/g, '')\r\n} else if (is_public && !has_course) {\r\n api.url = api.url.replace(/public=1/g, 'course_id=is.null')\r\n}\r\nreturn {\r\n ...api\r\n}",
},
columns: [
{
type: 'tpl',
label: '项目',
tpl: '<% if(!this.course_id) { %>\n <span class="label label-info">公共</span><span> <%= this.project_name %> <span>\n<% } else {%>\n <span> <%= this.project_name %> <span>\n<% } %>',
inline: false,
placeholder: '-',
id: 'u:a41eff5b6b05',
},
{
name: 'day',
label: '日期',
type: 'text',
placeholder: '-',
id: 'u:d4c925b1426d',
},
{
type: 'text',
label: '节次',
name: 'period_name',
placeholder: '-',
id: 'u:97591d821213',
},
{
type: 'text',
label: '地点',
name: 'location_name',
placeholder: '-',
id: 'u:d15f6350ec5c',
},
{
type: 'text',
label: '教师',
name: 'teacher_name',
placeholder: '-',
id: 'u:bb4bb20fe052',
},
{
type: 'text',
label: '考勤数',
placeholder: '-',
name: 'att_num',
id: 'u:d25f22581135',
},
{
type: 'button-group',
label: '操作',
buttons: [
{
type: 'button',
label: '查看学生',
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '学生信息',
body: [
{
type: 'crud',
api: {
method: 'get',
url: 'rest/user2projects?select=*,att_status_text:user2project_att_status_dict_fk(dictvalue),schedule_status:user2project_schedule_status_dict_fk(dictvalue),student:users!user2project_student_id_fkey(*),projects(*)&schedule_id=eq.${id}&schedule_status=neq.canceled&order=id.asc',
data: {},
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map((item,index) => {\r\n return {\r\n ...item,\r\n row_number: index+1\r\n }\r\n })\r\n }\r\n}',
},
columns: [
{
label: '序号',
type: 'plain',
inline: false,
name: 'row_number',
tpl: '',
},
{
name: 'schedule_status.dictvalue',
label: '选课状态',
type: 'plain',
placeholder: '-',
inline: false,
},
{
name: 'student.name',
label: '学生',
type: 'text',
placeholder: '-',
},
{
type: 'text',
label: '学号',
name: 'student.code',
placeholder: '-',
},
{
type: 'text',
label: '考勤状态',
name: 'att_status_text.dictvalue',
placeholder: '-',
groupName: '',
remark: '',
},
{
type: 'operation',
label: '操作',
buttons: [
{
type: 'button',
label: '查看',
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '$student.name($student.code)',
body: [
{
type: 'crud',
syncLocation: false,
api: {
method: 'get',
url: 'rest/tasks?select=*,dict_status:dicts!task_status_fkey(*),dict_type:dicts!task_type_fkey(*)&schedule_id=eq.$schedule_id&user_id=eq.$student.id',
messages: {},
requestAdaptor: '',
adaptor: '',
},
columns: [
{
name: 'content',
label: '任务',
type: 'text',
id: 'u:b7848b797bf9',
placeholder: '-',
},
{
name: 'dict_type.dictvalue',
label: '类型',
type: 'text',
id: 'u:f7ed595f5c57',
placeholder: '-',
},
{
type: 'text',
wrapperComponent: '',
id: 'u:80864ebd90a9',
label: '状态',
placeholder: '-',
name: 'dict_status.dictvalue',
},
],
bulkActions: [],
itemActions: [],
id: 'u:3458736acc74',
perPageAvailable: [10],
messages: {},
},
],
size: 'lg',
actions: [],
closeOnEsc: true,
closeOnOutside: false,
showCloseButton: true,
id: 'u:bc2fcc411e2a',
},
size: 'xs',
level: 'primary',
closeOnEsc: true,
},
],
},
],
messages: {},
headerToolbar: [
{
type: 'tpl',
tpl: '${stu_title}',
},
],
syncLocation: false,
affixHeader: false,
bodyClassName: '',
className: '',
perPageAvailable: [10],
name: 'schedule_data',
silentPolling: true,
},
],
size: 'lg',
actions: [],
closeOnEsc: true,
showCloseButton: true,
closeOnOutside: false,
},
level: 'primary',
size: 'xs',
id: 'u:a05b0e0b829a',
},
],
placeholder: '-',
id: 'u:8bd7633bd4c7',
},
],
messages: {},
syncLocation: false,
pageField: 'page',
perPageField: 'perPage',
headerToolbar: [],
footerToolbar: [
{
type: 'pagination',
},
{
type: 'switch-per-page',
},
{
type: 'statistics',
},
],
name: 'manage_loc',
perPageAvailable: [10, 20, 30, 40, 50],
id: 'u:6488ee7bd91a',
},
],
className: 'p-t-none',
id: 'u:66531bc383d6',
},
{
title: '学生一览',
body: [
{
type: 'grid',
columns: [
{
md: 3,
body: [
{
type: 'form',
reload:
'stu?parent=${tree}&tree=${tree}&org_name=${name}&type=${type}&filter=${filter}',
data: {
parentId: 1,
},
wrapWithPanel: false,
title: '表单',
submitOnChange: true,
target: '',
resetAfterSubmit: false,
initApi: {
method: 'get',
url: 'rest/orgs?type=eq.school',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n tree: payload.data.id\r\n }\r\n}',
headers: {
Accept: 'application/vnd.pgrst.object+json',
},
},
body: [
{
type: 'service',
name: 'service',
messages: {},
api: {
method: 'get',
url: 'rest/orgs/${tree}',
sendOn: 'this.tree',
},
body: [
{
type: 'input-text',
label: '部门类型',
name: 'type',
visibleOn: 'false',
},
{
type: 'input-text',
label: '部门名称',
name: 'name',
visibleOn: 'false',
},
{
type: 'input-text',
label: '部门Path',
name: 'path',
visibleOn: 'false',
value: 'root',
},
{
type: 'input-text',
label: '过滤条件',
name: 'filter',
visibleOn: 'false',
},
{
type: 'formula',
name: 'filter',
formula:
"'org_path.cd.' + data.path + '.' + data.tree + ',org_id.eq.' + data.tree",
condition: 'data.tree',
},
{
type: 'formula',
name: 'filter',
formula: "'org_path.cd.root'",
condition: '!data.tree',
},
],
},
{
type: 'input-tree',
label: '',
name: 'tree',
inputClassName: 'partment-height',
creatable: false,
removable: false,
editable: false,
deleteApi: 'rest/orgs/$id',
rootCreatable: false,
unfoldedLevel: 1,
initiallyOpen: false,
virtualThreshold: false,
source: {
method: 'get',
url: 'rest/orgs?select=*&path=cd.root&order=code',
adaptor:
"const recursive = id => {\r\n let nodes = payload.data.items.filter(x => {\r\n return x.parent === id\r\n })\r\n return nodes.map(item => {\r\n return {\r\n ...item,\r\n label: item.name,\r\n value: `${item.id}`,\r\n children: recursive(item.id)\r\n }\r\n })\r\n}\r\nlet school = payload.data.items.find(x => x.type === 'school');\r\nreturn {\r\n ...payload,\r\n data: [{\r\n label: school.name,\r\n value: `${school.id}`,\r\n children: recursive(school.id)\r\n }]\r\n}",
},
},
],
},
],
},
{
md: 9,
body: [
{
name: 'header',
type: 'wrapper',
body: [
{
type: 'service',
name: 'service',
messages: {},
api: {
method: 'get',
url: 'rest/orgs/${tree}',
sendOn: 'this.tree',
adaptor:
"let role_items = [];\r\nif(payload.data.type === '')\r\nreturn {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n role_items: payload.data.\r\n }\r\n}",
},
body: [
{
type: 'input-text',
label: '部门类型',
name: 'type',
visibleOn: 'false',
},
{
type: 'input-text',
label: '部门名称',
name: 'name',
visibleOn: 'false',
},
],
},
{
type: 'crud',
name: 'stu',
api: {
method: 'get',
url: 'rest/users_ex?and=(or(${filter}),or(code.like.*${keywords}*,name.like.*${keywords}*),role.eq.student)&org_type=eq.class&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n ...item,\r\n stu_id: item.id\r\n }\r\n })\r\n }\r\n}',
data: {
page: '$page',
perPage: '${perPage}',
},
requestAdaptor:
'api.url = api.url.replace(/and=([^&]*?)&/g, (match, p1) => {\r\n let encodeAnd = window.btoa(p1);\r\n\treturn `encodeAnd=${encodeAnd}&`;\r\n});',
sendOn: 'this.filter',
},
columns: [
{
name: 'code',
type: 'text',
label: '账号',
placeholder: '-',
},
{
type: 'text',
label: '姓名',
name: 'name',
placeholder: '-',
},
{
type: 'text',
label: '部门',
name: 'org_name',
placeholder: '-',
},
{
type: 'button-group',
label: '操作',
buttons: [
{
type: 'button',
label: '查看场次',
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '场次信息',
body: [
{
type: 'crud',
api: {
method: 'get',
url: 'rest/user2projects?select=*,courses(id,name),projects(id,code,name,free_schedule),att_status_text:user2project_att_status_dict_fk(dictvalue),schedule_status:user2project_schedule_status_dict_fk(dictvalue)&user_id=eq.$id&schedule_status=neq.canceled&semester_id=eq.$currentSemester&order=created_at.asc',
data: {
page: '$page',
perPage: '$perPage',
},
},
columns: [
{
name: 'schedule_status.dictvalue',
label: '选课状态',
type: 'plain',
placeholder: '-',
inline: false,
},
{
name: 'courses.name',
label: '课程',
type: 'text',
placeholder: '-',
},
{
type: 'tpl',
label: '项目',
name: 'projects.name',
placeholder: '-',
tpl: '<% if(this.projects && this.projects.free_schedule) { %>\n <span class="label label-info">自由安排</span><span><%= this.projects.name %></span>\n<% } else if(this.projects) { %>\n <span><%= this.projects.name %></span>\n<% } %>',
inline: false,
},
{
type: 'text',
label: '考勤状态',
name: 'att_status_text.dictvalue',
placeholder: '-',
groupName: '',
remark: '',
},
{
type: 'operation',
label: '操作',
buttons: [
{
type: 'button',
label: '查看',
actionType: 'dialog',
dialog: {
type: 'dialog',
body: [
{
type: 'crud',
syncLocation: false,
api: {
method: 'get',
url: 'rest/tasks?select=*,dict_status:dicts!task_status_fkey(*),dict_type:dicts!task_type_fkey(*)&schedule_id=eq.$schedule_id&user_id=eq.$user_id',
messages: {},
requestAdaptor: '',
adaptor: '',
},
columns: [
{
name: 'content',
label: '任务',
type: 'text',
id: 'u:b7848b797bf9',
placeholder: '-',
},
{
name: 'dict_type.dictvalue',
label: '类型',
type: 'text',
id: 'u:f7ed595f5c57',
placeholder: '-',
},
{
type: 'text',
wrapperComponent: '',
id: 'u:80864ebd90a9',
label: '状态',
placeholder: '-',
name: 'dict_status.dictvalue',
},
],
bulkActions: [],
itemActions: [],
id: 'u:3458736acc74',
perPageAvailable: [10],
messages: {},
},
],
size: 'lg',
actions: [],
closeOnEsc: true,
closeOnOutside: false,
showCloseButton: true,
id: 'u:bc2fcc411e2a',
},
size: 'xs',
level: 'primary',
closeOnEsc: true,
},
],
},
],
messages: {},
syncLocation: false,
affixHeader: false,
bodyClassName: '',
className: '',
perPageAvailable: [10],
},
],
actions: [],
closeOnEsc: true,
showCloseButton: true,
size: 'lg',
},
level: 'primary',
size: 'xs',
},
],
placeholder: '-',
},
],
messages: {},
className: '',
headerToolbar: [],
md: 9,
syncLocation: false,
footerToolbar: [
{
type: 'form',
wrapWithPanel: false,
reload: 'stu?perPage=${perPage}',
submitOnChange: true,
submitOnInit: false,
body: [
{
type: 'select',
name: 'perPage',
value: '10',
options: [
{
label: '默认',
value: '17',
},
{
label: '10',
value: '10',
},
{
label: '20',
value: '20',
},
{
label: '50',
value: '50',
},
{
label: '100',
value: '100',
},
{
label: '500',
value: '500',
},
],
label: '每页显示',
mode: 'inline',
checkAll: false,
className: 'm-t-sm',
},
],
},
{
type: 'pagination',
},
],
perPageAvailable: '',
columnsTogglable: 'auto',
filter: {
title: '',
affixFooter: false,
actions: [],
body: [
{
type: 'input-group',
name: 'keywords',
label: '学号或姓名 ',
body: [
{
type: 'input-text',
placeholder: '',
inputClassName: 'b-r-none p-r-none',
name: 'keywords',
size: 'md',
clearable: true,
value: '',
},
{
type: 'control',
label: '',
body: [
{
type: 'submit',
level: 'primary',
actionType: 'submit',
label: '搜索',
},
],
},
],
},
],
},
bulkActions: [],
},
],
size: 'xs',
},
],
},
],
className: 'partment-grid',
},
],
},
],
tabsMode: 'line',
tabClassName: '',
id: 'u:4f5cf5424791',
},
],
messages: {},
bodyClassName: '',
title: '',
id: 'u:976cfa59becd',
};
export { schema };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
const schema = {
body: [
{
type: 'form',
api: {
method: 'patch',
url: 'rest/config?key=eq.evaluation&organization_id=eq.$centre_id',
data: {
evaluationInstruStatus: '$evaluationInstruStatus',
evaluationInstru: '$evaluationInstru',
evaluationIntroStatus: '$evaluationIntroStatus',
evaluationIntro: '$evaluationIntro',
},
dataType: 'json',
requestAdaptor:
'return {\r\n ...api,\r\n data: {\r\n value: api.data\r\n }\r\n}',
},
name: 'form',
title: '',
wrapWithPanel: true,
initApi: {
method: 'get',
url: 'rest/config?key=eq.evaluation&organization_id=eq.$centre_id',
adaptor:
'let item = payload.data.items[0]\r\nreturn {\r\n data: {\r\n ...item.value\r\n }\r\n}',
},
body: [
{
type: 'fieldset',
title: '评教指引及评教说明配置',
collapsable: true,
body: [
{
type: 'switch',
name: 'evaluationInstruStatus',
value: true,
option: '评教指引',
optionAtLeft: false,
trueValue: true,
falseValue: false,
onText: '开启',
offText: '关闭',
},
{
type: 'input-rich-text',
label: '',
name: 'evaluationInstru',
mode: 'normal',
visibleOn: '',
clearValueOnHidden: false,
},
{
type: 'switch',
option: '评教说明',
name: 'evaluationIntroStatus',
optionAtLeft: false,
trueValue: true,
falseValue: false,
value: true,
onText: '开启',
offText: '关闭',
},
{
type: 'input-rich-text',
name: 'evaluationIntro',
label: '',
mode: 'normal',
visibleOn: '',
clearValueOnHidden: false,
},
],
},
],
submitText: '保存',
submitOnChange: false,
submitOnInit: false,
reload: '',
affixFooter: false,
},
],
type: 'page',
messages: {},
title: '评教参数设置',
toolbar: [
{ type: 'submit', label: '保存', level: 'primary', target: 'form' },
],
bodyClassName: '',
style: {},
subTitle: '设置评教指引、评教说明等参数',
};
export { schema };

View File

@@ -0,0 +1,789 @@
const schema = {
type: 'page',
body: [
{
type: 'crud',
name: 'eval',
id: 'u:1eda9b4880ff',
columns: [
{
type: 'text',
name: 'parent_name',
label: '一级指标',
id: 'u:6dbfe25ccbea',
placeholder: '-',
quickEdit: {
mode: 'popOver',
saveImmediately: {
api: {
method: 'patch',
url: 'rest/teaching_evaluation_items/$id',
data: {
name: '$parent_name',
},
},
},
type: 'input-text',
name: 'parent_name',
size: 'lg',
},
quickEditEnabledOn: '!this.parent',
},
{
name: 'name',
type: 'text',
label: '二级指标',
id: 'u:a42f65cf3947',
placeholder: '-',
quickEdit: {
mode: 'popOver',
type: 'input-text',
label: '',
name: 'name',
saveImmediately: {
api: {
method: 'patch',
url: 'rest/teaching_evaluation_items/$id',
data: {
name: '$name',
},
},
},
size: 'lg',
},
quickEditEnabledOn: 'this.parent',
},
{
name: 'order',
label: '顺序',
type: 'tpl',
id: 'u:a617718a149f',
placeholder: '-',
tpl: '${order}',
inline: true,
quickEdit: {
type: 'input-number',
name: 'order',
label: '数字',
min: '1',
step: 1,
value: 1,
saveImmediately: {
api: {
method: 'patch',
url: 'rest/teaching_evaluation_items?id=eq.${id}',
data: {
order: '$order',
},
dataType: 'form',
},
},
mode: 'popOver',
},
},
{
type: 'text',
label: '权重',
name: 'weight',
id: 'u:956ac49cc933',
placeholder: '-',
quickEdit: {
type: 'input-number',
name: 'weight',
label: '',
step: 0.5,
min: '0',
saveImmediately: {
api: {
method: 'patch',
url: 'rest/teaching_evaluation_items/$id',
data: {
weight: '$weight',
},
},
},
mode: 'popOver',
},
},
{
type: 'operation',
label: '操作',
id: 'u:6a7475988e72',
buttons: [
{
type: 'button-group',
buttons: [
{
type: 'button',
label: '',
visibleOn: '!this.parent',
icon: 'fa fa-plus text-primary',
level: 'link',
size: 'md',
tooltip: '添加二级指标',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
id: 'u:e0d17fb5cf3e',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '添加二级指标',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'rest/teaching_evaluation_items',
data: {
name: '$name',
weight: '$weight',
organization_id: '$centre_id',
semester_id: '$currentSemester',
parent: '$id',
},
},
body: [
{
type: 'input-text',
label: '名称',
name: 'name',
mode: 'normal',
required: true,
id: 'u:6ba0f199321c',
},
{
type: 'input-number',
name: 'weight',
label: '权重',
mode: 'normal',
min: '0',
step: 0.5,
required: true,
id: 'u:fa0cff8dbbc5',
},
],
id: 'u:a7a2df747e8d',
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:1eda9b4880ff',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'lg',
id: 'u:18243755deb7',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:188cf1c021ad',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:c83910750b73',
},
],
},
},
],
},
},
},
{
type: 'button',
level: 'link',
size: 'md',
icon: 'fa fa-pencil text-info',
visibleOn: '!this.parent',
tooltip: '修改一级指标',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
id: 'u:e2ca6634c8a6',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '修改一级指标',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'patch',
url: 'rest/teaching_evaluation_items/$id',
data: {
name: '$parent_name',
weight: '$weight',
},
dataType: 'json',
},
body: [
{
label: '名称',
type: 'input-text',
name: 'parent_name',
mode: 'normal',
required: true,
id: 'u:e694855bd5a0',
},
{
type: 'input-number',
label: '权重',
name: 'weight',
mode: 'normal',
min: '0',
step: 0.5,
required: true,
id: 'u:6ef32f0c3c51',
},
],
id: 'u:0045f9b77be4',
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:1eda9b4880ff',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'lg',
id: 'u:3e72890a4b8e',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:7472c80f7c68',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:d860e25841e9',
},
],
},
},
],
},
},
},
{
type: 'button',
confirmText: '确认删除"${parent_name}"',
level: 'link',
icon: 'fa fa-times text-danger',
size: 'md',
tooltip: '删除一级指标',
tooltipPlacement: 'top',
reload: '',
visibleOn: '!this.parent && !this.children.length',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
id: 'u:967e36c71712',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
outputVar: 'responseResult',
actionType: 'ajax',
options: {},
api: {
url: 'rest/teaching_evaluation_items/$id',
method: 'delete',
requestAdaptor: '',
adaptor: '',
messages: {},
},
},
{
componentId: 'u:1eda9b4880ff',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
{
type: 'button',
level: 'link',
size: 'md',
icon: 'fa fa-pencil text-info',
visibleOn: 'this.parent',
tooltip: '修改二级指标',
tooltipPlacement: 'top',
className: 'p-r-none p-l-none',
iconClassName: 'pull-left',
id: 'u:8290e35c33ea',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '修改二级指标',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'patch',
url: 'rest/teaching_evaluation_items/$id',
data: {
name: '$name',
weight: '$weight',
parent: '$parent',
},
dataType: 'json',
},
body: [
{
label: '名称',
type: 'input-text',
name: 'name',
mode: 'normal',
required: true,
id: 'u:c2480af665ff',
},
{
type: 'input-number',
label: '权重',
name: 'weight',
mode: 'normal',
min: '0',
step: 0.5,
required: true,
id: 'u:b401731a558d',
},
{
type: 'select',
label: '一级指标',
name: 'parent',
mode: 'normal',
options: [],
checkAll: false,
source: {
method: 'get',
url: 'rest/teaching_evaluation_items?parent=is.null&organization_id=eq.$centre_id&semester_id=eq.$currentSemester&order=order',
adaptor:
'return {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: item.id\r\n }\r\n })\r\n}',
},
searchable: true,
required: true,
id: 'u:d1ed468a2aa3',
},
],
id: 'u:d0af5e81cfa8',
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:1eda9b4880ff',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'lg',
id: 'u:44a6ba40d2f2',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:05a728e1cf94',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:8bed7ea1617c',
},
],
},
},
],
},
},
},
{
type: 'button',
confirmText: '确认删除"${name}"',
level: 'link',
icon: 'fa fa-times text-danger',
size: 'md',
tooltip: '删除二级指标',
tooltipPlacement: 'top',
reload: '',
visibleOn: 'this.parent',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
id: 'u:46e8dd7835fd',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
outputVar: 'responseResult',
actionType: 'ajax',
options: {},
api: {
url: 'rest/teaching_evaluation_items/$id',
method: 'delete',
requestAdaptor: '',
adaptor: '',
messages: {},
},
},
{
componentId: 'u:1eda9b4880ff',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
label: '操作',
id: 'u:ae8cc0b046b6',
},
],
},
],
messages: {},
api: {
method: 'get',
url: 'rest/teaching_evaluation_items?semester_id=eq.$currentSemester&organization_id=eq.$centre_id&order=order',
data: null,
requestAdaptor: '',
adaptor:
'let evaluations = []\r\nlet parents = payload.data.items.filter(item => !item.parent)\r\n\r\nparents.length && parents.forEach((parent, index) => {\r\n evaluations.push({\r\n id: parent.id,\r\n order: parent.order,\r\n parent_name: parent.name,\r\n weight: parent.weight,\r\n parent: parent.parent,\r\n children: payload.data.items\r\n .filter(item => item.parent === parent.id)\r\n })\r\n})\r\n\r\nreturn {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: evaluations\r\n }\r\n}',
sendOn: '',
},
syncLocation: false,
headerToolbar: [
{
type: 'button',
align: 'right',
icon: '',
level: 'primary',
label: '添加一级指标',
id: 'u:37f0878b29a0',
reload: '',
tpl: '内容',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '添加一级指标',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'rest/teaching_evaluation_items',
data: {
name: '$name',
weight: '$weight',
organization_id: '$centre_id',
semester_id: '$currentSemester',
},
},
body: [
{
type: 'input-text',
label: '名称',
name: 'name',
required: true,
mode: 'normal',
id: 'u:53e7e8e5c2f6',
},
{
type: 'input-number',
name: 'weight',
label: '权重',
mode: 'normal',
required: true,
min: '0',
step: 0.5,
id: 'u:a85fdbfc8915',
},
],
id: 'u:4f356ae00643',
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:1eda9b4880ff',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'lg',
id: 'u:bdb5abe2835a',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:0b55901a86eb',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:6dbf2b39f297',
},
],
},
},
],
},
},
},
{
type: 'button',
label: '指定学期导入',
align: 'right',
level: 'primary',
visibleOn: '',
className: '',
id: 'u:595e4f3e93ab',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '指定学期',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'rest/rpc/create_evaluation_items_from_history',
data: {
organization_id: '$centre_id',
semester_id: '$semester_id',
},
},
target: '',
name: 'form',
actions: [
{
actionType: 'reload',
target: 'recognize',
},
],
body: [
{
label: '学期',
type: 'select',
name: 'semester_id',
mode: 'inline',
options: [],
source: {
method: 'get',
url: 'rest/semesters?is_open=is.false&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: item.id\r\n }\r\n })\r\n }\r\n}',
requestAdaptor: '',
},
size: 'md',
inputClassName: 'm-l-xs',
checkAll: false,
clearable: false,
searchable: true,
id: 'u:7ee00833ea34',
},
],
id: 'u:551a790ac5aa',
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:1eda9b4880ff',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'sm',
id: 'u:8d46598e4801',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:39cbcc618226',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:cab0fce5d690',
},
],
},
},
],
},
},
},
],
footerToolbar: [],
perPageAvailable: [10, 20, 30, 40, 50],
filter: null,
bulkActions: [],
className: '',
md: 9,
columnsTogglable: false,
quickSaveItemApi: '',
hideQuickSaveBtn: true,
visibleOn: '',
perPage: null,
loadDataOnce: true,
},
],
id: 'u:23e12f269e33',
messages: {},
title: '',
bodyClassName: '',
};
export { schema };

View File

@@ -0,0 +1,690 @@
const schema = {
body: [
{
type: 'tabs',
className: '',
tabs: [
{
title: '日程一览',
body: [
{
type: 'form',
title: '表单',
className: '',
submitOnChange: true,
submitOnInit: true,
reload:
'manage_loc?course_id=$course_id&project_id=$project_id&teacher_id=$teacher_id&location_id=$location_id&date=$date&no_tch=$no_tch&public=$public&semesterSelect=$semesterSelect',
initApi: {
method: 'get',
url: 'rest/users?role=eq.teacher&id=eq.${user.id}&order=code',
requestAdaptor: '',
adaptor:
"if (payload.data.items.length !== 0) {\r\n return {\r\n teacher_id: 'eq.'.concat(payload.data.items[0].id)\r\n }\r\n} else {\r\n return {}\r\n}",
},
wrapWithPanel: false,
mode: 'inline',
name: 'filter',
body: [
{
type: 'group',
body: [
{
type: 'select',
label: '历史',
name: 'semesterSelect',
options: [],
checkAll: false,
source: {
method: 'get',
url: 'rest/semesters?order=is_open.desc,since.desc',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: item.id\r\n }\r\n })\r\n }\r\n}',
},
searchable: true,
clearable: false,
mode: 'inline',
size: 'md',
inputClassName: 'm-l-xs',
id: 'u:a4b2e9a131fa',
},
{
label: '课程',
type: 'select',
name: 'course_id',
mode: 'inline',
clearable: true,
source: {
method: 'get',
url: 'rest/courses?organization_id=eq.$centre_id&semester_id=eq.$semesterSelect&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
checkAll: false,
size: 'md',
searchable: true,
className: 'm-l-sm',
inputClassName: 'm-l-xs',
id: 'u:22532e0d2a66',
},
{
type: 'select',
label: '实验',
name: 'project_id',
mode: 'inline',
clearable: true,
source: {
method: 'get',
url: 'rest/projects?select=*,course2projects(*)&organization_id=eq.$centre_id&semester_id=eq.$semesterSelect&course2projects.course_id=$course_id&order=code',
adaptor:
"return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.filter(\r\n f => f.course2projects.length > 0\r\n ).map(item => {\r\n return {\r\n label: item.name,\r\n value: 'eq.'.concat(item.id)\r\n }\r\n })\r\n }\r\n}",
requestAdaptor:
'if (api.query.course2projects.course_id === \'\') {\r\n api.url = api.url.replace("&course2projects[course_id]=", "")\r\n}\r\n\r\nreturn api',
},
checkAll: false,
size: 'md',
searchable: true,
className: 'm-l-sm',
inputClassName: 'm-l-xs',
id: 'u:cf571eb6cc1a',
},
],
id: 'u:0cf1ac6473cc',
},
{
type: 'group',
body: [
{
type: 'input-date-range',
label: '日期',
name: 'date',
mode: 'inline',
clearable: true,
value: '',
format: 'YYYY-MM-DD',
ranges:
'yesterday,today,7daysago,prevweek,thismonth,prevmonth,prevquarter',
inputClassName: 'm-l-xs',
id: 'u:b78bf613761a',
},
{
type: 'select',
label: '地点',
name: 'location_id',
mode: 'inline',
clearable: true,
source: {
method: 'get',
url: 'rest/locations?organization_id=eq.$centre_id&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
checkAll: false,
size: 'md',
searchable: true,
className: 'm-l-sm',
inputClassName: 'm-l-xs',
id: 'u:d90502be2317',
},
{
label: '教师',
type: 'select',
name: 'teacher_id',
mode: 'inline',
clearable: true,
checkAll: false,
source: {
method: 'get',
url: 'rest/users?role=eq.teacher&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
size: 'md',
searchable: true,
inputClassName: 'm-l-xs',
className: 'm-l-sm',
id: 'u:2fd7d9613116',
},
],
id: 'u:9df1f6742c55',
},
{
type: 'group',
body: [
{
type: 'switch',
name: 'no_tch',
option: '仅显示无教师场次',
mode: 'inline',
optionAtLeft: false,
trueValue: 1,
falseValue: 0,
value: 0,
disabledOn: 'this.teacher_id',
id: 'u:16519e5c1c12',
},
{
type: 'switch',
name: 'public',
option: '仅显示公共场次',
mode: 'inline',
optionAtLeft: false,
trueValue: 1,
falseValue: 0,
value: 0,
disabledOn: 'this.course_id',
inputClassName: 'm-l-sm',
id: 'u:0d1445244f47',
},
],
id: 'u:6add1eda4c2a',
},
],
id: 'u:42c847232809',
},
{ type: 'divider', lineStyle: 'solid', id: 'u:3eb06bbf6fa8' },
{
type: 'crud',
api: {
method: 'get',
url: 'rest/teaching_evaluation_stats?select=*?organization_id=eq.$centre_id&semester_id=eq.$semesterSelect&is_publish=is.true&order=project_name,date,teacher_name',
data: { '&': '$$', page: '$page', perPage: '$perPage' },
requestAdaptor:
"if (api.query.semesterSelect) {\r\n api.url = api.url.replace(/&semesterSelect=\\d*/g, '')\r\n api.url = api.url.replace(/(?<=semester_id=eq\\.)\\d*(?=&)/g, api.query.semesterSelect)\r\n} else {\r\n api.url = api.url.replace(/&semesterSelect=/g, '')\r\n}\r\nif (api.body.course_id === '') {\r\n api.url = api.url.replace(/&course_id=/g, '')\r\n}\r\nif (api.body.project_id === '') {\r\n api.url = api.url.replace(/&project_id=/g, '')\r\n}\r\nif (api.body.date === '') {\r\n api.url = api.url.replace(/&date=/g, '')\r\n}\r\nif (api.body.location_id === '') {\r\n api.url = api.url.replace(/&location_id=/g, '')\r\n}\r\nif (api.body.teacher_id === '') {\r\n api.url = api.url.replace(/&teacher_id=/g, '')\r\n}\r\nif (api.body.no_tch === 0) {\r\n api.url = api.url.replace(/&no_tch=0/g, '')\r\n}\r\nif (api.body.public === 0) {\r\n api.url = api.url.replace(/&public=0/g, '')\r\n}\r\nconst pattern = /(?:date=)(\\d+-\\d+-\\d+)%2C(\\d+-\\d+-\\d+)/g\r\nconst result = pattern.exec(api.url)\r\nif (result && result[1] === result[2]) {\r\n api.url = api.url.replace(result[0], 'date=eq.'.concat(result[1]))\r\n} else if (result) {\r\n api.url = api.url.replace(result[0], 'date=gte.'.concat(result[1], '&date=lte.', result[2]))\r\n}\r\nconst is_no_tch = /no_tch=1/g.test(api.url)\r\nconst has_teacher = /teacher_id=.+?(?=&)/g.test(api.url)\r\nif (is_no_tch && has_teacher) {\r\n const teacher = /teacher_id=.+?(?=&)/g.exec(api.url)\r\n api.url = api.url.replace(teacher, 'or=('.concat(teacher, ',teacher_id.is.null)')).replace('teacher_id=', 'teacher_id.')\r\n api.url = api.url.replace(/no_tch=1/g, '')\r\n} else if (is_no_tch && !has_teacher) {\r\n api.url = api.url.replace(/no_tch=1/g, 'teacher_id=is.null')\r\n}\r\nconst is_public = /public=1/g.test(api.url)\r\nconst has_course = /course_id=.+?(?=&)/g.test(api.url)\r\nif (is_public && has_course) {\r\n const course = /course_id=.+?(?=&)/g.exec(api.url)\r\n api.url = api.url.replace(course, 'or=('.concat(course, ',course_id.is.null)')).replace('course_id=', 'course_id.')\r\n api.url = api.url.replace(/public=1/g, '')\r\n} else if (is_public && !has_course) {\r\n api.url = api.url.replace(/public=1/g, 'course_id=is.null')\r\n}\r\nreturn {\r\n ...api\r\n}",
adaptor:
"let supportDateTimeFormat = typeof(Intl.DateTimeFormat) === 'function';\r\npayload.data.items.forEach(item => {\r\n item.day = supportDateTimeFormat ? `${item.date}${new Intl.DateTimeFormat('zh-CN', { weekday: 'short'}).format(new Date(item.date))}` : item.date;\r\n item.stu_title = item.project_name.concat(\r\n ' :: ', item.date, ' ', item.period_name,\r\n ' :: ', item.location_name,\r\n ' :: ', item.teacher_name ? item.teacher_name : '无教师认领'\r\n )\r\n})\r\n\r\nreturn {\r\n ...payload\r\n}",
},
columns: [
{
label: '实验',
type: 'tpl',
placeholder: '-',
tpl: '<% if(!this.course_id) { %>\n <span class="label label-info">公共</span><span> <%= this.project_name %> <span>\n<% } else {%>\n <span> <%= this.project_name %> <span>\n<% } %>',
inline: false,
id: 'u:821411c19e45',
},
{
name: 'day',
label: '日期',
type: 'text',
placeholder: '-',
id: 'u:65690067149b',
},
{
type: 'text',
label: '节次',
name: 'period_name',
placeholder: '-',
id: 'u:2aed39988a12',
},
{
type: 'text',
label: '地点',
name: 'location_name',
placeholder: '-',
id: 'u:9298ea2c6eb8',
},
{
type: 'tpl',
tpl: '${teacher_name}',
inline: false,
label: '教师姓名',
name: 'teacher_name',
placeholder: '-',
id: 'u:1b5d4fe03c4f',
},
{
type: 'tpl',
tpl: '${teacher_code}',
inline: false,
label: '教师工号',
name: 'teacher_code',
placeholder: '-',
id: 'u:01a76262c44b',
},
{
type: 'tpl',
tpl: '<% if (data.participation_count<1) { %> \n<span>暂无</span>\n<% } else { %>\n<%= data.evaluation_rating %>\n<% } %>',
inline: false,
label: '统计总分',
name: 'evaluation_rating',
placeholder: '-',
id: 'u:01b6021e08ff',
},
{
type: 'tpl',
tpl: '${participation_count}',
inline: false,
label: '学生评价人数',
name: 'participation_count',
placeholder: '-',
id: 'u:fb9b688b6471',
},
{
type: 'tpl',
tpl: '${current_student_number}',
inline: false,
label: '总选课人数',
name: 'current_student_number',
placeholder: '-',
id: 'u:10215551a08f',
},
{
type: 'tpl',
tpl: '${participation_rating}',
inline: false,
label: '参评率',
name: 'participation_rating',
id: 'u:c3abec775099',
},
{
type: 'operation',
label: '操作',
buttons: [
{
label: '评教明细',
type: 'button',
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '评教明细',
body: [
{
type: 'grid',
columns: [
{
xs: 5,
body: [
{
type: 'form',
title: '表单',
wrapWithPanel: false,
submitOnChange: true,
reload:
'evaluation?selected_user=$selected_user',
name: 'filter',
debug: false,
body: [
{
type: 'switch',
name: 'open',
option: '显示姓名',
optionAtLeft: false,
trueValue: true,
falseValue: false,
value: false,
mode: 'normal',
submitOnChange: false,
id: 'u:ae62dc94f85c',
},
{
label: '',
type: 'input-tree',
name: 'selected_user',
mode: 'inline',
size: 'lg',
source: {
method: 'get',
url: 'rest/user2projects?select=*,users:users!user2project_student_id_fkey(*)&schedule_id=eq.$schedule_id&schedule_status=eq.elected&open=$open',
adaptor:
"return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map((item, index) => {\r\n let temp = null\r\n\r\n if ((index + 1).toString().length === 1) {\r\n temp = '00' + (index + 1)\r\n } else if ((index + 1).toString().length === 2) {\r\n temp = '0' + (index + 1)\r\n } else {\r\n temp = '' + (index + 1)\r\n }\r\n\r\n return {\r\n label: item.users.name\r\n ? '' + temp + ' - 学号: ' + item.users.code + ' - 姓名: ' + item.users.name\r\n : '' + temp + ' - 学号: xxx - 姓名: xxx',\r\n value: item.users.id\r\n }\r\n })\r\n }\r\n}",
data: null,
requestAdaptor:
"if (api.query.open) {\r\n api.url = api.url.replace('&open=true', '')\r\n} else {\r\n api.url = api.url.replace('&open=false', '')\r\n api.url = api.url.replace('%2Cusers%3Ausers%21user2project_student_id_fkey%28%2A%29', '%2Cusers%3Ausers%21user2project_student_id_fkey%28id%29')\r\n}",
sendOn: '',
},
inputClassName: 'evaluation-height',
className: '',
labelClassName: '',
id: 'u:2234b8e979fc',
multiple: false,
enableNodePath: false,
hideRoot: true,
showIcon: true,
initiallyOpen: true,
virtualThreshold: false,
},
],
id: 'u:f15b97a9e90e',
},
],
md: 4,
id: 'u:f5120659ae2f',
},
{
body: [
{
type: 'crud',
api: {
method: 'get',
url: 'rest/user2projects?select=*,teaching_evaluations(*,teaching_evaluation_items(*)),teaching_evaluation_message(content)&schedule_id=eq.$schedule_id&user_id=eq.$selected_user&schedule_status=eq.elected',
sendOn: 'this.selected_user',
adaptor:
'let evaluations = []\r\n\r\npayload.data.items[0].teaching_evaluations\r\n .filter(item => !item.teaching_evaluation_items.parent)\r\n .sort((a, b) => {\r\n return a.teaching_evaluation_items.order - b.teaching_evaluation_items.order\r\n })\r\n .forEach(parent => {\r\n evaluations.push({\r\n ...parent,\r\n parent_name: parent.teaching_evaluation_items.name\r\n })\r\n payload.data.items[0].teaching_evaluations\r\n .filter(item => item.teaching_evaluation_items.parent\r\n === parent.teaching_evaluation_items.id)\r\n .sort((a, b) => {\r\n return a.teaching_evaluation_items.order - b.teaching_evaluation_items.order\r\n })\r\n .forEach(item => evaluations.push({\r\n ...item,\r\n child_name: item.teaching_evaluation_items.name\r\n }))\r\n })\r\n\r\nconsole.log(payload.data.items[0].teaching_evaluation_message[0]);return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n content:payload.data.items[0].teaching_evaluation_message[0]?payload.data.items[0].teaching_evaluation_message[0].content:null, items: evaluations\r\n }\r\n}\r\n',
requestAdaptor: '',
},
columns: [
{
name: 'parent_name',
label: '一级指标',
type: 'text',
id: 'u:b0fbb3e1005c',
},
{
name: 'child_name',
label: '二级指标',
type: 'text',
id: 'u:cf4a41c690ef',
},
{
name: 'evaluation_rating',
label: '评分',
type: 'text',
id: 'u:15f856dabafe',
},
],
bulkActions: [],
itemActions: [],
perPageAvailable: [10],
messages: {},
headerToolbar: [
{
type: 'tpl',
tpl: '${stu_title}',
id: 'u:6ee9e9f6eea1',
},
],
footerToolbar: [
{
type: 'tpl',
tpl: '评语:${content}',
id: 'u:6ee9e9f6eea1',
visibleOn: 'this.content'
},
],
syncLocation: false,
name: 'evaluation',
affixHeader: false,
id: 'u:9c49baa041b9',
},
],
md: 8,
id: 'u:65c044dae897',
},
],
id: 'u:9a86c3778456',
},
],
closeOnEsc: true,
showCloseButton: true,
actions: [],
data: null,
size: 'lg',
closeOnOutside: false,
bodyClassName: '',
id: 'u:43821eb65ad8',
},
level: 'primary',
size: 'xs',
id: 'u:bda614e0a3f8',
},
],
id: 'u:2b35aaf1e56c',
},
],
bulkActions: [],
itemActions: [],
syncLocation: false,
headerToolbar: [
{
type: 'export-excel',
label: '导出',
icon: 'fa fa-download',
level: 'primary',
align: 'right',
api: {
method: 'get',
url: 'rest/teaching_evaluation_stats?organization_id=eq.$centre_id&semester_id=eq.$semesterSelect&is_publish=is.true&order=project_name,date,teacher_name',
dataType: 'form',
data: { semesterSelect: '$semesterSelect' },
requestAdaptor:
"if (api.query.semesterSelect) {\r\n api.url = api.url.replace(/&semesterSelect=\\d*/g, '')\r\n api.url = api.url.replace(/(?<=semester_id=eq\\.)\\d*(?=&)/g, api.query.semesterSelect)\r\n} else {\r\n api.url = api.url.replace(/&semesterSelect=/g, '')\r\n}\r\n\r\nreturn {\r\n ...api\r\n}",
adaptor:
'let columns = [{\r\n "name": "project_name",\r\n "label": "实验" },\r\n {\r\n "name": "date",\r\n "label": "日期" },\r\n {\r\n "name": "teacher_name",\r\n "label": "教师姓名"\r\n },\r\n {\r\n "name": "teacher_code",\r\n "label": "教师工号"\r\n },\r\n {\r\n "name": "evaluation_rating",\r\n "label": "总计总分"\r\n },\r\n {\r\n "name": "participation_count",\r\n "label": "学生评价人数"\r\n },\r\n {\r\n "name": "current_student_number",\r\n "label": "总选课人数"\r\n },\r\n {\r\n "name": "participation_rating",\r\n "label": "参评率"\r\n }];\r\nreturn {\r\n ...payload,\r\n data: {\r\n items: payload.data.items,\r\n columns: columns\r\n }\r\n}',
},
filename: '评教统计',
id: 'u:1b59142be0d3',
},
],
name: 'manage_loc',
perPageAvailable: [10, 20, 30, 40, 50],
messages: {},
id: 'u:367dac93b9ef',
},
],
className: 'p-t-none',
id: 'u:804603aed3fd',
},
{
title: '自由安排',
body: [
{
type: 'crud',
columns: [
{ type: 'text', label: '编号', name: 'code' },
{ name: 'name', type: 'text', label: '课程' },
{
type: 'button-group',
label: '操作',
buttons: [
{
type: 'button',
label: '查看项目',
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '项目信息',
body: [
{
type: 'crud',
api: {
method: 'get',
url: 'rest/course2projects?select=*,projects(*)&course_id=eq.$id',
data: { page: '$page', perPage: '$perPage' },
adaptor:
'let free, not_free\r\nfree = payload.data.items.filter(\r\n item => item.projects.free_schedule\r\n ).sort(\r\n (a, b) => a.projects.code.localeCompare(b.projects.code)\r\n )\r\nnot_free = payload.data.items.filter(\r\n item => !item.projects.free_schedule\r\n ).sort(\r\n (a, b) => a.projects.code.localeCompare(b.projects.code)\r\n )\r\nreturn {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: not_free.concat(free)\r\n }\r\n}',
},
columns: [
{
name: 'projects.code',
label: '编号',
type: 'text',
placeholder: '-',
},
{
name: 'projects.name',
label: '项目',
type: 'tpl',
placeholder: '-',
tpl: '<% if(this.projects && this.projects.free_schedule) { %>\n <span class="label label-info">自由安排</span><span><%= this.projects.name %></span>\n<% } else if(this.projects) { %>\n <span><%= this.projects.name %></span>\n<% } %>',
inline: false,
},
{
type: 'button-group',
label: '操作',
placeholder: '-',
buttons: [
{
type: 'button',
label: '评教明细',
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '评教明细',
body: [
{
type: 'grid',
columns: [
{
name: 'filter',
type: 'form',
title: '表单',
controls: [
{
type: 'switch',
name: 'open',
option: '显示姓名',
optionAtLeft: false,
trueValue: true,
falseValue: false,
value: false,
mode: 'inline',
submitOnChange: false,
},
{
label: '',
type: 'tree',
name: 'selected_user',
options: [],
mode: 'inline',
size: 'md',
source: {
method: 'get',
url: 'rest/user2projects?select=*,users:users!user2project_student_id_fkey(*)&schedule_id=eq.$id&schedule_status=eq.elected&open=$open',
adaptor:
"return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n label: item.users.name ? item.users.name : 'xxx',\r\n value: item.users.id\r\n }\r\n })\r\n }\r\n}",
data: null,
requestAdaptor:
"if (api.query.open) {\r\n api.url = api.url.replace('&open=true', '')\r\n} else {\r\n api.url = api.url.replace('&open=false', '')\r\n api.url = api.url.replace('%2Cusers%3Ausers%21user2project_student_id_fkey%28%2A%29', '%2Cusers%3Ausers%21user2project_student_id_fkey%28id%29')\r\n}",
sendOn: '',
},
},
],
wrapWithPanel: false,
submitOnChange: true,
xs: 4,
reload:
'evaluation?selected_user=$selected_user',
debug: false,
},
{
name: 'evaluation',
type: 'crud',
api: {
method: 'get',
url: 'rest/user2projects?select=*,teaching_evaluations(*,teaching_evaluation_items(*)),teaching_evaluation_message(content)&project_id=eq.$id&user_id=eq.$selected_user&schedule_status=eq.free_schedule',
sendOn: 'this.selected_user',
adaptor:
'let evaluations = []\r\n\r\npayload.data.items[0].teaching_evaluations\r\n .filter(item => !item.teaching_evaluation_items.parent)\r\n .forEach(parent => {\r\n evaluations.push({\r\n ...parent,\r\n parent_name: parent.teaching_evaluation_items.name\r\n })\r\n payload.data.items[0].teaching_evaluations\r\n .filter(item => item.teaching_evaluation_items.parent\r\n === parent.teaching_evaluation_items.id)\r\n .forEach(item => evaluations.push({\r\n ...item,\r\n child_name: item.teaching_evaluation_items.name\r\n }))\r\n })\r\n\r\nreturn {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: evaluations\r\n }\r\n}\r\n',
requestAdaptor: '',
},
columns: [
{
name: 'parent_name',
label: '一级指标',
type: 'text',
},
{
name: 'child_name',
label: '二级指标',
type: 'text',
},
{
name: 'evaluation_rating',
label: '评分',
type: 'text',
},
],
bulkActions: [],
itemActions: [],
perPageAvailable: [10],
messages: {},
syncLocation: false,
},
],
},
],
actions: [],
closeOnEsc: true,
showCloseButton: true,
data: null,
size: 'lg',
},
size: 'xs',
level: 'primary',
visibleOn: 'this.projects.free_schedule',
},
],
},
],
messages: {},
syncLocation: false,
headerToolbar: [],
affixHeader: false,
name: 'project_view',
footerToolbar: [{ type: 'pagination' }],
},
],
size: 'lg',
actions: [],
closeOnEsc: true,
showCloseButton: true,
},
level: 'primary',
size: 'xs',
},
],
},
],
api: {
method: 'get',
url: 'rest/courses?organization_id=eq.$centre_id&semester_id=eq.$semesterSelect&order=code',
data: { page: '$page', perPage: '$perPage' },
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n ...item,\r\n selected_course_id: item.id\r\n }\r\n })\r\n }\r\n}',
},
messages: {},
syncLocation: false,
pageField: 'page',
perPageField: 'perPage',
},
],
className: 'p-t-none',
},
],
tabsMode: 'line',
tabClassName: '',
id: 'u:264fc2e5b77b',
},
],
type: 'page',
messages: {},
bodyClassName: '',
title: '',
definitions: {
options: {
type: 'combo',
multiple: true,
multiLine: true,
controls: [
{
type: 'group',
controls: [
{ label: '选项', name: 'key', type: 'text' },
{ label: '内容', name: 'label', type: 'text' },
{
type: 'switch',
option: '开关',
name: 'is_fixed',
label: '是否固定',
},
],
label: false,
},
],
},
},
id: 'u:21edf657a725',
};
export { schema };

View File

@@ -0,0 +1,323 @@
const schema = {
body: [
{
type: 'crud',
name: 'custom_tp',
api: {
method: 'get',
url: 'rest/project_process_templates?select=*,exam_paper:exam_paper!project_preparation_exam_exam_paper_id_fk(*)&organization_id=eq.$centre_id&semester_id=eq.${currentSemester}&order=code',
adaptor: '',
data: { page: '$page', perPage: '$perPage' },
requestAdaptor: '',
},
columns: [
{ name: 'code', label: '编号', type: 'text', placeholder: '-' },
{ name: 'name', label: '名称', type: 'text', placeholder: '-' },
{
type: 'text',
label: '考试试卷',
name: 'exam_paper.name',
placeholder: '-',
},
{
type: 'tpl',
inline: false,
name: 'exam_start_fix',
label: '上课多久允许答题',
style: {},
},
{
type: 'text',
label: '持续时间',
placeholder: '-',
name: 'exam_duration',
},
{
type: 'button-group',
buttons: [
{
type: 'button',
label: '设置考试',
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '设置实验考试',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'rest/project_preparation_exam',
data: {
name: '$name',
exam_paper_id: '$exam_paper_id',
type: '$type',
exam_start_time: '$exam_start_time',
exam_start_fix: '$exam_start_fix',
exam_duration: '$exam_duration',
organization_id: '${centre_id}',
project_code: '$code',
exam_type: 'process_assessment',
},
requestAdaptor:
'if(!api.data.exam_start_fix) {\r\n api.data.exam_start_fix=0;\r\n}\r\nif(!api.data.exam_duration) {\r\n api.data.exam_duration=10;\r\n}\r\nreturn api;',
headers: { Prefer: 'resolution=merge-duplicates' },
},
initApi: {
method: 'get',
url: 'rest/project_preparation_exam/$template_id',
sendOn: 'this.template_id !== null',
data: null,
adaptor:
'if(payload.data.exam_start_time){payload.data.exam_start_time=payload.data.exam_start_time.substring(0,19).replace("T", " ");}\r\nreturn payload;',
},
body: [
{
label: '名称',
name: 'name',
mode: 'normal',
type: 'input-text',
required: true,
},
{
type: 'select',
label: '选择实验考试试卷',
name: 'exam_paper_id',
options: [],
mode: 'normal',
required: true,
checkAll: false,
source: {
method: 'get',
url: 'rest/exam_paper?select=label:name,value:id&type=eq.exam_template_preparation',
},
},
{
type: 'formula',
name: 'type',
condition: 'data.free_schedule',
formula: "'exam_time_fixed'",
},
{
type: 'list-select',
label: '',
name: 'type',
mode: 'normal',
options: [
{
label: '以上课时间为参考',
value: 'exam_time_offset',
},
{ label: '固定开始时间', value: 'exam_time_fixed' },
],
value: 'exam_time_offset',
disabledOn: 'this.free_schedule',
},
{
type: 'input-datetime',
label: '考试时间',
name: 'exam_start_time',
mode: 'normal',
required: true,
visibleOn: "this.type === 'exam_time_fixed'",
placeholder: '请选择考试日期时间',
value: '',
minDate: '',
maxDate: '',
format: 'YYYY-MM-DD HH:mm:ss',
inputFormat: 'YYYY-MM-DD HH:mm:ss',
},
{
type: 'input-text',
label: '上课多久允许答题',
name: 'exam_start_fix',
mode: 'normal',
description:
'使用d/h/m或D/H/M表示天/小时/分钟,默认为分钟,负值为提前',
validations: {
matchRegexp1:
/^(\d+|\d+[Dd]|\d+[Hh]|\d+[Mm])$|^-(\d+|\d+[Dd]|\d+[Hh]|\d+[Mm])$/,
},
validationErrors: {
matchRegexp1:
'请输入正确的格式使用d/h/m或D/H/M表示天/小时/分钟',
},
validateOnChange: true,
step: 1,
required: true,
visibleOn: "this.type === 'exam_time_offset'",
},
{
type: 'input-text',
label: '持续时间',
name: 'exam_duration',
mode: 'normal',
description:
'使用d/h/m或D/H/M表示天/小时/分钟,默认为分钟',
validations: {
matchRegexp1: /^(\d+|\d+[Dd]|\d+[Hh]|\d+[Mm])$/,
},
validationErrors: {
matchRegexp1:
'请输入正确的格式使用d/h/m或D/H/M表示天/小时/分钟',
},
validateOnChange: true,
step: 1,
required: true,
},
],
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'lg',
closeOnOutside: false,
},
level: 'primary',
icon: 'fa fa-cog',
size: 'sm',
className: 'pull-right',
},
],
label: '操作',
},
],
messages: {},
syncLocation: false,
headerToolbar: [],
footerToolbar: [
{ type: 'pagination' },
{ type: 'switch-per-page' },
{ type: 'statistics' },
],
bodyClassName: 'rpt-tp-height',
affixHeader: false,
perPageAvailable: [10, 20, 30, 40, 50],
perPageField: 'perPage',
},
],
type: 'page',
title: '',
messages: {},
bodyClassName: '',
definitions: {
variable: {
type: 'combo',
label: '组合输入',
name: 'combo',
multiple: true,
multiLine: false,
joinValues: true,
messages: {},
mode: 'normal',
conditions: [
{
label: '变量',
test: 'this.type === "text"',
controls: [{ type: 'text', label: '变量', name: 'string' }],
scaffold: { type: 'text', label: '变量', name: '' },
},
{
label: '对象',
test: 'this.type === "object"',
controls: [
{ type: 'text', label: '变量', name: 'string' },
{ $ref: 'variable' },
],
scaffold: { type: 'object', label: '对象', name: '' },
},
{
label: '数组',
test: 'this.type === "array"',
controls: [{ $ref: 'variable' }],
scaffold: { type: 'array', label: '数组', name: '' },
},
],
},
textItem: {
type: 'group',
controls: [
{
name: 'type',
value: 'text',
type: 'select',
clearable: false,
size: 'sm',
options: [
{ label: '变量', value: 'string' },
{ label: '对象', value: 'object' },
{ label: '数组', value: 'array' },
],
},
{ name: 'name', type: 'text', placeholder: '名称', required: true },
{ name: 'title', type: 'text', placeholder: '说明', required: true },
{
type: 'text',
name: 'default',
placeholder: '例值',
required: true,
visibleOn: "this.type==='text'",
},
],
},
objectItem: {
type: 'combo',
multiLine: true,
controls: [{ name: 'data', value: {}, $ref: 'fieldItem', minLength: 1 }],
multiple: false,
},
arrayItem: {
type: 'combo',
multiLine: true,
controls: [
{ name: 'data', value: {}, $ref: 'elementItem', minLength: 1 },
],
multiple: false,
},
fieldItem: {
type: 'combo',
multiple: true,
multiLine: true,
typeSwitchable: false,
controls: [
{ $ref: 'textItem', minLength: 0 },
{
$ref: 'objectItem',
minLength: 0,
visibleOn: "this.type==='object'",
name: 'children',
},
{
$ref: 'arrayItem',
minLength: 0,
visibleOn: "this.type==='array'",
name: 'children',
},
],
},
elementItem: {
type: 'combo',
multiple: false,
multiLine: true,
typeSwitchable: false,
controls: [
{ $ref: 'textItem', minLength: 0, unique: true },
{
$ref: 'objectItem',
minLength: 0,
visibleOn: "this.type==='object'",
name: 'children',
},
{
$ref: 'arrayItem',
minLength: 0,
visibleOn: "this.type==='array'",
name: 'children',
},
],
},
},
};
export { schema };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,322 @@
const schema = {
body: [
{
type: 'crud',
name: 'custom_tp',
api: {
method: 'get',
url: 'rest/project_exam_templates?select=*,exam_paper:exam_paper!project_preparation_exam_exam_paper_id_fk(*)&organization_id=eq.$centre_id&semester_id=eq.${currentSemester}&order=code',
adaptor: '',
data: { page: '$page', perPage: '$perPage' },
requestAdaptor: '',
},
columns: [
{ name: 'code', label: '编号', type: 'text', placeholder: '-' },
{ name: 'name', label: '名称', type: 'text', placeholder: '-' },
{
type: 'text',
label: '考试试卷',
name: 'exam_paper.name',
placeholder: '-',
},
{
type: 'tpl',
inline: false,
name: 'exam_start_fix',
label: '上课多久允许答题',
style: {},
},
{
type: 'text',
label: '持续时间',
placeholder: '-',
name: 'exam_duration',
},
{
type: 'button-group',
buttons: [
{
type: 'button',
label: '设置考试',
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '设置实验考试',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'rest/project_preparation_exam',
data: {
name: '$name',
exam_paper_id: '$exam_paper_id',
type: '$type',
exam_start_time: '$exam_start_time',
exam_start_fix: '$exam_start_fix',
exam_duration: '$exam_duration',
organization_id: '${centre_id}',
project_code: '$code',
},
requestAdaptor:
'if(!api.data.exam_start_fix) {\r\n api.data.exam_start_fix=0;\r\n}\r\nif(!api.data.exam_duration) {\r\n api.data.exam_duration=10;\r\n}\r\nreturn api;',
headers: { Prefer: 'resolution=merge-duplicates' },
},
initApi: {
method: 'get',
url: 'rest/project_preparation_exam/$template_id',
sendOn: 'this.template_id !== null',
data: null,
adaptor:
'if(payload.data.exam_start_time){payload.data.exam_start_time=payload.data.exam_start_time.substring(0,19).replace("T", " ");}\r\nreturn payload;',
},
body: [
{
label: '名称',
name: 'name',
mode: 'normal',
type: 'input-text',
required: true,
},
{
type: 'select',
label: '选择实验考试试卷',
name: 'exam_paper_id',
options: [],
mode: 'normal',
required: true,
checkAll: false,
source: {
method: 'get',
url: 'rest/exam_paper?select=label:name,value:id&type=eq.exam_template_preparation',
},
},
{
type: 'formula',
name: 'type',
condition: 'data.free_schedule',
formula: "'exam_time_fixed'",
},
{
type: 'list-select',
label: '',
name: 'type',
mode: 'normal',
options: [
{
label: '以上课时间为参考',
value: 'exam_time_offset',
},
{ label: '固定开始时间', value: 'exam_time_fixed' },
],
value: 'exam_time_offset',
disabledOn: 'this.free_schedule',
},
{
type: 'input-datetime',
label: '考试时间',
name: 'exam_start_time',
mode: 'normal',
required: true,
visibleOn: "this.type === 'exam_time_fixed'",
placeholder: '请选择考试日期时间',
value: '',
minDate: '',
maxDate: '',
format: 'YYYY-MM-DD HH:mm:ss',
inputFormat: 'YYYY-MM-DD HH:mm:ss',
},
{
type: 'input-text',
label: '上课多久允许答题',
name: 'exam_start_fix',
mode: 'normal',
description:
'使用d/h/m或D/H/M表示天/小时/分钟,默认为分钟,负值为提前',
validations: {
matchRegexp1:
/^(\d+|\d+[Dd]|\d+[Hh]|\d+[Mm])$|^-(\d+|\d+[Dd]|\d+[Hh]|\d+[Mm])$/,
},
validationErrors: {
matchRegexp1:
'请输入正确的格式使用d/h/m或D/H/M表示天/小时/分钟',
},
validateOnChange: true,
step: 1,
required: true,
visibleOn: "this.type === 'exam_time_offset'",
},
{
type: 'input-text',
label: '持续时间',
name: 'exam_duration',
mode: 'normal',
description:
'使用d/h/m或D/H/M表示天/小时/分钟,默认为分钟',
validations: {
matchRegexp1: /^(\d+|\d+[Dd]|\d+[Hh]|\d+[Mm])$/,
},
validationErrors: {
matchRegexp1:
'请输入正确的格式使用d/h/m或D/H/M表示天/小时/分钟',
},
validateOnChange: true,
step: 1,
required: true,
},
],
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'lg',
closeOnOutside: false,
},
level: 'primary',
icon: 'fa fa-cog',
size: 'sm',
className: 'pull-right',
},
],
label: '操作',
},
],
messages: {},
syncLocation: false,
headerToolbar: [],
footerToolbar: [
{ type: 'pagination' },
{ type: 'switch-per-page' },
{ type: 'statistics' },
],
bodyClassName: 'rpt-tp-height',
affixHeader: false,
perPageAvailable: [10, 20, 30, 40, 50],
perPageField: 'perPage',
},
],
type: 'page',
title: '',
messages: {},
bodyClassName: '',
definitions: {
variable: {
type: 'combo',
label: '组合输入',
name: 'combo',
multiple: true,
multiLine: false,
joinValues: true,
messages: {},
mode: 'normal',
conditions: [
{
label: '变量',
test: 'this.type === "text"',
controls: [{ type: 'text', label: '变量', name: 'string' }],
scaffold: { type: 'text', label: '变量', name: '' },
},
{
label: '对象',
test: 'this.type === "object"',
controls: [
{ type: 'text', label: '变量', name: 'string' },
{ $ref: 'variable' },
],
scaffold: { type: 'object', label: '对象', name: '' },
},
{
label: '数组',
test: 'this.type === "array"',
controls: [{ $ref: 'variable' }],
scaffold: { type: 'array', label: '数组', name: '' },
},
],
},
textItem: {
type: 'group',
controls: [
{
name: 'type',
value: 'text',
type: 'select',
clearable: false,
size: 'sm',
options: [
{ label: '变量', value: 'string' },
{ label: '对象', value: 'object' },
{ label: '数组', value: 'array' },
],
},
{ name: 'name', type: 'text', placeholder: '名称', required: true },
{ name: 'title', type: 'text', placeholder: '说明', required: true },
{
type: 'text',
name: 'default',
placeholder: '例值',
required: true,
visibleOn: "this.type==='text'",
},
],
},
objectItem: {
type: 'combo',
multiLine: true,
controls: [{ name: 'data', value: {}, $ref: 'fieldItem', minLength: 1 }],
multiple: false,
},
arrayItem: {
type: 'combo',
multiLine: true,
controls: [
{ name: 'data', value: {}, $ref: 'elementItem', minLength: 1 },
],
multiple: false,
},
fieldItem: {
type: 'combo',
multiple: true,
multiLine: true,
typeSwitchable: false,
controls: [
{ $ref: 'textItem', minLength: 0 },
{
$ref: 'objectItem',
minLength: 0,
visibleOn: "this.type==='object'",
name: 'children',
},
{
$ref: 'arrayItem',
minLength: 0,
visibleOn: "this.type==='array'",
name: 'children',
},
],
},
elementItem: {
type: 'combo',
multiple: false,
multiLine: true,
typeSwitchable: false,
controls: [
{ $ref: 'textItem', minLength: 0, unique: true },
{
$ref: 'objectItem',
minLength: 0,
visibleOn: "this.type==='object'",
name: 'children',
},
{
$ref: 'arrayItem',
minLength: 0,
visibleOn: "this.type==='array'",
name: 'children',
},
],
},
},
};
export { schema };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,104 @@
const schema = {
body: [
{
type: 'crud',
messages: {},
api: {
method: 'get',
url: 'rest/projects?select=*&organization_id=eq.${centre_id}&semester_id=eq.${currentSemester}&order=code',
data: { page: '$page', perPage: '$perPage' },
},
headerToolbar: [],
syncLocation: false,
pageField: 'page',
perPageField: 'perPage',
name: 'score',
footerToolbar: [
{ type: 'pagination' },
{ type: 'switch-per-page' },
{ type: 'statistics' },
],
mode: 'table',
affixHeader: true,
perPageAvailable: [10, 20, 30, 40, 50],
columns: [
{ name: 'code', type: 'text', label: '编号' },
{ type: 'text', name: 'name', label: '名称' },
{
type: 'operation',
label: '操作',
buttons: [
{
type: 'button',
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '附件管理',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'patch',
url: 'rest/projects?id=eq.${id}',
data: {
files: '${files}',
},
dataType: 'json',
},
body: [
{
type: 'combo',
label: '',
name: 'files',
mode: 'inline',
inputClassName: 'm-l-xs',
multiple: true,
controls: [
{
type: 'dataset-picker',
bucket: 'mooc',
name: 'path',
required: true,
},
{ type: 'tpl', tpl: '$path' },
],
multiLine: false,
joinValues: false,
draggable: false,
strictMode: false,
delimiter: ',',
scaffold: '',
disabled: false,
disabledOn: '',
removable: true,
},
],
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'lg',
},
level: 'link',
size: 'md',
icon: 'fa fa-cog text-warning',
tooltip: '附件管理',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'm-r-none m-l-none',
},
],
placeholder: '-',
},
],
bodyClassName: '',
initApi: '',
initFetch: '',
label: '名称',
},
],
type: 'page',
};
export { schema };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
const schema = {
type: 'page',
body: {
type: 'iframe',
src: '/res',
height: window.innerHeight - 80,
},
};
window.parent.postMessage(
{
type: 'amis:resize',
data: {
height: window.innerHeight - 80
}
},
'*'
);
export { schema };

View File

@@ -0,0 +1,423 @@
const schema = {
type: 'page',
body: [
{
type: 'crud',
id: 'u:d0a463529569',
columns: [
{
type: 'text',
name: 'title',
label: '标题',
id: 'u:068708870a6a',
},
{
name: 'creator.name',
type: 'text',
label: '发送人',
id: 'u:118fc9ffbf89',
},
{
name: 'created_at',
label: '发送时间',
type: 'date',
id: 'u:9f0439c43297',
},
{
type: 'button-group',
label: '操作',
id: 'u:537781aa8eec',
placeholder: '-',
buttons: [
{
type: 'button',
level: 'link',
icon: 'fa fa-eye',
size: 'md',
tooltip: '删除',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
id: 'u:9731651193a3',
onEvent: {
click: {
weight: 0,
actions: [
{
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '${title}',
body: [
{
type: 'tpl',
tpl: '${data}',
wrapperComponent: 'p',
inline: false,
id: 'u:2f15ac0923a7',
},
],
showCloseButton: true,
showErrorMsg: true,
showLoading: true,
className: 'app-popover',
id: 'u:93d802668933',
closeOnEsc: true,
actions: [],
},
},
],
},
},
},
{
type: 'button',
confirmText: '确认删除?',
level: 'link',
icon: 'fa fa-times text-danger',
size: 'md',
tooltip: '删除',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
id: 'u:9731651193a3',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
outputVar: 'responseResult',
actionType: 'ajax',
options: {},
api: {
url: 'rest/notifications/$id',
method: 'delete',
requestAdaptor: '',
adaptor: '',
messages: {},
},
},
{
componentId: 'u:d0a463529569',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
width: 200,
},
],
messages: {},
api: {
method: 'get',
url: 'rest/notifications?select=*,creator:users!notification_creator_id_fkey(id,name),user_notifications!user_notification_notification_id_fkey(users(name))&order=created_at.desc&organization_id=eq.${centre_id}&type=eq.announcement',
data: null,
},
headerToolbar: [
{
type: 'button',
align: 'right',
icon: 'fa fa-plus text-primary',
level: 'link',
label: '',
id: 'u:6adf561275c3',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '发送消息',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'rest/notifications',
data: {
'&': '$$',
organization_id: '$centre_id',
},
dataType: 'json',
requestAdaptor:
"let notification = {\r\n title: api.data.title,\r\n data: api.data.data,\r\n push2wechat: api.data.push2wechat,\r\n organization_id: api.data.organization_id\r\n};\r\nconsole.log(api)\r\nif (!api.data.push2wechat && api.data.target === 'toorg') {\r\n notification.org_list = `{${api.data.org_list}}`\r\n} else if (!api.data.push2wechat && api.data.target === 'touser') {\r\n notification.user_id_list = `{${api.data.user_id_list}}`\r\n}\r\nreturn {\r\n ...api,\r\n data: notification\r\n}",
},
body: [
{
type: 'group',
label: false,
name: 'title',
id: 'u:5cb966ef8b55',
body: [
{
type: 'input-text',
label: '',
name: 'title',
mode: 'normal',
required: true,
columnRatio: 9,
placeholder: '消息标题',
id: 'u:2e6661386694',
},
{
type: 'switch',
name: 'push2wechat',
option: '是否推送微信消息',
mode: 'normal',
label: '',
optionAtLeft: false,
trueValue: true,
falseValue: false,
value: false,
id: 'u:90d1abf14f6c',
},
],
},
{
type: 'control',
label: '发送给谁',
mode: 'normal',
id: 'u:9064f08b91d6',
className: 'm-t-sm',
visibleOn: '!this.push2wechat',
body: [
{
type: 'grid',
columns: [
{
body: [
{
type: 'select',
label: '',
name: 'target',
options: [
{
label: '所有人',
value: 'toall',
},
{
label: '指定班级',
value: 'toorg',
},
{
label: '指定人',
value: 'touser',
},
],
checkAll: false,
required: true,
value: 'touser',
visibleOn: '',
className: '1css',
labelClassName: '2css',
inputClassName: '3css',
id: 'u:150a4240cd56',
},
],
id: 'u:65ffd686a587',
},
{
body: [
{
label: '',
name: 'org_list',
type: 'picker',
visibleOn: "this.target === 'toorg'",
required: true,
pickerSchema: {
mode: 'list',
listItem: {
title: '${label}',
},
messages: {},
syncLocation: false,
defaultParams: {
page: 1,
perPage: 10,
},
},
source: {
method: 'get',
url: 'rest/orgs?type=eq.class',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: item.id\r\n }\r\n })\r\n }\r\n}',
},
multiple: true,
mode: 'normal',
modalMode: 'dialog',
joinValues: true,
id: 'u:8cb5375b4bfb',
},
{
type: 'picker',
name: 'user_id_list',
visibleOn: "this.target === 'touser'",
required: true,
pickerSchema: {
mode: 'list',
listItem: {
title: '${label}',
},
messages: {},
footerToolbar: [
{
type: 'pagination',
tpl: '内容',
},
],
defaultParams: {
perPage: 10,
page: 1,
},
syncLocation: false,
filter: {
type: 'form',
title: '表单',
wrapWithPanel: false,
submitOnInit: false,
submitOnChange: false,
target: '',
resetAfterSubmit: false,
body: [
{
type: 'input-text',
name: 'keywords',
clearable: true,
size: 'md',
label: '学号',
id: 'u:8a6776c917cd',
mode: 'inline',
},
{
type: 'control',
id: 'u:fa7b6a89a71b',
label: '',
body: [
{
type: 'button',
level: 'primary',
actionType: 'submit',
label: '搜索',
id: 'u:544f3de716b9',
reload:
'userlist?code=like.*${keywords}*',
},
],
mode: 'inline',
},
],
id: 'u:e95d6e701a18',
},
name: 'userlist',
},
source: {
method: 'get',
url: 'rest/users?role=eq.student&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n label: `${item.code}-${item.name}`,\r\n value: item.id\r\n }\r\n })\r\n }\r\n}',
data: {
'&': '$$',
keywords: '__undefined',
},
},
modalMode: 'dialog',
multiple: true,
joinValues: false,
mode: 'normal',
label: '',
extractValue: true,
id: 'u:eff885fa85ae',
},
],
id: 'u:5352be84ebdb',
},
],
className: '',
id: 'u:7a1bff6ac12a',
},
],
},
{
type: 'input-rich-text',
label: '内容',
name: 'data',
mode: 'normal',
required: true,
inputClassName: 'm-t-sm',
options: {
menubar: false,
},
id: 'u:1ff80f85808d',
},
],
id: 'u:b2bb8e58c569',
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:d0a463529569',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'md',
id: 'u:50bb302aecfa',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:2a8cd9b988ff',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:2e2b83a9fcd0',
},
],
},
},
],
},
},
size: 'lg',
},
],
},
],
id: 'u:cd729a982c31',
};
export { schema };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,929 @@
const schema = {
type: 'page',
body: [
{
type: 'form',
id: 'u:90c6fa536042',
mode: 'inline',
name: 'filter',
title: '表单',
className: 'm-b-sm',
submitOnChange: true,
submitOnInit: true,
reload:
'group?course_id=$course_id&class_ids=$class_ids&studentgroup_ids=$studentgroup_ids',
wrapWithPanel: false,
body: [
{
type: 'group',
body: [
{
type: 'select',
mode: 'inline',
label: '课程',
name: 'course_id',
source: {
method: 'get',
url: 'rest/courses?organization_id=eq.$centre_id&semester_id=eq.$currentSemester&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: item.id\r\n }\r\n })\r\n}',
},
size: 'md',
searchable: true,
className: 'm-l-sm',
inputClassName: 'm-l-xs',
id: 'u:23277f1965f7',
},
],
id: 'u:8575c6f7cefc',
},
{
type: 'group',
body: [
{
type: 'tree-select',
label: '班级',
name: 'class_ids',
mode: 'horizontal',
multiple: true,
joinValues: false,
extractValue: true,
onlyChildren: true,
clearable: true,
searchable: true,
source: {
method: 'get',
url: 'rest/orgs?select=*,dicts:dicts!organization_type_fkey(*),course2orgs(*)&path=cd.root&course2orgs.course_id=eq.${course_id}&order=name',
adaptor:
"const recursive = id => {\r\n const nodes = payload.data.items.filter(item => {\r\n return item.parent === id && (\r\n (item.type === 'class' ||\r\n item.type === 'reelectclass') &&\r\n item.course2orgs.length ||\r\n item.type !== 'class' &&\r\n item.type !== 'reelectclass' &&\r\n item.type !== 'studentgroup'\r\n )\r\n })\r\n const result = []\r\n\r\n if (nodes.length) {\r\n nodes.forEach(item => {\r\n const children = recursive(item.id)\r\n\r\n if (children)\r\n result.push({\r\n ...item,\r\n label: item.name,\r\n children,\r\n })\r\n else if (\r\n item.type === 'class' ||\r\n item.type === 'reelectclass'\r\n ) result.push({\r\n ...item,\r\n label: item.name,\r\n value: item.id,\r\n })\r\n })\r\n\r\n if (result.length) return result\r\n } else return null\r\n}\r\n\r\nlet school = payload.data.items.find(x => x.type === 'school')\r\n\r\nreturn {\r\n ...payload,\r\n data: recursive(school.id)\r\n}\r\n",
requestAdaptor:
'if (api.query.course2orgs.course_id === "eq.") {\r\n \n api.url = api.url.replace("&course2orgs[course_id]=eq.", "")\r\n}\r\nif (api.query.course2orgs.course_id === "eq.undefined") {\r\n api.url = api.url.replace("&course2orgs[course_id]=eq.undefined", "")\r\n}',
sendOn: 'course_id',
messages: {},
},
withChildren: true,
initiallyOpen: false,
id: 'u:42ede681efa8',
autoCheckChildren: true,
enableNodePath: false,
showIcon: true,
heightAuto: true,
virtualThreshold: false,
},
],
id: 'u:f2885926e84d',
},
{
type: 'group',
body: [
{
type: 'tree-select',
label: '分组',
name: 'studentgroup_ids',
mode: 'horizontal',
multiple: true,
joinValues: false,
extractValue: true,
onlyChildren: true,
clearable: true,
searchable: true,
source: {
method: 'get',
url: 'rest/orgs?select=*,dicts:dicts!organization_type_fkey(*),course2orgs(*)&path=cd.root&course2orgs.course_id=eq.${course_id}&order=name',
adaptor:
"const recursive = id => {\r\n const nodes = payload.data.items.filter(item => {\r\n return item.parent === id && (\r\n item.type === 'studentgroup' &&\r\n item.course2orgs.length ||\r\n item.type !== 'class' &&\r\n item.type !== 'reelectclass' &&\r\n item.type !== 'studentgroup'\r\n )\r\n })\r\n const result = []\r\n\r\n if (nodes.length) {\r\n nodes.forEach(item => {\r\n const children = recursive(item.id)\r\n\r\n if (children)\r\n result.push({\r\n ...item,\r\n label: item.name,\r\n children,\r\n })\r\n else if (item.type === 'studentgroup')\r\n result.push({\r\n ...item,\r\n label: item.name,\r\n value: item.id,\r\n })\r\n })\r\n\r\n if (result.length) return result\r\n } else return null\r\n}\r\n\r\nlet school = payload.data.items.find(x => x.type === 'school')\r\n\r\nreturn {\r\n ...payload,\r\n data: recursive(school.id)\r\n}\r\n",
requestAdaptor:
'if (api.query.course2orgs.course_id === "eq.") {\r\n \n api.url = api.url.replace("&course2orgs[course_id]=eq.", "")\r\n}\r\nif (api.query.course2orgs.course_id === "eq.undefined") {\r\n api.url = api.url.replace("&course2orgs[course_id]=eq.undefined", "")\r\n}',
sendOn: 'course_id',
messages: {},
},
withChildren: true,
initiallyOpen: false,
id: 'u:42ede681efa8',
autoCheckChildren: true,
enableNodePath: false,
showIcon: true,
heightAuto: true,
virtualThreshold: false,
},
],
id: 'u:bfa2b4c7ed01',
},
],
feat: 'Insert',
},
{
type: 'divider',
lineStyle: 'solid',
id: 'u:e807ff502b0a',
},
{
type: 'crud',
messages: {},
api: {
method: 'get',
url: 'rest/groupusers?semester_id=eq.$currentSemester&course_id=eq.$course_id&or=(class_id.in.($class_ids),studentgroup_id.in.($studentgroup_ids))&order=student_code',
sendOn: 'this.course_id && (this.class_ids || this.studentgroup_ids)',
data: {
orderBy: '$orderBy',
orderDir: '$orderDir',
},
requestAdaptor:
"if (api.query.orderDir) {\r\n api.url = api.url.replace('&order=student_code', '');\r\n api.url = api.url.replace('&orderBy=' + api.query.orderBy + '&orderDir=' + api.query.orderDir, '&order=' + api.query.orderBy + '.' + api.query.orderDir);\r\n} else if (api.query.orderBy) {\r\n api.url = api.url.replace('&order=student_code', '');\r\n api.url = api.url.replace('&orderBy=' + api.query.orderBy + '&orderDir=', '&order=' + api.query.orderBy);\r\n} else {\r\n api.url = api.url.replace('&orderBy=&orderDir=', '');\r\n}\r\n\r\nreturn api;",
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map((item, index) => {\r\n return {\r\n ...item,\r\n n: index + 1\r\n }\r\n })\r\n }\r\n}',
},
headerToolbar: [
{
type: 'button',
align: 'right',
label: '导入分组',
icon: 'fa fa-upload',
level: 'warning',
id: 'u:ce034375ceff',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '分组导入',
size: 'xl',
body: [
{
type: 'form',
title: '表单',
body: [
{
type: 'select',
mode: 'inline',
label: '课程',
name: 'course_id',
source: {
method: 'get',
url: 'rest/courses?organization_id=eq.$centre_id&semester_id=eq.$currentSemester&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: item.id\r\n }\r\n })\r\n}',
},
size: 'md',
searchable: true,
className: 'm-l-sm',
inputClassName: 'm-l-xs',
id: 'u:7cfa89be2243',
},
{
type: 'tree-select',
label: '上级部门',
name: 'parent_id',
mode: 'inline',
extractValue: true,
onlyChildren: true,
source: {
method: 'get',
url: 'rest/orgs?select=*&or=(type.eq.school,type.eq.faculty,type.eq.centre)&path=cd.root&order=code',
adaptor:
"const recursive = id => {\r\n let nodes = payload.data.items.filter(x => {\r\n return x.parent === id\r\n })\r\n\r\n return nodes.map(item => {\r\n return {\r\n ...item,\r\n label: item.name,\r\n value: `${item.id}`,\r\n children: recursive(item.id)\r\n }\r\n })\r\n}\r\n\r\nlet school = payload.data.items.find(x => x.type === 'school')\r\n\r\nreturn {\r\n ...payload,\r\n data: recursive(school.id)\r\n}\r\n",
sendOn: '',
},
withChildren: true,
initiallyOpen: false,
searchable: true,
clearable: false,
heightAuto: true,
virtualThreshold: false,
id: 'u:37ca2401871c',
},
{
type: 'excel-import-group',
name: 'groups',
label: '',
mode: 'normal',
hiddenOn: '${!course_id || !parent_id}',
id: 'u:a8c9dbf3d073',
},
{
type: 'input-table',
name: 'groups',
label: '',
columns: [
{
label: 'Excel 行号',
name: 'no',
id: 'u:a31efd3393b0',
},
{
name: 'user_code',
label: '*学号',
classNameExpr:
"<%= data.no && !data.user_id ? 'bg-danger' : '' %>",
id: 'u:8d68435bf70e',
},
{
label: '姓名',
name: 'user_name',
id: 'u:fc9cb5a03b8f',
},
{
label: '分组',
name: 'group',
classNameExpr:
"<%= !data.no ? 'bg-secondary' : !data.group ? 'bg-danger' : '' %>",
id: 'u:ca487d89fd7c',
},
],
mode: 'normal',
visibleOn: 'this.groups',
strictMode: true,
removable: true,
editable: false,
clearValueOnHidden: true,
id: 'u:625db3dac0ec',
},
],
reload: 'grp',
initApi: {
method: 'get',
url: 'rest/report_check_items?organization_id=eq.$centre_id&order=type_order,type,display_order,comment',
},
id: 'u:ccd0d1410b18',
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'View',
},
],
closeOnEsc: true,
closeOnOutside: false,
showCloseButton: true,
actions: [],
id: 'u:d85ab95adb25',
},
},
],
},
},
},
{
type: 'export-excel',
align: 'right',
label: '导出',
icon: 'fa fa-download',
level: 'primary',
hiddenOn: '${!class_ids && !studentgroup_ids}',
api: {
method: 'get',
url: 'rest/groupusers?semester_id=eq.$currentSemester&course_id=eq.$course_id&or=(class_id.in.($class_ids),studentgroup_id.in.($studentgroup_ids))&order=student_code',
data: {
orderBy: '$orderBy',
orderDir: '$orderDir',
},
requestAdaptor:
"if (api.query.orderDir) {\r\n api.url = api.url.replace('&order=student_code', '');\r\n api.url = api.url.replace('&orderBy=' + api.query.orderBy + '&orderDir=' + api.query.orderDir, '&order=' + api.query.orderBy + '.' + api.query.orderDir);\r\n} else if (api.query.orderBy) {\r\n api.url = api.url.replace('&order=student_code', '');\r\n api.url = api.url.replace('&orderBy=' + api.query.orderBy + '&orderDir=', '&order=' + api.query.orderBy);\r\n} else {\r\n api.url = api.url.replace('&orderBy=&orderDir=', '');\r\n}\r\n\r\nreturn api;",
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map((item, index) => {\r\n return {\r\n ...item,\r\n n: index + 1\r\n }\r\n })\r\n }\r\n}',
},
filename: '分组',
exportColumns: [
{
label: '*学号',
name: 'student_code',
},
{
label: '姓名',
name: 'student_name',
},
{
label: '*分组',
name: 'studentgroup_name',
},
],
id: 'u:a44ab3164746',
},
{
type: 'bulk-actions',
},
],
bulkActions: [
{
label: '批量设置',
type: 'button',
id: 'u:99fa372537dc',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '批量设置',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'rest/rpc/batch_group',
data: {
org_id: '$org_id',
selectedItems: '$selectedItems',
},
heades: {
Prefer: 'params=single-object',
},
requestAdaptor:
'const user2org = api.data.selectedItems.map(item => ({\r\n user_id: item.student_id,\r\n old_org_id: item.studentgroup_id,\r\n new_org_id: api.data.org_id,\r\n}))\r\n\r\napi.data = { user2org }\r\n\r\nreturn api',
},
body: [
{
type: 'select',
label: '分组',
name: 'org_id',
mode: 'normal',
cascade: true,
searchable: true,
required: true,
source: {
method: 'get',
url: 'rest/orgs?select=*,course2orgs!inner(*),dicts:dicts!organization_type_fkey(*)&type=eq.studentgroup&path=cd.root.1&course2orgs.course_id=eq.${course_id}&order=name',
requestAdaptor:
'if (api.query.course2orgs.course_id === "eq.") {\r\n api.url = api.url.replace("&course2orgs[course_id]=eq.", "")\r\n}\r\nif (api.query.course2orgs.course_id === "eq.undefined") {\r\n api.url = api.url.replace("&course2orgs[course_id]=eq.undefined", "")\r\n}',
adaptor:
'return {\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: item.id\r\n }\r\n })\r\n}',
sendOn: 'course_id',
},
extractValue: true,
id: 'u:8527f8d399e6',
},
],
id: 'u:e9215f23110f',
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:148f700a4963',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
id: 'u:ce48989de1f9',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:f8c9f57fa665',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:8cccdee1e21b',
},
],
},
},
],
},
},
},
{
label: '批量清除',
confirmText: '确认清除分组吗?',
type: 'button',
id: 'u:5eb51d3e6692',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
outputVar: 'responseResult',
actionType: 'ajax',
options: {},
api: {
url: 'rest/user_orgs',
method: 'delete',
requestAdaptor:
'api.url = api.url + `?id=in.(${api.data.selectedItems.filter(item => item.user_studentgroup_id).map(item => item.user_studentgroup_id).toString()})`\r\ndelete api.data.selectedItems\r\n\r\nreturn api',
adaptor: '',
messages: {},
data: {
selectedItems: '$selectedItems',
},
},
},
{
componentId: 'u:148f700a4963',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
{
label: '批量分组',
type: 'button',
id: 'u:0371266b981c',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '批量分组',
body: [
{
type: 'form',
title: '表单',
id: 'u:f39e60c373bc',
body: [
{
type: 'tree-select',
label: '上级部门',
name: 'parent',
mode: 'normal',
extractValue: true,
onlyChildren: true,
source: {
method: 'get',
url: 'rest/orgs?select=*&or=(type.eq.school,type.eq.faculty,type.eq.centre)&path=cd.root&order=code',
adaptor:
"const recursive = id => {\r\n let nodes = payload.data.items.filter(x => {\r\n return x.parent === id\r\n })\r\n\r\n return nodes.map(item => {\r\n return {\r\n ...item,\r\n label: item.name,\r\n value: `${item.id}`,\r\n children: recursive(item.id)\r\n }\r\n })\r\n}\r\n\r\nlet school = payload.data.items.find(x => x.type === 'school')\r\n\r\nreturn {\r\n ...payload,\r\n data: recursive(school.id)\r\n}\r\n",
sendOn: '',
},
withChildren: true,
required: true,
initiallyOpen: false,
searchable: true,
clearable: false,
heightAuto: true,
virtualThreshold: false,
id: 'u:1efc22d916f7',
},
{
name: 'groups',
label: '新分组',
type: 'input-array',
items: {
type: 'input-text',
},
removable: true,
required: true,
mode: 'normal',
id: 'u:0a41abbda787',
},
{
label: '选择分组方式',
type: 'select',
name: 'way',
required: true,
mode: 'normal',
options: [
{
label: '顺序分组',
value: 'order',
},
{
label: '随机分组',
value: 'random',
},
],
id: 'u:436b4893fdfc',
},
{
type: 'batch-group',
label: false,
id: 'u:04fbf3b1b31c',
},
],
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:148f700a4963',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
actions: [
{
type: 'button',
label: '关闭',
onEvent: {
click: {
actions: [
{
componentId: 'u:148f700a4963',
actionType: 'reload',
data: null,
},
{
actionType: 'closeDialog',
},
],
},
},
id: 'u:f7831022039a',
},
],
id: 'u:3ead95907fbd',
},
},
],
},
},
},
],
syncLocation: false,
footerToolbar: [],
columns: [
{
type: 'text',
name: 'n',
label: '序号',
id: 'u:dc3b48ab2a2a',
},
{
type: 'text',
name: 'student_code',
sortable: true,
label: '学号',
id: 'u:9233f140d314',
},
{
type: 'text',
name: 'student_name',
sortable: true,
label: '姓名',
id: 'u:cb913014ed05',
},
{
type: 'text',
sortable: true,
label: '班级',
name: 'class_name',
id: 'u:d0de67b68472',
},
{
type: 'text',
label: '课程',
name: 'course_name',
id: 'u:ecd0d94b6c58',
},
{
type: 'text',
sortable: true,
label: '分组',
name: 'studentgroup_name',
id: 'u:de1610df96e6',
},
{
type: 'button-group',
label: '操作',
buttons: [
{
type: 'button',
level: 'link',
icon: 'fa fa-cog text-info',
size: 'md',
tooltip: '设置分组',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
hiddenOn: '${studentgroup_id}',
id: 'u:ccec6786dabf',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
title: '设置分组',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'rest/user_orgs',
data: {
user_id: '$student_id',
organization_id: '$group_id',
},
dataType: 'json',
},
id: 'u:a20070f8b13c',
body: [
{
type: 'select',
label: '分组',
name: 'group_id',
mode: 'normal',
cascade: true,
searchable: true,
required: true,
source: {
method: 'get',
url: 'rest/orgs?select=*,course2orgs!inner(*),dicts:dicts!organization_type_fkey(*)&type=eq.studentgroup&path=cd.root.1&course2orgs.course_id=eq.${course_id}&order=name',
requestAdaptor:
'if (api.query.course2orgs.course_id === "eq.") {\r\n api.url = api.url.replace("&course2orgs[course_id]=eq.", "")\r\n}\r\nif (api.query.course2orgs.course_id === "eq.undefined") {\r\n api.url = api.url.replace("&course2orgs[course_id]=eq.undefined", "")\r\n}',
adaptor:
'return {\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: item.id\r\n }\r\n })\r\n}',
sendOn: 'course_id',
},
extractValue: true,
id: 'u:75a5e1ef2a35',
},
],
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:148f700a4963',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
type: 'dialog',
closeOnEsc: true,
showCloseButton: true,
id: 'u:05921ba2d106',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:5a3bed9137d5',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:9982eb0126f2',
},
],
},
},
],
},
},
},
{
type: 'button',
level: 'link',
icon: 'fa fa-pencil text-warning',
size: 'md',
tooltip: '修改分组',
tooltipPlacement: 'top',
hiddenOn: '${!studentgroup_id}',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
id: 'u:b1bda5ad847a',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
title: '修改分组',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'patch',
url: 'rest/user_orgs?user_id=eq.$student_id&organization_id=eq.$studentgroup_id',
data: {
organization_id: '$group_id',
},
dataType: 'json',
},
id: 'u:2b0b7a8f5877',
body: [
{
type: 'tree-select',
label: '原分组',
name: 'studentgroup_id',
searchable: true,
readOnly: true,
onlyChildren: true,
source: {
method: 'get',
url: 'rest/orgs?select=*&or=(type.eq.school,type.eq.faculty,type.eq.centre,type.eq.studentgroup)&path=cd.root&order=code',
adaptor:
"const recursive = id => {\r\n let nodes = payload.data.items.filter(x => {\r\n return x.parent === id\r\n })\r\n\r\n return nodes.map(item => {\r\n return {\r\n ...item,\r\n label: item.name,\r\n value: item.id,\r\n children: recursive(item.id)\r\n }\r\n })\r\n}\r\n\r\nlet school = payload.data.items.find(x => x.type === 'school')\r\n\r\nreturn {\r\n ...payload,\r\n data: recursive(school.id)\r\n}\r\n",
},
withChildren: true,
initiallyOpen: false,
heightAuto: true,
virtualThreshold: false,
id: 'u:9b803b7fffbe',
},
{
type: 'select',
label: '新分组',
name: 'group_id',
mode: 'normal',
cascade: true,
searchable: true,
required: true,
source: {
method: 'get',
url: 'rest/orgs?select=*,course2orgs!inner(*),dicts:dicts!organization_type_fkey(*)&type=eq.studentgroup&path=cd.root.1&course2orgs.course_id=eq.${course_id}&order=name',
requestAdaptor:
'if (api.query.course2orgs.course_id === "eq.") {\r\n api.url = api.url.replace("&course2orgs[course_id]=eq.", "")\r\n}\r\nif (api.query.course2orgs.course_id === "eq.undefined") {\r\n api.url = api.url.replace("&course2orgs[course_id]=eq.undefined", "")\r\n}',
adaptor:
'return {\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: item.id\r\n }\r\n })\r\n}',
sendOn: 'course_id',
},
extractValue: true,
id: 'u:fd14fbccb8fc',
},
],
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:148f700a4963',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
type: 'dialog',
closeOnEsc: true,
showCloseButton: true,
id: 'u:94b2f1f09462',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:0c74d636f491',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:2e60b730d920',
},
],
},
},
],
},
},
},
{
type: 'button',
confirmText: '确认清除分组吗?',
level: 'link',
icon: 'fa fa-trash text-danger',
size: 'md',
tooltip: '清除分组',
tooltipPlacement: 'top',
hiddenOn: '${!studentgroup_id}',
iconClassName: 'pull-left',
className: 'p-r-none p-l-none',
id: 'u:e719d85aa542',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
outputVar: 'responseResult',
actionType: 'ajax',
options: {},
api: {
url: 'rest/user_orgs?user_id=eq.$student_id&organization_id=eq.$studentgroup_id',
method: 'delete',
requestAdaptor: '',
adaptor: '',
messages: {},
},
},
{
componentId: 'u:148f700a4963',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
placeholder: '-',
width: 200,
id: 'u:b59ecbce78a6',
},
],
name: 'grp',
bodyClassName: '',
title: '',
initFetch: true,
columnsTogglable: true,
id: 'u:148f700a4963',
},
],
title: '',
messages: {},
id: 'u:9e76719da6ac',
};
export { schema };

View File

@@ -0,0 +1,525 @@
const schema = {
type: 'page',
body: [
{
type: 'crud',
messages: {},
api: {
method: 'get',
url: 'rest/projects?select=*&organization_id=eq.${centre_id}&semester_id=eq.${currentSemester}&order=code',
data: {
page: '$page',
perPage: '$perPage',
},
},
headerToolbar: [
{
type: 'button',
id: 'u:b88c442cf1c9',
align: 'right',
label: '添加',
icon: 'fa fa-plus',
level: 'default',
className: 'm-l-xs',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '添加实验',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'rest/projects',
data: {
code: '${project_code}',
name: '${project_name}',
intro: '${project_intro}',
organization_id: '${centre_id}',
type: '${type}',
free_schedule: '${free_schedule}',
},
dataType: 'json',
requestAdaptor:
'if(api.data.type===\'examination\') {\r\n api.data.score_item=[{"item": "exp_preparation", "label": "实验预习", "weight": 1}];\r\n}\r\nreturn api;',
},
body: [
{
type: 'input-text',
label: '编号',
name: 'project_code',
value: '',
required: true,
mode: 'normal',
id: 'u:4a4471a8f6fd',
},
{
label: '名称',
type: 'input-text',
name: 'project_name',
value: '',
required: true,
mode: 'normal',
id: 'u:5d11604bb560',
},
{
label: '类型',
type: 'select',
name: 'type',
required: true,
mode: 'normal',
checkAll: false,
source: {
method: 'get',
url: 'rest/dicts?select=dictkey,dictvalue&typecode=eq.012',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n label: item.dictvalue,\r\n value: item.dictkey\r\n }\r\n })\r\n }\r\n}',
},
value: 'experiment',
options: [
{
label: '实验课',
value: 'experiment',
},
],
id: 'u:4f93120dc92a',
},
{
label: '学生自主安排时间',
type: 'checkbox',
name: 'free_schedule',
mode: 'inline',
value: false,
id: 'u:d8de66ea2a6b',
},
{
label: '描述',
type: 'input-rich-text',
name: 'project_intro',
mode: 'normal',
options: {
menubar: false,
},
id: 'u:41a4ab0a0f14',
},
],
id: 'u:87d3612f0e3f',
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:faff85507083',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
id: 'u:d6cc5749dac1',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:25b4e52d9f7a',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:6f9f855502d6',
},
],
},
},
],
},
},
},
{
type: 'export-excel',
align: 'right',
label: '实验导出',
icon: 'fa fa-download',
level: 'primary',
id: 'u:d57d6b7f1525',
api: 'rest/projects?select=*,dicts:dicts!project_type_fk(dictkey,dictvalue)&organization_id=eq.$centre_id&semester_id=eq.$currentSemester&order=code',
filename: '实验',
exportColumns: [
{
label: '*编号',
name: 'code',
},
{
name: 'name',
label: '*名称',
},
{
name: 'dicts.dictvalue',
label: '*类型',
},
],
},
],
syncLocation: false,
perPageAvailable: [10, 20, 30, 40, 50],
footerToolbar: [
{
type: 'pagination',
},
{
type: 'switch-per-page',
},
{
type: 'statistics',
},
],
perPageField: 'perPage',
pageField: 'page',
mode: 'table',
affixHeader: true,
id: 'u:faff85507083',
name: 'score',
columns: [
{
name: 'code',
type: 'text',
label: '编号',
id: 'u:bad42a88a366',
},
{
type: 'text',
name: 'name',
label: '名称',
id: 'u:d9f55c020ac5',
},
{
type: 'tpl',
label: '评分项',
name: 'score_item',
tpl: "<% if (this.score_item.length > 0) {\n this.score_item.forEach(score => { %>\n <% if (score.item === 'exp_report') { %>\n <span class='label label-info' style='margin-right: 1px; font-size: smaller;'> <%= score.label %> (<%= score.weight %>)\n <% if (score.isRecorded) { %>\n <i class='fa fa-pencil'></i>\n <% } %>\n </span>\n <% } else if (score.item === 'exp_data') { %>\n <span class='label label-success' style='margin-right: 1px; font-size: smaller;'> <%= score.label %> (<%= score.weight %>)\n <% if (score.isRecorded) { %>\n <i class='fa fa-pencil'></i>\n <% } %>\n </span>\n <% } else if (score.item === 'exp_preparation') { %>\n <span class='label label-danger' style='margin-right: 1px; font-size: smaller;'> <%= score.label %> (<%= score.weight %>)\n <% if (score.isRecorded) { %>\n <i class='fa fa-pencil'></i>\n <% } %>\n </span>\n <% } else if (score.item === 'exp_operation') { %>\n <span class='label label-warning' style='margin-right: 1px; font-size: smaller;'> <%= score.label %> (<%= score.weight %>)\n <% if (score.isRecorded) { %>\n <i class='fa fa-pencil'></i>\n <% } %>\n </span>\n <% } else if (score.item === 'exp_notebook') { %>\n <span class='label' style='margin-right: 1px; background: purple; font-size: smaller;'> <%= score.label %> (<%= score.weight %>)\n <% if (score.isRecorded) { %>\n <i class='fa fa-pencil'></i>\n <% } %>\n </span>\n <% } else if (score.item === 'exp_process_assessment') { %>\n <span class='label' style='margin-right: 1px; background: orange; font-size: smaller;'> <%= score.label %> (<%= score.weight %>)\n <% if (score.isRecorded) { %>\n <i class='fa fa-pencil'></i>\n <% } %>\n </span>\n <% } else if (score.item === 'exp_team') { %>\n <span class='label' style='margin-right: 1px; background: pink; font-size: smaller;'> <%= score.label %> (<%= score.weight %>)\n <% if (score.isRecorded) { %>\n <i class='fa fa-pencil'></i>\n <% } %>\n </span>\n <% } else { %>\n <span class='label' style=\"margin-right: 1px; background: #374151; font-size: smaller;\"> <%= score.label %> (<%= score.weight %>)\n <% if (score.isRecorded) { %>\n <i class='fa fa-pencil'></i>\n <% } %>\n </span>\n <% } %>\n <% }) %>\n <% } %>",
inline: false,
id: 'u:6fb5baeecfab',
},
{
type: 'operation',
label: '操作',
buttons: [
{
type: 'button',
level: 'link',
size: 'md',
icon: 'fa fa-pencil text-info',
tooltip: '修改',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'm-r-none m-l-none',
id: 'u:74e446e57e2b',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '修改实验',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'patch',
url: 'rest/projects?id=eq.${id}',
data: {
code: '${code}',
name: '${name}',
intro: '${intro}',
type: '${type}',
free_schedule: '${free_schedule}',
},
dataType: 'json',
},
body: [
{
type: 'input-text',
label: '编号',
name: 'code',
value: '',
required: true,
mode: 'normal',
id: 'u:45350664b507',
},
{
label: '名称',
type: 'input-text',
name: 'name',
value: '',
required: true,
mode: 'normal',
id: 'u:8e70cec48464',
},
{
label: '类型',
type: 'select',
name: 'type',
required: true,
mode: 'normal',
options: [],
checkAll: false,
source: {
method: 'get',
url: 'rest/dicts?select=dictkey,dictvalue&typecode=eq.012',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n label: item.dictvalue,\r\n value: item.dictkey\r\n }\r\n })\r\n }\r\n}',
},
id: 'u:425ae91adc74',
},
{
label: '学生自主安排时间',
type: 'checkbox',
name: 'free_schedule',
mode: 'inline',
id: 'u:b43e9dc2a55d',
},
{
label: '描述',
type: 'input-rich-text',
name: 'intro',
value: '',
mode: 'normal',
id: 'u:84e41b04cf8c',
},
],
id: 'u:3c25e724c688',
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:faff85507083',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
closeOnEsc: true,
showCloseButton: true,
id: 'u:98e4cae66fe9',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:8dddacfd03ef',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:9a7c3432ab97',
},
],
},
},
],
},
},
},
{
type: 'button',
confirmText: '确认删除"${name}(${code})"',
level: 'link',
icon: 'fa fa-times text-danger',
size: 'md',
tooltip: '删除',
tooltipPlacement: 'top',
iconClassName: 'pull-left',
className: 'm-r-none m-l-none',
id: 'u:58e504e36911',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
outputVar: 'responseResult',
actionType: 'ajax',
options: {},
api: {
url: 'rest/projects/$id',
method: 'delete',
requestAdaptor: '',
adaptor: '',
messages: {},
},
},
{
componentId: 'u:faff85507083',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
{
type: 'button',
icon: 'fa fa-cog text-warning',
level: 'link',
messages: {},
title: '报告模板',
size: 'md',
confirmText: '',
tooltip: '修改实验评分项',
tooltipPlacement: 'top',
className: 'm-r-none m-l-none',
iconClassName: 'pull-left',
id: 'u:a5fd11fac042',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
title: '修改实验评分项',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'patch',
url: 'rest/projects/${id}',
data: {
score_item: '$score_item',
},
requestAdaptor: '',
dataType: 'json',
},
body: [
{
type: 'projectscoreitem',
name: 'score_item',
api: {
url: 'rest/dicts?typecode=eq.007',
},
mode: 'normal',
id: 'u:e3a860ff8f80',
},
],
id: 'u:3510789a5776',
actions: [
{
type: 'submit',
label: '提交',
primary: true,
},
],
feat: 'Insert',
dsType: 'api',
onEvent: {
submitSucc: {
weight: 0,
actions: [
{
componentId: 'u:faff85507083',
ignoreError: false,
actionType: 'reload',
args: {
resetPage: false,
},
},
],
},
},
},
],
type: 'dialog',
closeOnEsc: true,
closeOnOutside: false,
showCloseButton: true,
size: 'md',
id: 'u:34a7904cefc0',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:6fd9b88723bf',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:457223ebeaee',
},
],
},
},
],
},
},
},
],
placeholder: '-',
id: 'u:33cc39b76ad4',
},
],
bodyClassName: '',
initApi: '',
initFetch: '',
label: '名称',
},
],
id: 'u:b7c1c11acda6',
};
export { schema };

View File

@@ -0,0 +1,69 @@
const schema = {
type: 'page',
body: [
{
type: 'form',
title: '',
body: [
{
type: 'input-text',
label: '数字化教材名称',
mode: 'horizontal',
required: true,
name: 'name',
id: 'u:1e67e22855ce',
},
{
type: 'combo',
label: false,
name: 'cells',
required: true,
multiple: true,
addable: true,
removable: true,
removableMode: 'icon',
addBtn: {
label: '新增',
level: 'primary',
size: 'sm',
id: 'u:3300178a20b6',
},
items: [
{
type: 'vditor',
name: 'source',
id: 'u:48cbfec9c64d',
},
],
id: 'u:1e67e22855be',
},
],
id: 'u:ccd0d1410b18',
feat: 'View',
api: {
method: 'post',
url: 'rest/test',
data: {
cells: '$cells',
},
},
actions: [
{
type: 'textbook-submit',
label: '提交',
},
],
wrapWithPanel: true,
dsType: 'api',
},
],
title: '',
id: 'u:9e76719da6ac',
messages: {},
asideResizor: false,
pullRefresh: {
disabled: true,
},
};
export { schema };

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
const schema = {
type: 'page',
body: [
{
type: 'button',
label: '使用说明',
onEvent: {
click: {
actions: [
{
args: {
url: 'https://www.platosoft.org/doc/olms/plugin/mark.html',
},
actionType: 'url',
},
],
weight: 0,
},
},
size: 'md',
level: 'primary',
block: false,
},
{
type: 'button',
label: '进入批阅',
onEvent: {
click: {
actions: [
{
args: {
url: '/check',
},
actionType: 'url',
},
],
weight: 0,
},
},
level: 'primary',
blank: true,
className: 'm-l',
},
],
title: '报告上传与在线批阅',
messages: {},
};
export { schema };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,513 @@
const schema = {
type: 'page',
body: [
{
type: 'scheduler-renderer',
name: 'scheduler',
body: [],
plan: {
type: 'button',
label: '按钮',
actionType: 'drawer',
dialog: { title: '系统提示', body: '对你点击了' },
drawer: {
type: 'drawer',
title: '弹框标题',
body: [
{ type: 'tpl', tpl: '<p>对,你刚刚点击了</p>', inline: false },
],
},
},
planlistbutton: {
type: 'button',
label: '',
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '排课计划',
body: [
{
type: 'service',
body: [
{
type: 'button',
label: '添加计划',
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '添加计划',
body: [
{
type: 'wizard',
reload: 'plancrud',
steps: [
{
title: '计划设置',
mode: 'normal',
controls: [
{
type: 'text',
label: '计划名称',
name: 'name',
mode: 'normal',
required: true,
},
{
type: 'date-range',
label: '日期范围',
name: 'date',
required: true,
format: 'YYYY-MM-DD',
minDate: '${semester.since}',
maxDate: '${semester.to}',
},
{
type: 'number',
label: '老师每周最多上课数量',
name: 'teacherFreqWeekly',
required: true,
mode: 'normal',
min: '1',
step: 1,
value: 10,
},
{
type: 'number',
label: '老师每天最多上课数量',
name: 'teacherFreqDaily',
mode: 'normal',
required: true,
min: '1',
step: 1,
value: 1,
},
{
type: 'number',
label: '老师同时上课数量',
name: 'maxConcurrentOfTeacher',
mode: 'normal',
required: true,
min: '1',
step: 1,
value: 1,
},
{
type: 'number',
name: 'maxConcurrent',
mode: 'normal',
label: '同一时段最多排课数量',
required: true,
value: 4,
min: '1',
step: 1,
},
{
type: 'number',
name: 'maxFreqWeekly',
mode: 'normal',
label: '学生每周上课数',
required: true,
value: 1,
min: '1',
step: 1,
},
{
type: 'switch',
name: 'fixTeacherMode',
mode: 'normal',
label: '',
required: true,
value: false,
option: ' 教师跟班模式',
optionAtLeft: false,
trueValue: true,
falseValue: false,
},
],
},
{
title: '资源设置',
controls: [
{
type: 'course-list',
label: '课程',
name: 'courselist',
source: {
method: 'get',
url: 'rest/courses?select=id,name,course2projects(projects(id,name,validRoomList,preSubjectList)),course2orgs(orgs(id,name, validTimeslotList:timeslot_class_id_fkey(id,date,period:periods(id,name,startTime:start_time,endTime:end_time))))&organization_id=eq.${centre_id}&semester_id=eq.${currentSemester}&order=code',
},
required: true,
multiple: true,
},
],
mode: 'normal',
},
{
mode: 'normal',
title: '工作量设置',
controls: [
{
type: 'workload',
name: 'workload',
source:
'rest/users?select=id,name:name,role,subjectList,studentGroupList,user_orgs(organization_id),timeslotList:timeslot_teacher_id_fkey(id,date,period:periods(id,name,startTime:start_time,endTime:end_time)),secondChoiceTimeslotList:timeslot_teacher_secondary_id_fkey(id,date,period:periods(id,name,startTime:start_time,endTime:end_time))&role=eq.teacher&user_orgs.organization_id=eq.${centre_id}&order=code',
},
],
},
],
mode: 'horizontal',
api: {
method: 'post',
url: 'rest/schedule_plans',
requestAdaptor:
"return {\r\n ...api,\r\n data: {\r\n name: api.data.name,\r\n date: api.data.date,\r\n status: '待求解',\r\n organization_id: api.data.organization_id,\r\n problem: {\r\n problemConfig: {\r\n fixTeacherMode: api.data.fixTeacherMode,\r\n maxConcurrent: api.data.maxConcurrent,\r\n maxConcurrentOfTeacher: api.data.maxConcurrentOfTeacher,\r\n maxFreqWeekly: api.data.maxFreqWeekly,\r\n teacherFreqWeekly: api.data.teacherFreqWeekly,\r\n teacherFreqDaily: api.data.teacherFreqDaily,\r\n excludeRoomTime: api.data.excludeRoomTime,\r\n excludeTeacherStudentGroup: api.data.excludeTeacherStudentGroup,\r\n excludeTeacherSubject: api.data.excludeTeacherSubject,\r\n excludeTeacherTime: api.data.excludeTeacherTime,\r\n },\r\n courseList: api.data.courselist.map((item) => {\r\n return {\r\n id: item.id,\r\n name: item.name,\r\n subjectList: item.subjectList.map((e) => {\r\n return {\r\n id: e.id,\r\n name: e.name,\r\n abbreviation: item.name,\r\n validRoomList: e.validRoomList,\r\n preSubjectList: e.preSubjectList\r\n };\r\n }),\r\n studentGroupList: item.studentGroupList.map((e) => {\r\n return {\r\n id: e.id,\r\n name: e.name,\r\n validTimeslotList: e.validTimeslotList,\r\n };\r\n }),\r\n };\r\n }),\r\n validTeacherList: api.data.workload.filter(item => item.workload > 0).map((item) => {\r\n return {\r\n id: item.id,\r\n name: item.name,\r\n workload: item.workload,\r\n timeslotList: item.timeslotList,\r\n secondChoiceTimeslotList: item.secondChoiceTimeslotList,\r\n subjectList: item.subjectList,\r\n studentGroupList: item.studentGroupList,\r\n };\r\n }),\r\n },\r\n },\r\n};\r\n",
data: {
'&': '$$',
organization_id: '${centre_id}',
excludeRoomTime: '${excludeRoomTime}',
excludeTeacherStudentGroup:
'${excludeTeacherStudentGroup}',
excludeTeacherSubject: '${excludeTeacherSubject}',
excludeTeacherTime: '${excludeTeacherTime}',
},
},
name: 'stepper',
actionNextSaveLabel: '下一步',
},
],
closeOnEsc: true,
showCloseButton: true,
actions: [],
data: {
currentSemester: '${currentSemester}',
centre_id: '${centre_id}',
semester: '${semester}',
excludeRoomTime: '${excludeRoomTime}',
excludeTeacherStudentGroup:
'${excludeTeacherStudentGroup}',
excludeTeacherSubject: '${excludeTeacherSubject}',
excludeTeacherTime: '${excludeTeacherTime}',
},
},
className: 'm-b-sm ',
block: false,
},
{
type: 'crud',
api: {
method: 'get',
url: 'rest/schedule_plans?semester_id=eq.${currentSemester}&organization_id=eq.${centre_id}&order=updated_at.desc',
data: { '&': '$$', page: '${page}', perPage: '${perPage}' },
},
columns: [
{ name: 'name', label: '排课计划', type: 'text' },
{ type: 'text', label: '状态', name: 'status' },
{
type: 'operation',
label: '操作',
buttons: [
{
label: '选择',
type: 'button',
visibleOn: '!this.is_current',
actionType: 'ajax',
confirmText: '确认切换排课计划?',
className: '',
api: {
method: 'get',
url: 'rest/schedule_plans/${id}',
},
size: 'sm',
level: 'primary',
reload: 'scheduler?plan=${id}',
},
{
type: 'button',
visibleOn: '!this.is_current',
label: '编辑',
actionType: 'dialog',
dialog: {
title: '修改计划',
body: [
{
type: 'wizard',
reload: 'plancrud',
api: {
method: 'patch',
url: 'rest/schedule_plans?id=eq.${id}',
requestAdaptor:
"return {\r\n ...api,\r\n data: {\r\n name: api.data.name,\r\n date: api.data.date,\r\n status: '待求解',\r\n organization_id: api.data.organization_id,\r\n problem: {\r\n problemConfig: {\r\n fixTeacherMode: api.data.fixTeacherMode,\r\n maxConcurrent: api.data.maxConcurrent,\r\n maxConcurrentOfTeacher: api.data.maxConcurrentOfTeacher,\r\n maxFreqWeekly: api.data.maxFreqWeekly,\r\n teacherFreqWeekly: api.data.teacherFreqWeekly,\r\n teacherFreqDaily: api.data.teacherFreqDaily,\r\n excludeRoomTime: api.data.excludeRoomTime,\r\n excludeTeacherStudentGroup: api.data.excludeTeacherStudentGroup,\r\n excludeTeacherSubject: api.data.excludeTeacherSubject,\r\n excludeTeacherTime: api.data.excludeTeacherTime,\r\n },\r\n courseList: api.data.courselist.map((item) => {\r\n return {\r\n id: item.id,\r\n name: item.name,\r\n subjectList: item.subjectList.map((e) => {\r\n return {\r\n id: e.id,\r\n name: e.name,\r\n abbreviation: item.name,\r\n validRoomList: e.validRoomList,\r\n preSubjectList: e.preSubjectList\r\n };\r\n }),\r\n studentGroupList: item.studentGroupList.map((e) => {\r\n return {\r\n id: e.id,\r\n name: e.name,\r\n validTimeslotList: e.validTimeslotList\r\n\r\n };\r\n }),\r\n };\r\n }),\r\n validTeacherList: api.data.workload.filter(item => item.workload > 0).map((item) => {\r\n return {\r\n id: item.id,\r\n name: item.name,\r\n workload: item.workload,\r\n timeslotList: item.timeslotList,\r\n secondChoiceTimeslotList: item.secondChoiceTimeslotList,\r\n subjectList: item.subjectList,\r\n studentGroupList: item.studentGroupList,\r\n };\r\n }),\r\n },\r\n },\r\n};\r\n",
data: {
'&': '$$',
organization_id: '${centre_id}',
excludeRoomTime: '${excludeRoomTime}',
excludeTeacherStudentGroup:
'${excludeTeacherStudentGroup}',
excludeTeacherSubject:
'${excludeTeacherSubject}',
excludeTeacherTime: '${excludeTeacherTime}',
},
},
steps: [
{
title: '计划设置',
mode: 'normal',
controls: [
{
type: 'text',
label: '计划名称',
name: 'name',
mode: 'normal',
required: true,
},
{
type: 'date-range',
label: '日期范围',
name: 'date',
required: true,
format: 'YYYY-MM-DD',
minDate: '${semester.since}',
maxDate: '${semester.to}',
},
{
type: 'number',
label: '老师每周最多上课数量',
name: 'teacherFreqWeekly',
required: true,
mode: 'normal',
min: '1',
step: 1,
value: 10,
},
{
type: 'number',
label: '老师每天最多上课数量',
name: 'teacherFreqDaily',
mode: 'normal',
required: true,
min: '1',
step: 1,
value: 1,
},
{
type: 'number',
label: '老师同时上课数量',
name: 'maxConcurrentOfTeacher',
mode: 'normal',
required: true,
min: '1',
step: 1,
value: 1,
},
{
type: 'number',
name: 'maxConcurrent',
mode: 'normal',
label: '同一时段最多排课数量',
required: true,
value: 4,
min: '1',
step: 1,
},
{
type: 'number',
name: 'maxFreqWeekly',
mode: 'normal',
label: '学生每周上课数',
required: true,
value: 1,
min: '1',
step: 1,
},
{
type: 'switch',
name: 'fixTeacherMode',
mode: 'normal',
label: '',
required: true,
value: false,
option: ' 教师跟班模式',
optionAtLeft: false,
trueValue: true,
falseValue: false,
},
],
},
{
title: '资源设置',
controls: [
{
type: 'course-list',
label: '课程',
name: 'courselist',
source: {
method: 'get',
url: 'rest/courses?select=id,name,course2projects(projects(id,name,validRoomList,preSubjectList)),course2orgs(orgs(id,name, validTimeslotList:timeslot_class_id_fkey(id,date,period:periods(id,name,startTime:start_time,endTime:end_time))))&organization_id=eq.${centre_id}&semester_id=eq.${currentSemester}&order=code',
},
required: true,
multiple: true,
},
],
mode: 'normal',
},
{
mode: 'normal',
title: '工作量设置',
controls: [
{
type: 'workload',
name: 'workload',
source:
'rest/users?select=id,name:name,role,subjectList,studentGroupList,user_orgs(organization_id),timeslotList:timeslot_teacher_id_fkey(id,date,period:periods(id,name,startTime:start_time,endTime:end_time)),secondChoiceTimeslotList:timeslot_teacher_secondary_id_fkey(id,date,period:periods(id,name,startTime:start_time,endTime:end_time))&role=eq.teacher&user_orgs.organization_id=eq.${centre_id}&order=code',
},
],
},
],
mode: 'horizontal',
name: 'stepper',
actionNextSaveLabel: '下一步',
initApi: {
method: 'get',
url: 'rest/schedule_plans?id=eq.${id}',
adaptor:
'const data = payload.data.items[0]\r\nreturn {\r\n data: {\r\n ...data.problem,\r\n ...data.problem.problemConfig,\r\n courselist: data.problem.courseList,\r\n workload: data.problem.validTeacherList,\r\n id: data.id,\r\n name: data.name,\r\n date: data.date\r\n }\r\n}',
},
},
],
type: 'dialog',
closeOnEsc: true,
showCloseButton: true,
actions: [],
data: {
'&': '$$',
name: '${name}',
maxConcurrent:
'${config.problemConfig.maxConcurrent}',
teacherFreqDaily:
'${config.problemConfig.teacherFreqDaily}',
date: '${config.date}',
maxConcurrentOfTeacher:
'${config.problemConfig.maxConcurrentOfTeacher}',
maxFreqWeekly:
'${config.problemConfig.maxFreqWeekly}',
fixTeacherMode:
'${config.problemConfig.fixTeacherMode}',
courselist: '${config.courseList}',
workload: '${config.validTeacherList}',
centre_id: '${centre_id}',
id: '${id}',
currentSemester: '${currentSemester}',
semester: '${semester}',
teacherFreqWeekly:
'${config.problemConfig.teacherFreqWeekly}',
},
},
size: 'sm',
icon: 'fa fa-pencil',
tooltip: '修改计划配置',
tooltipPlacement: 'bottom',
className: 'no-border',
},
{
type: 'button',
visibleOn: '!this.is_current',
label: '删除',
actionType: 'ajax',
size: 'sm',
confirmText: '确认删除该计划?',
api: {
method: 'delete',
url: 'rest/schedule_plans?id=eq.${id}',
},
reload: 'plancrud',
level: 'danger',
icon: 'fa fa-trash',
},
{
type: 'tpl',
tpl: '当前计划',
inline: true,
visibleOn: 'this.is_current',
},
],
},
],
messages: {},
name: 'plancrud',
syncLocation: false,
defaultParams: {},
filter: null,
headerToolbar: [{ type: 'bulk-actions' }],
perPageAvailable: '',
},
],
messages: {},
api: {
method: 'get',
url: 'rest/config?key=eq.system',
adaptor:
'let config = payload.data.items && payload.data.items.length > 0 ? payload.data.items[0].value : null\r\nreturn {\r\n data: {\r\n excludeRoomTime: config ? config.excludeRoomTime : true,\r\n excludeTeacherStudentGroup: config ? config.excludeTeacherStudentGroup : true,\r\n excludeTeacherSubject: config ? config.excludeTeacherSubject : true,\r\n excludeTeacherTime: config ? config.excludeTeacherTime : true,\r\n }\r\n}',
},
},
],
closeOnEsc: true,
showCloseButton: true,
actions: [],
},
size: 'xs',
icon: 'fa fa-list',
tooltip: '查看计划列表',
tooltipPlacement: 'bottom',
className: 'no-border',
},
planstartbutton: {
type: 'button',
actionType: 'ajax',
reload: 'window',
target: '',
blank: true,
api: {
method: 'post',
url: 'rest/timetable/${id}/solve',
dataType: 'json',
},
},
planstopbutton: {
type: 'button',
actionType: 'ajax',
reload: 'window',
target: '',
confirmText: '确认停止计算?',
blank: true,
api: {
method: 'post',
url: 'rest/timetable/${id}/stopSolving',
dataType: 'json',
},
},
plansavebutton: {
type: 'button',
actionType: 'ajax',
reload: '',
target: '',
confirmText: '确认生效该计划安排的场次?',
blank: true,
api: {
method: 'post',
url: 'rest/schedules',
dataType: 'json',
requestAdaptor:
'return {\r\n ...api,\r\n data: api.data.schedules\r\n}',
},
},
},
],
title: '',
messages: {},
bodyClassName: 'p-none flex',
className: 'bg-white h-full',
toolbar: [],
aside: [],
};
export { schema };

View File

@@ -0,0 +1,280 @@
const schema = {
type: 'page',
body: [
{
type: 'form',
submitOnChange: true,
reload: 'class_set?id=$orgSelect&org=$org',
wrapWithPanel: false,
body: [
{
type: 'tree-select',
name: 'orgSelect',
label: '查询',
mode: 'inline',
size: 'lg',
clearable: true,
searchable: true,
source: {
method: 'get',
url: 'rest/orgs?select=*,course2orgs(*,courses(*))&path=cd.root&course2orgs.courses.semester_id=eq.${currentSemester}&type=in.(school,centre,faculty)&order=name,code',
adaptor:
"const recursive = id => {\r\n let nodes = payload.data.items.filter(x => {\r\n return x.parent === id\r\n })\r\n return nodes.map(item => {\r\n return {\r\n ...item,\r\n label: item.name,\r\n value: `${item.id}`,\r\n children: recursive(item.id)\r\n }\r\n })\r\n}\r\nlet school = payload.data.items.find(x => x.type === 'school');\r\nreturn {\r\n ...payload,\r\n data: [{\r\n label: school.name,\r\n value: `${school.id}`,\r\n children: recursive(school.id)\r\n }]\r\n}\r\n",
},
inputClassName: 'm-l-xs',
options: [],
initiallyOpen: false,
unfoldedLevel: 2,
multiple: true,
joinValues: true,
onlyChildren: true,
withChildren: true,
extractValue: false,
heightAuto: true,
virtualThreshold: false,
id: 'u:3224d44c5c5d',
},
{
type: 'input-text',
label: '组织或组织编号',
placeholder: '',
name: 'org',
size: 'md',
clearable: true,
inputClassName: 'm-l-xs',
id: 'u:89966cee9c0b',
},
],
mode: 'inline',
debug: false,
id: 'u:254f488290d5',
feat: 'Insert',
},
{ type: 'divider', lineStyle: 'solid', id: 'u:c6f2f106da08' },
{
type: 'crud',
api: {
method: 'get',
url: 'rest/orgs?select=*,course2orgs!inner(*,courses!inner(*))&or=(name.like.*${org}*,code.like.*${org}*)&path=cd.root&type=in.(class,studentgroup,reelecclass)&course2orgs.courses.semester_id=eq.$currentSemester&order=name,code',
data: { page: '${page}', perPage: '${perPage}', parent: 'in.(${id})' },
adaptor: '',
requestAdaptor:
"if (api.query.parent === 'in.()') {\r\n api.url = api.url.replace('&parent=in.%28%29', '')\r\n}\r\n\r\nreturn {\r\n ...api\r\n}",
},
messages: {},
mode: 'cards',
card: {
type: 'card',
header: { title: '$name', subTitle: '$code' },
body: [],
toolbar: [
{
type: 'button',
label: '可用时间',
icon: 'fa fa-group',
iconClassName: '',
reload: '',
id: 'u:2681ac5e5b28',
editorState: 'default',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
body: [
{
type: 'timeslot-renderer',
api: {
method: 'get',
url: 'rest/timeslots?select=id,date,organization_id,class_id,periods(id,start_time,end_time)&class_id=eq.${id}&organization_id=eq.${centre_id}',
adaptor:
'return {\r\n data: {\r\n timeslots: payload.data.items.map(item => {\r\n return {\r\n id: item.id,\r\n date: item.date,\r\n semester_id: item.semester_id,\r\n organization_id: item.organization_id,\r\n class_id: item.class_id,\r\n period: item.periods\r\n }\r\n })\r\n }\r\n}',
},
copy: {
type: 'button',
label: '按钮',
actionType: 'dialog',
dialog: {
title: '复制本周设置到其他周',
body: [
{
type: 'form',
title: '表单',
api: {
method: 'post',
url: 'rest/rpc/timeslots',
dataType: 'json',
headers: { Prefer: 'params=single-object' },
requestAdaptor:
'return {\r\n ...api,\r\n data: api.data.timeslotcopy\r\n}',
},
id: 'u:f4b826ee7ac0',
body: [
{
type: 'timeslot-copy',
name: 'timeslotcopy',
mode: 'normal',
target: 'class',
id: 'u:ff2c4be14afe',
},
],
actions: [
{ type: 'submit', label: '提交', primary: true },
],
feat: 'Insert',
},
],
type: 'dialog',
closeOnEsc: true,
showCloseButton: true,
id: 'u:c7de31e65b52',
actions: [
{
type: 'button',
actionType: 'cancel',
label: '取消',
id: 'u:9451198740aa',
},
{
type: 'button',
actionType: 'confirm',
label: '确定',
primary: true,
id: 'u:f874015df55f',
},
],
},
},
target: 'class',
id: 'u:8467642842c2',
},
],
title: '班级时间设置',
closeOnEsc: true,
showCloseButton: true,
size: 'md',
actions: [],
id: 'u:5ddbcf531f34',
},
},
],
},
},
},
{
type: 'button',
label: ' 预览',
icon: 'fa fa-eye',
level: 'link',
size: 'md',
iconClassName: '',
id: 'u:f44e5cfab0c3',
editorState: 'default',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '班级可用时间',
body: [
{
type: 'tpl',
tpl: '<span class="label" style="background: #e5e7eb;">X</span>\n<span style="vertical-align: middle;">不可用时间</span>\n<span class="label label-info">X</span>\n<span style="vertical-align: middle;">可用时间</span>',
inline: false,
},
{
type: 'crud',
api: {
method: 'get',
url: 'rest/periods?select=*,timeslots(*)&timeslots.organization_id=eq.$centre_id&timeslots.date=gte.${semester.since}&timeslots.date=lte.${semester.to}&timeslots.class_id=eq.$id&order=code',
adaptor:
"let items = []\r\nconst week_list = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\r\nconst oneday = 24 * 60 * 60 * 1000\r\nconst get_week_num = (start, end) => {\r\n let s = new Date(start)\r\n let s_ms = s.getTime()\r\n let s_day = s.getDay() ? s.getDay() : 7\r\n s.setTime(s_ms - (s_day - 1) * oneday)\r\n s_ms = s.getTime()\r\n let e = new Date(end)\r\n let e_ms = e.getTime()\r\n let e_day = e.getDay() ? e.getDay() : 7\r\n return {\r\n sem_start: s,\r\n day: e_day,\r\n week_num: Math.ceil(((e_ms - s_ms) / oneday + 1) / 7)\r\n }\r\n}\r\nlet start = /(?<=\\[date\\]\\[0\\]=gte\\.)\\d+-\\d+-\\d+/g.exec(api.url)[0]\r\nlet end = /(?<=\\[date\\]\\[1\\]=lte\\.)\\d+-\\d+-\\d+/g.exec(api.url)[0]\r\nconst { sem_start, week_num } = get_week_num(start, end)\r\nfor (let i = 0; i < 7; i++) {\r\n let date_temp = new Date(sem_start)\r\n let date_temp_ms = date_temp.getTime()\r\n payload.data.items.map(x => {\r\n const [h, m, s] = x.start_time.split(':')\r\n return {\r\n ...x,\r\n cmp_time: new Date(2020, 0, 1, h, m, s)\r\n }\r\n }).sort((a, b) => {\r\n return a.cmp_time - b.cmp_time\r\n }).forEach(item => {\r\n let week_num_list = []\r\n for (let j = 0; j < week_num; j++) {\r\n date_temp.setTime(date_temp_ms + j * 7 * oneday + i * oneday)\r\n week_num_list.push({\r\n week: j + 1,\r\n date: null,\r\n tpl: \"<span title='\".concat(date_temp.toISOString().split('T')[0], \"' class='label' style='background: #e5e7eb;'>\", j + 1, \"</span>\")\r\n })\r\n }\r\n items.push({\r\n day: {\r\n lab: week_list[i],\r\n val: i + 1\r\n },\r\n period: {\r\n lab: item.name,\r\n val: item.id\r\n },\r\n weeks: week_num_list\r\n })\r\n })\r\n}\r\nconst row_mem = payload.data.items.length\r\npayload.data.items.map(x => {\r\n const [h, m, s] = x.start_time.split(':')\r\n return {\r\n ...x,\r\n cmp_time: new Date(2020, 0, 1, h, m, s)\r\n }\r\n}).sort((a, b) => {\r\n return a.cmp_time - b.cmp_time\r\n}).forEach((item, index) => {\r\n item.timeslots && item.timeslots.forEach(i => {\r\n const { day, week_num } = get_week_num(sem_start, i.date)\r\n items[(day - 1) * row_mem + index].weeks[week_num - 1].date = i.date\r\n items[(day - 1) * row_mem + index].weeks[week_num - 1].id = i.id\r\n const week = items[(day - 1) * row_mem + index].weeks[week_num - 1].week\r\n items[(day - 1) * row_mem + index].weeks[week_num - 1].tpl = \"<span title='\".concat(i.date, \"' class='label label-primary'>\", week, \"</span>\")\r\n })\r\n})\r\nreturn {\r\n ...payload,\r\n data: {\r\n items: items\r\n }\r\n}",
requestAdaptor: '',
},
messages: {},
columns: [
{
name: 'day.lab',
label: '',
type: 'text',
inline: true,
className: 'common-padding',
},
{
type: 'text',
label: '',
name: 'period.lab',
placeholder: '-',
className: 'common-padding',
inline: true,
},
{
name: 'weeks',
label: '',
type: 'each',
items: [
{
type: 'tpl',
tpl: '<%= data.tpl %>',
inline: true,
className: 'm-r-xs',
},
],
placeholder: '-',
className: 'common-padding',
},
],
combineNum: 1,
columnsTogglable: false,
syncLocation: false,
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'lg',
data: null,
bodyClassName: 'p-t-none',
actions: [],
},
},
],
},
},
},
],
actionsCount: 3,
id: 'u:140b3d90b196',
},
syncLocation: false,
placeholder: '暂无数据',
columnsCount: 4,
name: 'class_set',
headerToolbar: [],
footerToolbar: [
{ type: 'pagination' },
{ type: 'switch-per-page' },
{ type: 'statistics' },
],
perPageAvailable: [20, 40, 80],
defaultParams: { perPage: 20 },
bulkActions: [],
itemActions: [],
loadDataOnce: false,
id: 'u:06cd3a22ee0c',
},
],
messages: {},
title: '',
cssVars: { '--Checkbox-onDisabled-color': 'transparent' },
id: 'u:1cbc663fae2b',
};
export { schema };

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,289 @@
const schema = {
type: 'page',
body: [
{
type: 'form',
title: '表单',
className: 'm-b-sm',
submitOnChange: true,
submitOnInit: true,
reload:
'manage_list?project_id=$project_id&teacher_id=$teacher_id&location_id=$location_id&date=$date&period_id=$period_id',
wrapWithPanel: false,
mode: 'inline',
name: 'filter',
body: [
{
type: 'group',
body: [
{
type: 'select',
label: '项目',
name: 'project_id',
mode: 'inline',
clearable: true,
source: {
method: 'get',
url: 'rest/projects?organization_id=eq.$centre_id&semester_id=eq.$currentSemester&order=name',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
checkAll: false,
size: 'md',
searchable: true,
inputClassName: 'm-l-xs',
className: 'm-l-sm',
},
],
},
{
type: 'group',
body: [
{
type: 'input-date-range',
name: 'date',
mode: 'inline',
label: '日期',
clearable: true,
inputClassName: 'm-l-xs',
className: 'm-l-sm',
value: '',
format: 'YYYY-MM-DD',
ranges:
'yesterday,today,7daysago,prevweek,thismonth,prevmonth,prevquarter',
},
{
label: '节次',
type: 'select',
name: 'period_id',
mode: 'inline',
clearable: true,
source: {
method: 'get',
url: 'rest/periods?order=name',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}`,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
checkAll: false,
size: 'md',
searchable: true,
inputClassName: 'm-l-xs',
labelClassName: '',
},
],
},
{
type: 'group',
body: [
{
type: 'select',
name: 'location_id',
mode: 'inline',
label: '地点',
clearable: true,
source: {
method: 'get',
url: 'rest/locations?organization_id=eq.$centre_id&order=name',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
checkAll: false,
size: 'md',
searchable: true,
inputClassName: 'm-l-xs',
className: 'm-l-sm',
},
{
type: 'select',
name: 'teacher_id',
mode: 'inline',
className: 'm-l-sm',
label: '教师',
clearable: true,
inputClassName: 'm-l-xs',
size: 'md',
checkAll: false,
source: {
method: 'get',
url: 'rest/users?role=eq.teacher&order=name',
adaptor:
'return {\r\n ...payload,\r\n data: payload.data.items.map(item => {\r\n return {\r\n label: `${item.name}(${item.code})`,\r\n value: "eq.".concat(item.id)\r\n }\r\n })\r\n}',
},
searchable: true,
},
],
mode: '',
subFormMode: '',
gap: '',
className: '',
},
],
},
{
type: 'divider',
lineStyle: 'solid',
},
{
type: 'crud',
api: {
method: 'get',
url: 'rest/user2projects?select=*,att_status_text:user2project_att_status_dict_fk(dictvalue),schedule_status_text:user2project_schedule_status_dict_fk(dictvalue),users_ex:users_ex!user2project_student_id_fkey!inner(*),schedule_stats!inner(*)&semester_id=eq.${currentSemester}&schedule_status=neq.canceled&users_ex.role=eq.student&users_ex.org_type=eq.class&schedule_stats.organization_id=eq.${centre_id}',
data: {
page: '${page}',
perPage: '${perPage}',
'&': '$$',
},
adaptor:
"let supportDateTimeFormat = typeof(Intl.DateTimeFormat) === 'function'\r\n\r\npayload.data.items.forEach(item => {\r\n item.schedule_stats.day = supportDateTimeFormat ? `${item.schedule_stats.date}${new Intl.DateTimeFormat('zh-CN', { weekday: 'short'}).format(new Date(item.schedule_stats.date))}` : item.schedule_stats.date;\r\n})\r\n\r\nreturn {\r\n ...payload\r\n}",
requestAdaptor:
"if (api.body.project_id === '') {\r\n api.url = api.url.replace('&project_id=', '')\r\n}\r\n\r\nif (api.body.date === '') {\r\n api.url = api.url.replace('&date=', '')\r\n} else {\r\n let date_former = api.body.date && api.body.date.split(',')[0]\r\n let date_latter = api.body.date && api.body.date.split(',')[1]\r\n\r\n if (date_former === date_latter) {\r\n api.url = api.url.replace(\r\n '&date=' + date_former + '%2C' + date_latter,\r\n '&schedule_stats.date=eq.' + date_former\r\n )\r\n } else {\r\n api.url = api.url.replace(\r\n '&date=' + date_former + '%2C' + date_latter,\r\n '&schedule_stats.date=gte.' + date_former + '&schedule_stats.date=lte.' + date_latter\r\n )\r\n }\r\n}\r\n\r\nif (api.body.period_id === '') {\r\n api.url = api.url.replace('&period_id=', '')\r\n} else {\r\n api.url = api.url.replace(\r\n '&period_id=' + api.body.period_id,\r\n '&schedule_stats.period_id=' + api.body.period_id\r\n )\r\n}\r\n\r\nif (api.body.location_id === '') {\r\n api.url = api.url.replace('&location_id=', '')\r\n} else {\r\n api.url = api.url.replace(\r\n '&location_id=' + api.body.location_id,\r\n '&schedule_stats.location_id=' + api.body.location_id\r\n )\r\n}\r\n\r\nif (api.body.teacher_id === '') {\r\n api.url = api.url.replace('&teacher_id=', '')\r\n} else {\r\n api.url = api.url.replace(\r\n '&teacher_id=' + api.body.teacher_id,\r\n '&schedule_stats.teacher_id=' + api.body.teacher_id\r\n )\r\n}\r\n\r\nreturn api;",
},
columns: [
{
name: 'schedule_stats.project_name',
label: '项目',
type: 'text',
placeholder: '-',
},
{
name: 'schedule_stats.day',
label: '日期',
type: 'text',
placeholder: '-',
},
{
type: 'text',
label: '节次',
name: 'schedule_stats.period_name',
placeholder: '-',
},
{
type: 'text',
label: '地点',
name: 'schedule_stats.location_name',
placeholder: '-',
},
{
type: 'text',
label: '教师',
name: 'schedule_stats.teacher_name',
placeholder: '-',
},
{
type: 'text',
label: '学号',
name: 'users_ex.code',
placeholder: '-',
},
{
type: 'text',
label: '姓名',
name: 'users_ex.name',
placeholder: '-',
},
{
type: 'text',
label: '班级',
name: 'users_ex.org_name',
placeholder: '-',
},
{
type: 'text',
label: '选课状态',
name: 'schedule_status_text.dictvalue',
placeholder: '-',
},
{
type: 'text',
label: '考勤状态',
name: 'att_status_text.dictvalue',
placeholder: '-',
},
],
messages: {},
syncLocation: false,
pageField: 'page',
perPageField: 'perPage',
headerToolbar: [
{
type: 'export-excel',
align: 'right',
label: '导出',
icon: 'fa fa-download',
level: 'primary',
api: {
method: 'get',
url: 'rest/user2projects?select=*,att_status_text:user2project_att_status_dict_fk(dictvalue),schedule_status_text:user2project_schedule_status_dict_fk(dictvalue),users_ex:users_ex!user2project_student_id_fkey!inner(*),schedule_stats!inner(*)&semester_id=eq.${currentSemester}&schedule_status=neq.canceled&users_ex.role=eq.student&users_ex.org_type=eq.class&schedule_stats.organization_id=eq.${centre_id}',
data: {
project_id: '$project_id',
date: '$date',
period_id: '$period_id',
location_id: '$location_id',
teacher_id: '$teacher_id',
},
requestAdaptor:
"if (api.body.project_id === '') {\r\n api.url = api.url.replace('&project_id=', '')\r\n}\r\n\r\nif (api.body.date === '') {\r\n api.url = api.url.replace('&date=', '')\r\n} else {\r\n let date_former = api.body.date && api.body.date.split(',')[0]\r\n let date_latter = api.body.date && api.body.date.split(',')[1]\r\n\r\n if (date_former === date_latter) {\r\n api.url = api.url.replace(\r\n '&date=' + date_former + '%2C' + date_latter,\r\n '&schedule_stats.date=eq.' + date_former\r\n )\r\n } else {\r\n api.url = api.url.replace(\r\n '&date=' + date_former + '%2C' + date_latter,\r\n '&schedule_stats.date=gte.' + date_former + '&schedule_stats.date=lte.' + date_latter\r\n )\r\n }\r\n}\r\n\r\nif (api.body.period_id === '') {\r\n api.url = api.url.replace('&period_id=', '')\r\n} else {\r\n api.url = api.url.replace(\r\n '&period_id=' + api.body.period_id,\r\n '&schedule_stats.period_id=' + api.body.period_id\r\n )\r\n}\r\n\r\nif (api.body.location_id === '') {\r\n api.url = api.url.replace('&location_id=', '')\r\n} else {\r\n api.url = api.url.replace(\r\n '&location_id=' + api.body.location_id,\r\n '&schedule_stats.location_id=' + api.body.location_id\r\n )\r\n}\r\n\r\nif (api.body.teacher_id === '') {\r\n api.url = api.url.replace('&teacher_id=', '')\r\n} else {\r\n api.url = api.url.replace(\r\n '&teacher_id=' + api.body.teacher_id,\r\n '&schedule_stats.teacher_id=' + api.body.teacher_id\r\n )\r\n}\r\n\r\nreturn api;",
},
exportColumns: [
{
label: '项目',
name: 'schedule_stats.project_name',
},
{
name: 'schedule_stats.date',
label: '日期',
},
{
name: 'schedule_stats.period_name',
label: '节次',
},
{
name: 'schedule_stats.location_name',
label: '地点',
},
{
name: 'schedule_stats.teacher_name',
label: '教师',
},
{
name: 'users_ex.code',
label: '学号',
},
{
name: 'users_ex.name',
label: '姓名',
},
{
name: 'users_ex.org_name',
label: '班级',
},
{
name: 'schedule_status_text.dictvalue',
label: '选课状态',
},
{
name: 'att_status_text.dictvalue',
label: '考勤状态',
},
],
filename: '选课名单',
},
],
footerToolbar: [
{ type: 'pagination' },
{ type: 'switch-per-page' },
{ type: 'statistics' },
],
name: 'manage_list',
perPageAvailable: [10, 20, 30, 40, 50],
placeholder: '-',
},
],
title: '',
messages: {},
bodyClassName: '',
className: '',
headerClassName: '',
};
export { schema };

View File

@@ -0,0 +1,332 @@
const schema = {
type: 'page',
body: [
{
type: 'form',
className: '',
submitOnChange: true,
reload: 'loc_set?locationSelect=$locationSelect',
wrapWithPanel: false,
body: [
{
type: 'select',
name: 'locationSelect',
label: '查询',
mode: 'inline',
size: 'md',
clearable: true,
searchable: true,
source: {
method: 'get',
url: 'rest/locations?organization_id=eq.$centre_id&order=code',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n ...payload.data,\r\n items: payload.data.items.map(item => {\r\n return {\r\n label: item.name,\r\n value: item.id\r\n }\r\n })\r\n }\r\n}',
},
inputClassName: 'm-l-xs',
checkAll: false,
perPageAvailable: [10],
options: [],
},
],
},
{ type: 'divider', lineStyle: 'solid' },
{
type: 'tpl',
className: 'm-b-sm',
tpl: '<% if (data.excludeRoomTime) { %>\n <span class="label label-warning">选择不可用时间<span>\n<% } else { %>\n <span class="label label-info">选择可用时间<span>\n<% } %>',
inline: false,
},
{
type: 'crud',
syncLocation: false,
api: {
method: 'get',
url: 'rest/locations?organization_id=eq.$centre_id&order=code',
data: { page: '$page', perPage: '$perPage', id: 'eq.$locationSelect' },
requestAdaptor:
"if (api.query.id === 'eq.') {\r\n api.url = api.url.replace('&id=eq.', '')\r\n}\r\n\r\nreturn {\r\n ...api\r\n}",
adaptor: '',
replaceData: false,
},
perPageAvailable: [20, 40, 80],
messages: {},
name: 'loc_set',
defaultParams: { perPage: 20 },
card: {
type: 'card',
header: { title: '$name', subTitle: '$code' },
body: [{ type: 'tpl', tpl: '${intro|raw}' }],
toolbar: [
{
type: 'button',
label: '时间选择',
icon: 'fa fa-group',
iconClassName: '',
reload: '',
id: 'u:f67c49dcfe29',
editorState: 'default',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
body: [
{
type: 'timeslot-renderer',
api: {
method: 'get',
url: 'rest/timeslots?select=id,date,organization_id,location_id,periods(id,start_time,end_time)&location_id=eq.${id}&organization_id=eq.${centre_id}',
adaptor:
'return {\r\n data: {\r\n timeslots: payload.data.items.map(item => {\r\n return {\r\n id: item.id,\r\n date: item.date,\r\n semester_id: item.semester_id,\r\n organization_id: item.organization_id,\r\n location_id: item.location_id,\r\n period: item.periods\r\n }\r\n })\r\n }\r\n}',
},
copy: {
type: 'button',
label: '按钮',
actionType: 'dialog',
dialog: {
title: '复制本周设置到其他周',
body: [
{
type: 'form',
title: '表单',
controls: [
{
type: 'timeslot-copy',
name: 'timeslotcopy',
mode: 'normal',
target: 'location',
},
],
api: {
method: 'post',
url: 'rest/rpc/timeslots',
dataType: 'json',
headers: { Prefer: 'params=single-object' },
requestAdaptor:
'return {\r\n ...api,\r\n data: api.data.timeslotcopy\r\n}',
},
},
],
type: 'dialog',
closeOnEsc: false,
showCloseButton: true,
},
},
target: 'location',
},
],
title: '场地时间设置',
closeOnEsc: true,
showCloseButton: true,
size: 'md',
actions: [],
},
},
],
},
},
},
{
type: 'button',
label: '预览',
icon: 'fa fa-eye',
level: 'link',
size: 'md',
iconClassName: '',
visibleOn: '!this.excludeRoomTime',
id: 'u:c8b5eb1749e3',
editorState: 'default',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '场地可用时间',
body: [
{
type: 'tpl',
tpl: '<span class="label" style="background: #e5e7eb;">X</span>\n<span style="vertical-align: middle;">不可用时间</span>\n<span class="label label-info">X</span>\n<span style="vertical-align: middle;">可用时间</span>',
inline: false,
},
{
type: 'crud',
api: {
method: 'get',
url: 'rest/periods?select=*,timeslots(*)&timeslots.organization_id=eq.$centre_id&timeslots.date=gte.${semester.since}&timeslots.date=lte.${semester.to}&timeslots.location_id=eq.$id&order=code',
adaptor:
"let items = []\r\nconst week_list = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\r\nconst oneday = 24 * 60 * 60 * 1000\r\nconst get_week_num = (start, end) => {\r\n let s = new Date(start)\r\n let s_ms = s.getTime()\r\n let s_day = s.getDay() ? s.getDay() : 7\r\n s.setTime(s_ms - (s_day - 1) * oneday)\r\n s_ms = s.getTime()\r\n let e = new Date(end)\r\n let e_ms = e.getTime()\r\n let e_day = e.getDay() ? e.getDay() : 7\r\n return {\r\n sem_start: s,\r\n day: e_day,\r\n week_num: Math.ceil(((e_ms - s_ms) / oneday + 1) / 7)\r\n }\r\n}\r\nlet start = /(?<=\\[date\\]\\[0\\]=gte\\.)\\d+-\\d+-\\d+/g.exec(api.url)[0]\r\nlet end = /(?<=\\[date\\]\\[1\\]=lte\\.)\\d+-\\d+-\\d+/g.exec(api.url)[0]\r\nconst { sem_start, week_num } = get_week_num(start, end)\r\nfor (let i = 0; i < 7; i++) {\r\n let date_temp = new Date(sem_start)\r\n let date_temp_ms = date_temp.getTime()\r\n payload.data.items.map(x => {\r\n const [h, m, s] = x.start_time.split(':')\r\n return {\r\n ...x,\r\n cmp_time: new Date(2020, 0, 1, h, m, s)\r\n }\r\n }).sort((a, b) => {\r\n return a.cmp_time - b.cmp_time\r\n }).forEach(item => {\r\n let week_num_list = []\r\n for (let j = 0; j < week_num; j++) {\r\n date_temp.setTime(date_temp_ms + j * 7 * oneday + i * oneday)\r\n week_num_list.push({\r\n week: j + 1,\r\n date: null,\r\n tpl: \"<span title='\".concat(date_temp.toISOString().split('T')[0], \"' class='label' style='background: #e5e7eb;'>\", j + 1, \"</span>\")\r\n })\r\n }\r\n items.push({\r\n day: {\r\n lab: week_list[i],\r\n val: i + 1\r\n },\r\n period: {\r\n lab: item.name,\r\n val: item.id\r\n },\r\n weeks: week_num_list\r\n })\r\n })\r\n}\r\nconst row_mem = payload.data.items.length\r\npayload.data.items.map(x => {\r\n const [h, m, s] = x.start_time.split(':')\r\n return {\r\n ...x,\r\n cmp_time: new Date(2020, 0, 1, h, m, s)\r\n }\r\n}).sort((a, b) => {\r\n return a.cmp_time - b.cmp_time\r\n}).forEach((item, index) => {\r\n item.timeslots && item.timeslots.forEach(i => {\r\n const { day, week_num } = get_week_num(sem_start, i.date)\r\n items[(day - 1) * row_mem + index].weeks[week_num - 1].date = i.date\r\n items[(day - 1) * row_mem + index].weeks[week_num - 1].id = i.id\r\n const week = items[(day - 1) * row_mem + index].weeks[week_num - 1].week\r\n items[(day - 1) * row_mem + index].weeks[week_num - 1].tpl = \"<span title='\".concat(i.date, \"' class='label label-primary'>\", week, \"</span>\")\r\n })\r\n})\r\nreturn {\r\n ...payload,\r\n data: {\r\n items: items\r\n }\r\n}\r\n",
requestAdaptor: '',
},
messages: {},
columns: [
{
name: 'day.lab',
label: '',
type: 'text',
inline: true,
className: 'common-padding',
},
{
type: 'text',
label: '',
name: 'period.lab',
placeholder: '-',
className: 'common-padding',
inline: true,
},
{
name: 'weeks',
label: '',
type: 'each',
items: [
{
type: 'tpl',
tpl: '<%= data.tpl %>',
inline: true,
className: 'm-r-xs',
},
],
placeholder: '-',
className: 'common-padding',
},
],
combineNum: 1,
columnsTogglable: false,
syncLocation: false,
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'lg',
data: null,
bodyClassName: 'p-t-none',
actions: [],
},
},
],
},
},
},
{
type: 'button',
label: '预览',
icon: 'fa fa-eye',
level: 'link',
size: 'md',
iconClassName: '',
visibleOn: 'this.excludeRoomTime',
id: 'u:fb5fb80671b4',
editorState: 'default',
onEvent: {
click: {
weight: 0,
actions: [
{
ignoreError: false,
actionType: 'dialog',
dialog: {
type: 'dialog',
title: '场地可用时间',
body: [
{
type: 'tpl',
tpl: '<span class="label" style="background: #e5e7eb;">X</span>\n<span style="vertical-align: middle;">不可用时间</span>\n<span class="label label-info">X</span>\n<span style="vertical-align: middle;">可用时间</span>',
inline: false,
},
{
type: 'crud',
api: {
method: 'get',
url: 'rest/periods?select=*,timeslots(*)&timeslots.organization_id=eq.$centre_id&timeslots.date=gte.${semester.since}&timeslots.date=lte.${semester.to}&timeslots.location_id=eq.$id&order=code',
adaptor:
"let items = []\r\nconst week_list = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']\r\nconst oneday = 24 * 60 * 60 * 1000\r\nconst get_week_num = (start, end) => {\r\n let s = new Date(start)\r\n let s_ms = s.getTime()\r\n let s_day = s.getDay() ? s.getDay() : 7\r\n s.setTime(s_ms - (s_day - 1) * oneday)\r\n s_ms = s.getTime()\r\n let e = new Date(end)\r\n let e_ms = e.getTime()\r\n let e_day = e.getDay() ? e.getDay() : 7\r\n return {\r\n sem_start: s,\r\n day: e_day,\r\n week_num: Math.ceil(((e_ms - s_ms) / oneday + 1) / 7)\r\n }\r\n}\r\nlet start = /(?<=\\[date\\]\\[0\\]=gte\\.)\\d+-\\d+-\\d+/g.exec(api.url)[0]\r\nlet end = /(?<=\\[date\\]\\[1\\]=lte\\.)\\d+-\\d+-\\d+/g.exec(api.url)[0]\r\nconst { sem_start, week_num } = get_week_num(start, end)\r\nfor (let i = 0; i < 7; i++) {\r\n let date_temp = new Date(sem_start)\r\n let date_temp_ms = date_temp.getTime()\r\n payload.data.items.map(x => {\r\n const [h, m, s] = x.start_time.split(':')\r\n return {\r\n ...x,\r\n cmp_time: new Date(2020, 0, 1, h, m, s)\r\n }\r\n }).sort((a, b) => {\r\n return a.cmp_time - b.cmp_time\r\n }).forEach(item => {\r\n let week_num_list = []\r\n for (let j = 0; j < week_num; j++) {\r\n date_temp.setTime(date_temp_ms + j * 7 * oneday + i * oneday)\r\n week_num_list.push({\r\n week: j + 1,\r\n date: null,\r\n tpl: \"<span title='\".concat(date_temp.toISOString().split('T')[0], \"' class='label label-info'>\", j + 1, \"</span>\")\r\n })\r\n }\r\n items.push({\r\n day: {\r\n lab: week_list[i],\r\n val: i + 1\r\n },\r\n period: {\r\n lab: item.name,\r\n val: item.id\r\n },\r\n weeks: week_num_list\r\n })\r\n })\r\n}\r\nconst row_mem = payload.data.items.length\r\npayload.data.items.map(x => {\r\n const [h, m, s] = x.start_time.split(':')\r\n return {\r\n ...x,\r\n cmp_time: new Date(2020, 0, 1, h, m, s)\r\n }\r\n}).sort((a, b) => {\r\n return a.cmp_time - b.cmp_time\r\n}).forEach((item, index) => {\r\n item.timeslots && item.timeslots.forEach(i => {\r\n const { day, week_num } = get_week_num(sem_start, i.date)\r\n items[(day - 1) * row_mem + index].weeks[week_num - 1].date = i.date\r\n items[(day - 1) * row_mem + index].weeks[week_num - 1].id = i.id\r\n const week = items[(day - 1) * row_mem + index].weeks[week_num - 1].week\r\n items[(day - 1) * row_mem + index].weeks[week_num - 1].tpl = \"<span class='label' style='background: #e5e7eb;'>\".concat(week, \"</span>\")\r\n items[(day - 1) * row_mem + index].weeks[week_num - 1].tpl = \"<span title='\".concat(i.date, \"' class='label' style='background: #e5e7eb;'>\", week, \"</span>\")\r\n })\r\n})\r\nreturn {\r\n ...payload,\r\n data: {\r\n items: items\r\n }\r\n}",
requestAdaptor: '',
},
messages: {},
columns: [
{
name: 'day.lab',
label: '',
type: 'text',
inline: true,
className: 'common-padding',
},
{
type: 'text',
label: '',
name: 'period.lab',
placeholder: '-',
className: 'common-padding',
},
{
name: 'weeks',
label: '',
type: 'each',
items: [
{
type: 'tpl',
tpl: '<%= data.tpl %>',
inline: true,
className: 'm-r-xs',
},
],
placeholder: '-',
className: 'common-padding',
},
],
combineNum: 1,
columnsTogglable: false,
syncLocation: false,
},
],
closeOnEsc: true,
showCloseButton: true,
size: 'lg',
data: null,
bodyClassName: 'p-t-none',
actions: [],
},
},
],
},
},
},
],
actionsCount: 3,
},
mode: 'cards',
headerToolbar: [],
footerToolbar: [
{ type: 'pagination' },
{ type: 'switch-per-page' },
{ type: 'statistics' },
],
columnsCount: 4,
placeholder: '暂无数据',
pageField: 'page',
perPageField: 'perPage',
initApi: '',
initFetch: '',
checkAll: false,
},
],
messages: {},
initApi: {
method: 'get',
url: 'rest/config?key=eq.system&organization_id=eq.${centre_id}',
adaptor:
'return {\r\n ...payload,\r\n data: {\r\n excludeRoomTime: payload.data.items[0].value.excludeRoomTime\r\n }\r\n}',
},
title: '',
cssVars: { '--Checkbox-onDisabled-color': 'transparent' },
};
export { schema };

File diff suppressed because one or more lines are too long

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