├── .prettierignore ├── .stylelintignore ├── .eslintignore ├── public ├── config.js ├── client │ ├── favicon.ico │ ├── imgs │ │ ├── cli.jpg │ │ ├── favs.jpg │ │ ├── leave.jpg │ │ ├── type.jpg │ │ ├── window.jpg │ │ └── logo-icon-rotate.svg │ └── index.html ├── nginx.conf ├── package.json └── start.js ├── babel.config.js ├── postcss.config.js ├── .stylelintrc.json ├── env ├── local │ └── readme.txt └── demo │ └── elux.config.js ├── src ├── modules │ ├── dashboard │ │ ├── entity.ts │ │ ├── index.ts │ │ ├── package.json │ │ ├── views │ │ │ ├── Workplace │ │ │ │ └── index.module.less │ │ │ └── Main.tsx │ │ └── model.ts │ ├── stage │ │ ├── assets │ │ │ ├── css │ │ │ │ ├── var.less │ │ │ │ ├── antd-var.json │ │ │ │ └── global.module.less │ │ │ └── imgs │ │ │ │ ├── loading48x48.gif │ │ │ │ └── logo-icon.svg │ │ ├── components │ │ │ ├── MSelect │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── DateTime.tsx │ │ │ ├── MTable │ │ │ │ └── index.module.less │ │ │ ├── ErrorPage │ │ │ │ └── index.tsx │ │ │ ├── LoadingPanel │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── MSearch │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ └── DialogPage │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ ├── index.ts │ │ ├── views │ │ │ ├── ForgotPassword │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── Agreement │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── RegistryForm │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── LoginForm │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ └── Main.tsx │ │ ├── package.json │ │ ├── utils │ │ │ ├── const.ts │ │ │ ├── errors.ts │ │ │ ├── request.ts │ │ │ ├── base.ts │ │ │ ├── tools.ts │ │ │ └── resource.ts │ │ ├── entity.ts │ │ ├── api.ts │ │ └── model.ts │ ├── admin │ │ ├── views │ │ │ ├── Menu │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── Main │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── Flag │ │ │ │ ├── index.tsx │ │ │ │ └── index.module.less │ │ │ ├── Tabs │ │ │ │ ├── index.module.less │ │ │ │ ├── index.tsx │ │ │ │ └── Editor.tsx │ │ │ └── Header │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ ├── index.ts │ │ ├── package.json │ │ ├── entity.ts │ │ ├── api.ts │ │ └── model.ts │ ├── article │ │ ├── index.ts │ │ ├── model.ts │ │ ├── package.json │ │ ├── views │ │ │ ├── Edit.tsx │ │ │ ├── Index.tsx │ │ │ ├── Main.tsx │ │ │ ├── SearchForm.tsx │ │ │ ├── Detail.tsx │ │ │ ├── EditorForm.tsx │ │ │ ├── Maintain.tsx │ │ │ └── ListTable.tsx │ │ ├── api.ts │ │ └── entity.ts │ └── member │ │ ├── index.ts │ │ ├── model.ts │ │ ├── package.json │ │ ├── views │ │ ├── Edit.tsx │ │ ├── Main.tsx │ │ ├── SearchForm.tsx │ │ ├── Selector.tsx │ │ ├── Detail.tsx │ │ ├── Maintain.tsx │ │ ├── EditorForm.tsx │ │ └── ListTable.tsx │ │ ├── api.ts │ │ └── entity.ts ├── .eslintrc.js ├── env.d.ts ├── tsconfig.json ├── index.ts ├── Global.ts └── Project.ts ├── mock ├── public │ ├── imgs │ │ ├── admin.jpg │ │ └── guest.png │ └── index.html ├── package.json ├── tsconfig.json ├── start.js └── src │ ├── index.ts │ ├── database.ts │ └── routes │ ├── session.ts │ ├── article.ts │ └── member.ts ├── commitlint.config.js ├── .prettierrc.js ├── .gitignore ├── .editorconfig ├── tsconfig.json ├── .vscode ├── extensions.json └── settings.json ├── .eslintrc.js ├── .docs.js ├── LICENSE ├── elux.config.js └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | *.tsx 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | template/local/src/config.js -------------------------------------------------------------------------------- /public/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: 4003, 3 | proxy: {}, 4 | }; 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@elux', {ui: 'vue'}]], 3 | }; 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('autoprefixer')], 3 | }; 4 | -------------------------------------------------------------------------------- /.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@elux/stylelint-config/less"], 3 | "rules": {} 4 | } 5 | -------------------------------------------------------------------------------- /env/local/readme.txt: -------------------------------------------------------------------------------- 1 | - 默认本地环境设置,可不上传Git,本目录中的内容可以覆盖项目中的: 2 | /public 3 | /elux.config.js 4 | -------------------------------------------------------------------------------- /src/modules/dashboard/entity.ts: -------------------------------------------------------------------------------- 1 | export enum CurView { 2 | 'workplace' = 'workplace', 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/stage/assets/css/var.less: -------------------------------------------------------------------------------- 1 | @import '~ant-design-vue/lib/style/themes/default.less'; 2 | -------------------------------------------------------------------------------- /public/client/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiisea/elux-vue-antd-admin/HEAD/public/client/favicon.ico -------------------------------------------------------------------------------- /mock/public/imgs/admin.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiisea/elux-vue-antd-admin/HEAD/mock/public/imgs/admin.jpg -------------------------------------------------------------------------------- /mock/public/imgs/guest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiisea/elux-vue-antd-admin/HEAD/mock/public/imgs/guest.png -------------------------------------------------------------------------------- /public/client/imgs/cli.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiisea/elux-vue-antd-admin/HEAD/public/client/imgs/cli.jpg -------------------------------------------------------------------------------- /public/client/imgs/favs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiisea/elux-vue-antd-admin/HEAD/public/client/imgs/favs.jpg -------------------------------------------------------------------------------- /public/client/imgs/leave.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiisea/elux-vue-antd-admin/HEAD/public/client/imgs/leave.jpg -------------------------------------------------------------------------------- /public/client/imgs/type.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiisea/elux-vue-antd-admin/HEAD/public/client/imgs/type.jpg -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: {}, 4 | }; 5 | -------------------------------------------------------------------------------- /public/client/imgs/window.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiisea/elux-vue-antd-admin/HEAD/public/client/imgs/window.jpg -------------------------------------------------------------------------------- /src/modules/admin/views/Menu/index.module.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | padding-top: 15px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'es5', 4 | bracketSpacing: false, 5 | }; 6 | -------------------------------------------------------------------------------- /src/modules/stage/assets/css/antd-var.json: -------------------------------------------------------------------------------- 1 | { 2 | "@layout-header-background": "#1e2d3d", 3 | "@menu-dark-inline-submenu-bg": "#192639" 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .stylelintcache 3 | .eslintcache 4 | *.log 5 | dist/ 6 | node_modules/ 7 | deploy_versions/ 8 | .temp/ 9 | .rn_temp/ 10 | -------------------------------------------------------------------------------- /src/modules/stage/assets/imgs/loading48x48.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hiisea/elux-vue-antd-admin/HEAD/src/modules/stage/assets/imgs/loading48x48.gif -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['plugin:@elux/vue'], 4 | env: { 5 | browser: true, 6 | node: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/modules/stage/components/MSelect/index.module.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | width: 100%; 4 | 5 | .icon-button { 6 | user-select: none; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/article/index.ts: -------------------------------------------------------------------------------- 1 | import {exportModule} from '@elux/vue-web'; 2 | import {Model} from './model'; 3 | import main from './views/Main'; 4 | 5 | export default exportModule('article', Model, {main}); 6 | -------------------------------------------------------------------------------- /src/modules/member/index.ts: -------------------------------------------------------------------------------- 1 | import {exportModule} from '@elux/vue-web'; 2 | import {Model} from './model'; 3 | import main from './views/Main'; 4 | 5 | export default exportModule('member', Model, {main}); 6 | -------------------------------------------------------------------------------- /src/modules/dashboard/index.ts: -------------------------------------------------------------------------------- 1 | import {exportModule} from '@elux/vue-web'; 2 | import {Model} from './model'; 3 | import main from './views/Main'; 4 | 5 | export default exportModule('dashboard', Model, {main}); 6 | -------------------------------------------------------------------------------- /src/modules/admin/index.ts: -------------------------------------------------------------------------------- 1 | //封装并导出本模块 2 | import {exportModule} from '@elux/vue-web'; 3 | import {Model} from './model'; 4 | import main from './views/Main'; 5 | 6 | export default exportModule('admin', Model, {main}); 7 | -------------------------------------------------------------------------------- /src/modules/stage/index.ts: -------------------------------------------------------------------------------- 1 | //封装并导出本模块 2 | import {exportModule} from '@elux/vue-web'; 3 | import {Model} from './model'; 4 | import main from './views/Main'; 5 | 6 | export default exportModule('stage', Model, {main}); 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | max_line_length = 150 11 | -------------------------------------------------------------------------------- /src/modules/stage/views/ForgotPassword/index.module.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | width: 380px; 4 | 5 | .btn-send-captcha { 6 | float: right; 7 | width: 110px; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@elux/babel-preset/tsconfig.lint.json", 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "rootDir": "./" 6 | }, 7 | "include": ["./"], 8 | "exclude": ["./dist"] 9 | } 10 | -------------------------------------------------------------------------------- /public/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name admin-react-antd.eluxjs.com; 4 | root /var/www/admin-react-antd/html; 5 | index index.html; 6 | location / { 7 | try_files $uri /client/index.html; 8 | } 9 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["davidanson.vscode-markdownlint", "dbaeumer.vscode-eslint", "stylelint.vscode-stylelint", "esbenp.prettier-vscode"], 3 | "unwantedRecommendations": ["editorconfig.editorconfig", "octref.vetur"] 4 | } 5 | -------------------------------------------------------------------------------- /mock/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |Welcome to Express
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const tsconfig = require('./tsconfig.json'); 2 | 3 | module.exports = { 4 | root: true, 5 | extends: ['plugin:@elux/common'], 6 | env: { 7 | browser: false, 8 | node: true, 9 | }, 10 | rules: {}, 11 | ignorePatterns: tsconfig.exclude, 12 | }; 13 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg'; 2 | declare module '*.png'; 3 | declare module '*.jpg'; 4 | declare module '*.gif'; 5 | declare module '*.less'; 6 | declare module '*.scss'; 7 | declare module '*.vue' { 8 | const Component: (props: any) => JSX.Element; 9 | export default Component; 10 | } 11 | -------------------------------------------------------------------------------- /env/demo/elux.config.js: -------------------------------------------------------------------------------- 1 | //该配置文件可以覆盖项目根目录下的elux.config.js 2 | //使用yarn build:demo时会使用本配置文件进行编译 3 | module.exports = { 4 | prod: { 5 | clientGlobalVar: { 6 | ApiPrefix: 'http://api-admin-demo.eluxjs.com/', 7 | StaticPrefix: 'http://api-admin-demo.eluxjs.com/', 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/modules/stage/views/Agreement/index.module.less: -------------------------------------------------------------------------------- 1 | :global { 2 | :local(.root) { 3 | width: 800px; 4 | font-size: 16px; 5 | 6 | .footer { 7 | padding: 20px 0 10px; 8 | text-align: center; 9 | 10 | .ant-btn { 11 | width: 200px; 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /mock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@elux-admin-antd/mock", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "@types/express": "^4.17.9", 6 | "@types/morgan": "^1.9.3", 7 | "@types/mockjs": "^1.0.4", 8 | "mockjs": "^1.1.0", 9 | "express": "^4.0.0", 10 | "morgan": "^1.9.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/modules/member/model.ts: -------------------------------------------------------------------------------- 1 | import {BaseResource} from '@elux-admin-antd/stage/utils/resource'; 2 | import api from './api'; 3 | import {MemberResource, defaultListSearch} from './entity'; 4 | 5 | export class Model extends BaseResource([\w\W]*?)<\/pre>/g, (str, code) => {
13 | return ``;
14 | })
15 | .replace(/ align="center"/, '')
16 | .replace(/
]*?)>/g, '
')
17 | .replace(/public\/client\/imgs\//g, '/client/imgs/')
18 | );
19 | console.log('src/modules/dashboard/views/Workplace/summary.html');
20 |
--------------------------------------------------------------------------------
/src/modules/article/views/Edit.tsx:
--------------------------------------------------------------------------------
1 | import DialogPage from '@elux-admin-antd/stage/components/DialogPage';
2 | import {loadingPlaceholder} from '@elux-admin-antd/stage/utils/tools';
3 | import {Dispatch} from '@elux/vue-web';
4 | import {Skeleton} from 'ant-design-vue';
5 | import {ItemDetail} from '../entity';
6 | import EditorForm from './EditorForm';
7 |
8 | interface Props {
9 | dispatch: Dispatch;
10 | itemDetail?: ItemDetail;
11 | }
12 |
13 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
14 | const Component = ({itemDetail, dispatch}: Props) => {
15 | const title = loadingPlaceholder(itemDetail && (itemDetail.id ? '修改文章' : '新增文章'));
16 | return (
17 |
18 | {itemDetail ? : }
19 |
20 | );
21 | };
22 |
23 | export default Component;
24 |
--------------------------------------------------------------------------------
/src/modules/admin/entity.ts:
--------------------------------------------------------------------------------
1 | import {IRequest} from '@elux-admin-antd/stage/utils/base';
2 |
3 | export enum SubModule {
4 | 'dashboard' = 'dashboard',
5 | 'member' = 'member',
6 | 'article' = 'article',
7 | }
8 |
9 | export interface Notices {
10 | num: number;
11 | }
12 |
13 | export interface Tab {
14 | id: string;
15 | title: string;
16 | url: string;
17 | }
18 |
19 | export interface TabData {
20 | list: Tab[];
21 | maps: {[id: string]: Tab};
22 | }
23 |
24 | export interface MenuItem {
25 | key: string;
26 | label: string;
27 | icon?: string;
28 | match?: string | string[];
29 | link?: string;
30 | children?: MenuItem[];
31 | disable?: boolean;
32 | }
33 |
34 | export interface MenuData {
35 | items: MenuItem[];
36 | keyToParents: {[key: string]: string[]};
37 | keyToLink: {[key: string]: string};
38 | matchToKey: {[match: string]: string};
39 | }
40 | export type IGetNotices = IRequest<{}, Notices>;
41 |
42 | export type IGetMenu = IRequest<{}, MenuItem[]>;
43 |
--------------------------------------------------------------------------------
/src/modules/admin/views/Flag/index.module.less:
--------------------------------------------------------------------------------
1 | @import '@elux-admin-antd/stage/assets/css/var.less';
2 |
3 | :global {
4 | :local(.root) {
5 | height: 85px;
6 | overflow: hidden;
7 | background-color: @menu-dark-inline-submenu-bg;
8 |
9 | > .wrap {
10 | display: block;
11 | width: 180px;
12 | padding: 20px 0 0 10px;
13 | }
14 |
15 | .title {
16 | font-size: 16px;
17 | font-weight: bold;
18 | color: #fff;
19 | }
20 |
21 | .ver {
22 | display: inline-block;
23 | padding: 0 5px;
24 | font-size: 12px;
25 | line-height: 14px;
26 | color: black;
27 | background: #fff;
28 | border-radius: 10px;
29 | opacity: 0.5;
30 | }
31 |
32 | .logo {
33 | float: left;
34 | padding-top: 2px;
35 | margin-right: 8px;
36 | margin-left: 8px;
37 | }
38 | }
39 |
40 | .ant-layout-sider-collapsed :local(.root) {
41 | .title,
42 | .ver {
43 | display: none;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/modules/admin/views/Header/index.module.less:
--------------------------------------------------------------------------------
1 | :global {
2 | :local(.root) {
3 | display: flex;
4 | justify-content: space-between;
5 | height: 50px;
6 |
7 | > * {
8 | display: flex;
9 | align-items: center;
10 |
11 | > * {
12 | padding: 0 10px;
13 | }
14 | }
15 |
16 | > .side {
17 | .toggle-sider {
18 | padding-left: 20px;
19 | font-size: 20px;
20 | }
21 | }
22 |
23 | > .main {
24 | > * {
25 | line-height: 50px;
26 |
27 | &:hover {
28 | background: #eee;
29 | }
30 | }
31 |
32 | .notice {
33 | display: block;
34 | padding-right: 20px;
35 | padding-left: 20px;
36 | cursor: pointer;
37 |
38 | .anticon {
39 | font-size: 18px;
40 | }
41 | }
42 |
43 | .account {
44 | padding: 0 20px;
45 |
46 | .ant-avatar {
47 | margin-right: 8px;
48 | }
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/mock/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "CommonJS",
4 | "types": ["node"],
5 | "lib": ["es2020"],
6 | "target": "ESNext",
7 | "moduleResolution": "node",
8 | "skipLibCheck": true,
9 | "declaration": true,
10 | "allowJs": true,
11 | "checkJs": false,
12 | "strict": true,
13 | "isolatedModules": true,
14 | "preserveConstEnums": true,
15 | "sourceMap": false,
16 | "experimentalDecorators": true,
17 | "emitDecoratorMetadata": true,
18 | "newLine": "LF",
19 | "removeComments": true,
20 | "resolveJsonModule": true,
21 | "noImplicitReturns": true,
22 | "importHelpers": false,
23 | "forceConsistentCasingInFileNames": true,
24 | "suppressImplicitAnyIndexErrors": true,
25 | "allowSyntheticDefaultImports": true,
26 | "esModuleInterop": true,
27 | "outDir": "../dist/mock",
28 | "rootDir": "./",
29 | "baseUrl": "./",
30 | "paths": {
31 | "@/*": ["./src/*"]
32 | }
33 | },
34 | "include": ["./"]
35 | }
36 |
--------------------------------------------------------------------------------
/src/modules/member/views/Edit.tsx:
--------------------------------------------------------------------------------
1 | import DialogPage from '@elux-admin-antd/stage/components/DialogPage';
2 | import {loadingPlaceholder} from '@elux-admin-antd/stage/utils/tools';
3 | import {Dispatch} from '@elux/vue-web';
4 | import {Skeleton} from 'ant-design-vue';
5 | import {ItemDetail} from '../entity';
6 | import EditorForm from './EditorForm';
7 |
8 | interface Props {
9 | dispatch: Dispatch;
10 | itemDetail?: ItemDetail;
11 | }
12 |
13 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
14 | const Component = ({itemDetail, dispatch}: Props) => {
15 | const title = loadingPlaceholder(itemDetail && (itemDetail.id ? '修改用户' : '新增用户'));
16 | return (
17 |
18 |
19 | {itemDetail ? : }
20 |
21 |
22 | );
23 | };
24 |
25 | export default Component;
26 |
--------------------------------------------------------------------------------
/src/modules/dashboard/model.ts:
--------------------------------------------------------------------------------
1 | import {BaseModel} from '@elux/vue-web';
2 | import {pathToRegexp} from 'path-to-regexp';
3 | import {CurView} from './entity';
4 |
5 | export interface ModuleState {
6 | curView?: CurView;
7 | }
8 |
9 | interface RouteParams {
10 | curView?: CurView;
11 | }
12 |
13 | export class Model extends BaseModel {
14 | protected routeParams!: RouteParams;
15 | protected privateActions = this.getPrivateActions({});
16 |
17 | protected getRouteParams(): RouteParams {
18 | const {pathname} = this.getRouter().location;
19 | const [, , , curViewStr = ''] = pathToRegexp('/:admin/:dashboard/:curView').exec(pathname) || [];
20 | const curView: CurView | undefined = CurView[curViewStr] || undefined;
21 | return {curView};
22 | }
23 |
24 | public onMount(): void {
25 | this.routeParams = this.getRouteParams();
26 | const {curView} = this.routeParams;
27 | const initState: ModuleState = {curView};
28 | this.dispatch(this.privateActions._initState(initState));
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 hiisea
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/modules/article/views/Index.tsx:
--------------------------------------------------------------------------------
1 | import DialogPage from '@elux-admin-antd/stage/components/DialogPage';
2 | import {connectStore} from '@elux/vue-web';
3 | import {defineComponent} from 'vue';
4 | import {APPState} from '@/Global';
5 | import ListTable from './ListTable';
6 |
7 | interface StoreProps {
8 | prefixPathname: string;
9 | curRender?: string;
10 | }
11 |
12 | const mapStateToProps: (state: APPState) => StoreProps = (state) => {
13 | const {prefixPathname, curRender} = state.article!;
14 | return {prefixPathname, curRender};
15 | };
16 |
17 | const selection = {limit: -1};
18 |
19 | const Component = defineComponent({
20 | setup() {
21 | const storeProps = connectStore(mapStateToProps);
22 |
23 | return () => {
24 | const {prefixPathname, curRender} = storeProps;
25 | return (
26 |
27 |
28 |
29 |
30 |
31 | );
32 | };
33 | },
34 | });
35 |
36 | export default Component;
37 |
--------------------------------------------------------------------------------
/mock/start.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-fallthrough, no-console */
2 | 'use strict';
3 | var __importDefault =
4 | (this && this.__importDefault) ||
5 | function (mod) {
6 | return mod && mod.__esModule ? mod : {default: mod};
7 | };
8 | Object.defineProperty(exports, '__esModule', {value: true});
9 | const http_1 = __importDefault(require('http'));
10 |
11 | const port = 3003;
12 | const app = require('./src');
13 | const server = http_1.default.createServer(app);
14 | app.set('port', port);
15 | server.listen(port);
16 | server.on('error', (error) => {
17 | if (error.syscall !== 'listen') {
18 | throw error;
19 | }
20 | const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;
21 | switch (error.code) {
22 | case 'EACCES':
23 | console.error(`${bind} requires elevated privileges`);
24 | process.exit(1);
25 | case 'EADDRINUSE':
26 | console.error(`${bind} is already in use`);
27 | process.exit(1);
28 | default:
29 | throw error;
30 | }
31 | });
32 | server.on('listening', () => {
33 | console.log(`\n....running at http://localhost:${port}/`);
34 | });
35 | ['SIGINT', 'SIGTERM'].forEach((signal) => {
36 | process.on(signal, () => {
37 | process.exit(1);
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/mock/src/index.ts:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import express, {Request, Response} from 'express';
3 | import logger from 'morgan';
4 | import articleRouter from './routes/article';
5 | import memberRouter from './routes/member';
6 | import sessionRouter from './routes/session';
7 |
8 | const app = express();
9 |
10 | app.use(
11 | logger('dev', {
12 | skip(req: Request, res: Response) {
13 | const contentType = res.getHeader('Content-Type')?.toString() || '';
14 | return !contentType || contentType.indexOf('application/json') < 0;
15 | },
16 | })
17 | );
18 |
19 | const allowCrossDomain = function (req: Request, res: Response, next: (err?: Error) => void) {
20 | res.header('Access-Control-Allow-Origin', '*');
21 | res.header('Access-Control-Allow-Methods', '*');
22 | res.header('Access-Control-Allow-Headers', '*');
23 | if (req.method == 'OPTIONS') {
24 | res.sendStatus(200);
25 | } else {
26 | next();
27 | }
28 | };
29 | app.use(allowCrossDomain);
30 |
31 | app.use(express.json());
32 | app.use(express.urlencoded({extended: false}));
33 | app.use(express.static(path.join(__dirname, '../public')));
34 |
35 | app.use('/session', sessionRouter);
36 | app.use('/member', memberRouter);
37 | app.use('/article', articleRouter);
38 |
39 | export = app;
40 |
--------------------------------------------------------------------------------
/elux.config.js:
--------------------------------------------------------------------------------
1 | //工程配置文件,参见 https://eluxjs.com/guide/configure.html
2 | // eslint-disable-next-line import/no-extraneous-dependencies
3 | const antdVars = require('@elux-admin-antd/stage/assets/css/antd-var.json');
4 | const {getLocalIP} = require('@elux/cli-utils');
5 | const serverPort = 4003;
6 | const apiHosts = {
7 | local: `http://${getLocalIP()}:3003/`,
8 | demo: 'http://api-admin-demo.eluxjs.com/',
9 | };
10 | const APP_ENV = process.env.APP_ENV || 'local';
11 | module.exports = {
12 | type: 'vue',
13 | mockServer: {port: 3003},
14 | cssProcessors: {less: {javascriptEnabled: true, modifyVars: antdVars}},
15 | all: {
16 | //开发和生成环境都使用的配置
17 | serverPort,
18 | clientGlobalVar: {
19 | ApiPrefix: apiHosts[APP_ENV],
20 | StaticPrefix: apiHosts[APP_ENV],
21 | },
22 | webpackConfigTransform: (config) => {
23 | //此处可以自定义webpack配置
24 | //console.log(config.module.rules[0]);
25 | return config;
26 | },
27 | },
28 | dev: {
29 | //开发环境专用配置
30 | eslint: false,
31 | stylelint: false,
32 | //要使用开发代理可以放开下面代码
33 | // apiProxy: {
34 | // '/api': {
35 | // target: apiHosts[APP_ENV],
36 | // pathRewrite: {
37 | // '^/api': '',
38 | // },
39 | // },
40 | // },
41 | },
42 | };
43 |
--------------------------------------------------------------------------------
/src/modules/stage/components/MSearch/index.module.less:
--------------------------------------------------------------------------------
1 | @import '../../assets/css/var.less';
2 |
3 | :global {
4 | :local(.root) {
5 | padding: 15px 0 10px !important;
6 | border-bottom: 1px solid @border-color-split;
7 |
8 | .ant-form {
9 | display: flex;
10 | flex-flow: row wrap;
11 |
12 | > div {
13 | display: flex;
14 | padding-right: 1%;
15 | padding-left: 1%;
16 | margin: 0;
17 | margin-bottom: 15px;
18 | }
19 | }
20 |
21 | > .col4 {
22 | > div {
23 | width: 25%;
24 | }
25 | }
26 |
27 | > .col3 {
28 | > div {
29 | width: 33.3%;
30 | }
31 | }
32 |
33 | .ant-form-item-label {
34 | flex: none;
35 |
36 | .label {
37 | display: inline-block;
38 | }
39 | }
40 |
41 | .ant-form-item-control {
42 | flex: auto;
43 | }
44 |
45 | .ant-calendar-picker {
46 | width: 100% !important;
47 | }
48 |
49 | .form-btns {
50 | flex: 1;
51 | align-items: center;
52 | justify-content: flex-end;
53 |
54 | > * {
55 | margin-left: 10px;
56 | }
57 |
58 | .expand {
59 | display: block;
60 | padding: 5px;
61 | font-size: 12px;
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/modules/stage/entity.ts:
--------------------------------------------------------------------------------
1 | import {IRequest} from '@elux-admin-antd/stage/utils/base';
2 |
3 | export interface CurUser {
4 | id: string;
5 | username: string;
6 | avatar: string;
7 | mobile: string;
8 | hasLogin: boolean;
9 | }
10 |
11 | export interface LoginParams {
12 | username: string;
13 | password: string;
14 | keep: boolean;
15 | }
16 |
17 | export interface RegisterParams {
18 | username: string;
19 | password: string;
20 | }
21 |
22 | export interface SendCaptchaParams {
23 | phone: string;
24 | }
25 |
26 | export interface ResetPasswordParams {
27 | phone: string;
28 | password: string;
29 | captcha: string;
30 | }
31 |
32 | export type IGetCurUser = IRequest<{}, CurUser>;
33 |
34 | export type ILogin = IRequest;
35 |
36 | export type ILogout = IRequest<{}, CurUser>;
37 |
38 | export type IRegistry = IRequest;
39 |
40 | export type ISendCaptcha = IRequest;
41 |
42 | export type IResetPassword = IRequest;
43 |
44 | export enum SubModule {
45 | 'shop' = 'shop',
46 | 'admin' = 'admin',
47 | }
48 |
49 | export enum CurView {
50 | 'login' = 'login',
51 | 'registry' = 'registry',
52 | 'agreement' = 'agreement',
53 | 'forgetPassword' = 'forgetPassword',
54 | }
55 |
--------------------------------------------------------------------------------
/src/modules/stage/utils/request.ts:
--------------------------------------------------------------------------------
1 | import axios, {AxiosError, AxiosResponse} from 'axios';
2 | import {ApiPrefix} from '@/Global';
3 | import {CommonErrorCode, CustomError} from './errors';
4 |
5 | function mapHttpErrorCode(code: string): CommonErrorCode {
6 | const HttpErrorCode = {
7 | '401': CommonErrorCode.unauthorized,
8 | '403': CommonErrorCode.forbidden,
9 | '404': CommonErrorCode.notFound,
10 | };
11 | return HttpErrorCode[code] || CommonErrorCode.unkown;
12 | }
13 | const instance = axios.create({timeout: 15000});
14 |
15 | instance.interceptors.request.use((req) => {
16 | const token = localStorage.getItem('token');
17 | return {...req, headers: {Authorization: token}, url: req.url!.replace('/api/', ApiPrefix)};
18 | });
19 |
20 | instance.interceptors.response.use(
21 | (response: AxiosResponse) => {
22 | return response;
23 | },
24 | (error: AxiosError<{message: string}>) => {
25 | const httpErrorCode = error.response ? error.response.status : 0;
26 | const statusText = error.response ? error.response.statusText : '';
27 | const responseData: any = error.response ? error.response.data : '';
28 | const errorMessage = responseData.message || `${statusText}, failed to call ${error.config.url}`;
29 | throw new CustomError(mapHttpErrorCode(httpErrorCode.toString()), errorMessage, responseData);
30 | }
31 | );
32 |
33 | export default instance;
34 |
--------------------------------------------------------------------------------
/src/Project.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import stage from '@elux-admin-antd/stage';
3 | import {AdminHomeUrl} from '@elux-admin-antd/stage/utils/const';
4 | import {AppConfig, setConfig} from '@elux/vue-web';
5 | import {parse, stringify} from 'query-string';
6 |
7 | //定义模块的获取方式,同步或者异步都可以, 注意key名必需和模块名保持一致
8 | //配置成异步import(...)可以按需加载
9 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
10 | export const ModuleGetter = {
11 | //通常stage为根模块,使用同步加载,如果根模块要用别的名字,需要同时在以下setConfig中设置
12 | stage: () => stage,
13 | admin: () => import('@elux-admin-antd/admin'),
14 | dashboard: () => import('@elux-admin-antd/dashboard'),
15 | member: () => import('@elux-admin-antd/member'),
16 | article: () => import('@elux-admin-antd/article'),
17 | };
18 |
19 | //Elux全局设置,参见 https://eluxjs.com/api/react-web.setconfig.html
20 | export const appConfig: AppConfig = setConfig({
21 | ModuleGetter,
22 | //Elux并没定死怎么解析路由参数,你可以使用常用的'query-string'或者'json'
23 | //只需要将parse(解析)和stringify(序列化)方法设置给Elux
24 | QueryString: {parse, stringify},
25 | //elux内部使用的虚拟路由是独立自主的,但可以映射到原生路由
26 | NativePathnameMapping: {
27 | in(nativePathname) {
28 | if (nativePathname === '/') {
29 | nativePathname = AdminHomeUrl;
30 | }
31 | return nativePathname;
32 | },
33 | out(internalPathname) {
34 | return internalPathname;
35 | },
36 | },
37 | });
38 |
39 | export type IModuleGetter = typeof ModuleGetter;
40 |
--------------------------------------------------------------------------------
/src/modules/article/views/Main.tsx:
--------------------------------------------------------------------------------
1 | import ErrorPage from '@elux-admin-antd/stage/components/ErrorPage';
2 | import {Switch, connectStore, exportView} from '@elux/vue-web';
3 | import {defineComponent} from 'vue';
4 | import {APPState} from '@/Global';
5 | import {CurRender, CurView, ItemDetail} from '../entity';
6 | import Detail from './Detail';
7 | import Edit from './Edit';
8 | import Index from './Index';
9 | import Maintain from './Maintain';
10 |
11 | export interface StoreProps {
12 | curView?: CurView;
13 | curRender?: CurRender;
14 | itemDetail?: ItemDetail;
15 | }
16 |
17 | function mapStateToProps(appState: APPState): StoreProps {
18 | const {curView, curRender, itemDetail} = appState.article!;
19 | return {curView, curRender, itemDetail};
20 | }
21 |
22 | const Component = defineComponent({
23 | setup() {
24 | const storeProps = connectStore(mapStateToProps);
25 |
26 | return () => {
27 | const {curView, curRender, itemDetail, dispatch} = storeProps;
28 | return (
29 | }>
30 | {curView === 'list' && curRender === 'maintain' && }
31 | {curView === 'list' && curRender === 'index' && }
32 | {curView === 'item' && curRender === 'detail' && }
33 | {curView === 'item' && curRender === 'edit' && }
34 |
35 | );
36 | };
37 | },
38 | });
39 |
40 | export default exportView(Component);
41 |
--------------------------------------------------------------------------------
/src/modules/member/views/Main.tsx:
--------------------------------------------------------------------------------
1 | import ErrorPage from '@elux-admin-antd/stage/components/ErrorPage';
2 | import {Switch, connectStore, exportView} from '@elux/vue-web';
3 | import {defineComponent} from 'vue';
4 | import {APPState} from '@/Global';
5 | import {CurRender, CurView, ItemDetail} from '../entity';
6 | import Detail from './Detail';
7 | import Edit from './Edit';
8 | import Maintain from './Maintain';
9 | import Selector from './Selector';
10 |
11 | export interface StoreProps {
12 | curView?: CurView;
13 | curRender?: CurRender;
14 | itemDetail?: ItemDetail;
15 | }
16 |
17 | function mapStateToProps(appState: APPState): StoreProps {
18 | const {curView, curRender, itemDetail} = appState.member!;
19 | return {curView, curRender, itemDetail};
20 | }
21 |
22 | const Component = defineComponent({
23 | setup() {
24 | const storeProps = connectStore(mapStateToProps);
25 |
26 | return () => {
27 | const {curView, curRender, itemDetail, dispatch} = storeProps;
28 | return (
29 | }>
30 | {curView === 'list' && curRender === 'maintain' && }
31 | {curView === 'list' && curRender === 'selector' && }
32 | {curView === 'item' && curRender === 'detail' && }
33 | {curView === 'item' && curRender === 'edit' && }
34 |
35 | );
36 | };
37 | },
38 | });
39 |
40 | export default exportView(Component);
41 |
--------------------------------------------------------------------------------
/src/modules/member/views/SearchForm.tsx:
--------------------------------------------------------------------------------
1 | import MSearch from '@elux-admin-antd/stage/components/MSearch';
2 | import {useSearch} from '@elux-admin-antd/stage/utils/resource';
3 | import {SearchFromItems} from '@elux-admin-antd/stage/utils/tools';
4 | import {Input, Select} from 'ant-design-vue';
5 | import {defineComponent} from 'vue';
6 | import {DRole, DStatus, ListSearch, ListSearchFormData, defaultListSearch} from '../entity';
7 |
8 | const formItems: SearchFromItems = [
9 | {name: 'name', label: '用户名', formItem: },
10 | {name: 'nickname', label: '呢称', formItem: },
11 | {
12 | name: 'status',
13 | label: '状态',
14 | formItem: ,
15 | },
16 | {
17 | name: 'role',
18 | label: '角色',
19 | formItem: ,
20 | },
21 | {
22 | name: 'email',
23 | label: 'Email',
24 | formItem: ,
25 | },
26 | ];
27 |
28 | interface Props {
29 | listPathname: string;
30 | listSearch: ListSearch;
31 | fixedFields?: Partial;
32 | }
33 |
34 | const Component = defineComponent({
35 | props: ['listPathname', 'listSearch', 'fixedFields'] as any,
36 | setup(props) {
37 | const {onSearch} = useSearch(props, defaultListSearch);
38 |
39 | return () => {
40 | const {listSearch, fixedFields} = props;
41 | return (
42 |
43 | values={listSearch}
44 | fixedFields={fixedFields}
45 | expand={!!listSearch.email}
46 | items={formItems}
47 | onSearch={onSearch}
48 | />
49 | );
50 | };
51 | },
52 | });
53 |
54 | export default Component;
55 |
--------------------------------------------------------------------------------
/src/modules/stage/api.ts:
--------------------------------------------------------------------------------
1 | import request from '@elux-admin-antd/stage/utils/request';
2 | import {CurUser, IGetCurUser, ILogin, ILogout, IRegistry, IResetPassword, ISendCaptcha} from './entity';
3 |
4 | export const guest: CurUser = {
5 | id: '',
6 | username: '游客',
7 | hasLogin: false,
8 | avatar: '',
9 | mobile: '',
10 | };
11 |
12 | class API {
13 | public getCurUser(): Promise {
14 | return request
15 | .get('/api/session')
16 | .then((res) => {
17 | return res.data;
18 | })
19 | .catch(() => {
20 | return guest;
21 | });
22 | }
23 | public login(params: ILogin['Request']): Promise {
24 | return request.put('/api/session', params).then((res) => {
25 | localStorage.setItem('token', res.data.id + res.data.username);
26 | return res.data;
27 | });
28 | }
29 |
30 | public logout(): Promise {
31 | return request.delete('/api/session').then((res) => {
32 | localStorage.removeItem('token');
33 | return res.data;
34 | });
35 | }
36 |
37 | public registry(params: IRegistry['Request']): Promise {
38 | return request.post('/api/session', params).then((res) => {
39 | localStorage.setItem('token', res.data.id + res.data.username);
40 | return res.data;
41 | });
42 | }
43 |
44 | public resetPassword(params: IResetPassword['Request']): Promise {
45 | return request.put('/api/session/resetPassword', params).then((res) => {
46 | return res.data;
47 | });
48 | }
49 |
50 | public sendCaptcha(params: ISendCaptcha['Request']): Promise {
51 | return request.post('/api/session/sendCaptcha', params).then((res) => {
52 | return res.data;
53 | });
54 | }
55 | }
56 |
57 | export default new API();
58 |
--------------------------------------------------------------------------------
/src/modules/member/views/Selector.tsx:
--------------------------------------------------------------------------------
1 | import DialogPage from '@elux-admin-antd/stage/components/DialogPage';
2 | import {MColumns} from '@elux-admin-antd/stage/components/MTable';
3 | import {connectStore} from '@elux/vue-web';
4 | import {defineComponent} from 'vue';
5 | import {APPState, useRouter} from '@/Global';
6 | import {ListItem, ListSearch, LocationState} from '../entity';
7 | import ListTable from './ListTable';
8 | import SearchForm from './SearchForm';
9 |
10 | interface StoreProps {
11 | prefixPathname: string;
12 | listSearch: ListSearch;
13 | curRender?: string;
14 | }
15 |
16 | const mapStateToProps: (state: APPState) => StoreProps = (state) => {
17 | const {prefixPathname, curRender, listSearch} = state.member!;
18 | return {prefixPathname, curRender, listSearch};
19 | };
20 |
21 | const mergeColumns: {[field: string]: MColumns} = {
22 | articles: {disable: true},
23 | };
24 |
25 | const Component = defineComponent({
26 | setup() {
27 | const router = useRouter();
28 | const {selectLimit, showSearch, fixedSearch, selectedRows} = (router.location.state || {}) as LocationState;
29 | const storeProps = connectStore(mapStateToProps);
30 |
31 | return () => {
32 | const {listSearch, prefixPathname, curRender} = storeProps;
33 | return (
34 |
35 |
36 | {showSearch && }
37 |
43 |
44 |
45 | );
46 | };
47 | },
48 | });
49 |
50 | export default Component;
51 |
--------------------------------------------------------------------------------
/src/modules/member/api.ts:
--------------------------------------------------------------------------------
1 | import {BaseApi} from '@elux-admin-antd/stage/utils/base';
2 | import request from '@elux-admin-antd/stage/utils/request';
3 | import {IAlterItems, ICreateItem, IDeleteItems, IGetItem, IGetList, IUpdateItem} from './entity';
4 |
5 | export class API implements BaseApi {
6 | public getList(params: IGetList['Request']): Promise {
7 | return request.get('/api/member', {params}).then((res) => {
8 | return res.data;
9 | });
10 | }
11 |
12 | public getItem(params: IGetItem['Request']): Promise {
13 | if (!params.id) {
14 | return Promise.resolve({} as any);
15 | }
16 | return request.get(`/api/member/${params.id}`).then((res) => {
17 | return res.data;
18 | });
19 | }
20 |
21 | public alterItems(params: IAlterItems['Request']): Promise {
22 | const {id, data} = params;
23 | const ids = typeof id === 'string' ? [id] : id;
24 | return request.put(`/api/member/${ids.join(',')}`, data).then((res) => {
25 | return res.data;
26 | });
27 | }
28 |
29 | public updateItem(params: IUpdateItem['Request']): Promise {
30 | const {id, data} = params;
31 | return request.put(`/api/member/${id}`, data).then((res) => {
32 | return res.data;
33 | });
34 | }
35 |
36 | public deleteItems(params: IDeleteItems['Request']): Promise {
37 | const {id} = params;
38 | const ids = typeof id === 'string' ? [id] : id;
39 | return request.delete(`/api/member/${ids.join(',')}`).then((res) => {
40 | return res.data;
41 | });
42 | }
43 |
44 | public createItem(params: ICreateItem['Request']): Promise {
45 | return request.post<{id: string}>(`/api/member`, params).then((res) => {
46 | return res.data;
47 | });
48 | }
49 | }
50 |
51 | export default new API();
52 |
--------------------------------------------------------------------------------
/src/modules/article/api.ts:
--------------------------------------------------------------------------------
1 | import {BaseApi} from '@elux-admin-antd/stage/utils/base';
2 | import request from '@elux-admin-antd/stage/utils/request';
3 | import {IAlterItems, ICreateItem, IDeleteItems, IGetItem, IGetList, IUpdateItem} from './entity';
4 |
5 | export class API implements BaseApi {
6 | public getList(params: IGetList['Request']): Promise {
7 | return request.get('/api/article', {params}).then((res) => {
8 | return res.data;
9 | });
10 | }
11 |
12 | public getItem(params: IGetItem['Request']): Promise {
13 | if (!params.id) {
14 | return Promise.resolve({} as any);
15 | }
16 | return request.get(`/api/article/${params.id}`).then((res) => {
17 | return res.data;
18 | });
19 | }
20 |
21 | public alterItems(params: IAlterItems['Request']): Promise {
22 | const {id, data} = params;
23 | const ids = typeof id === 'string' ? [id] : id;
24 | return request.put(`/api/article/${ids.join(',')}`, data).then((res) => {
25 | return res.data;
26 | });
27 | }
28 |
29 | public updateItem(params: IUpdateItem['Request']): Promise {
30 | const {id, data} = params;
31 | return request.put(`/api/article/${id}`, data).then((res) => {
32 | return res.data;
33 | });
34 | }
35 |
36 | public deleteItems(params: IDeleteItems['Request']): Promise {
37 | const {id} = params;
38 | const ids = typeof id === 'string' ? [id] : id;
39 | return request.delete(`/api/article/${ids.join(',')}`).then((res) => {
40 | return res.data;
41 | });
42 | }
43 |
44 | public createItem(params: ICreateItem['Request']): Promise {
45 | return request.post(`/api/article`, params).then((res) => {
46 | return res.data;
47 | });
48 | }
49 | }
50 |
51 | export default new API();
52 |
--------------------------------------------------------------------------------
/src/modules/article/views/SearchForm.tsx:
--------------------------------------------------------------------------------
1 | import {ListSearch as MemberListSearch, Role, Status} from '@elux-admin-antd/member/entity';
2 | import MSearch from '@elux-admin-antd/stage/components/MSearch';
3 | import MSelect from '@elux-admin-antd/stage/components/MSelect';
4 | import {useSearch} from '@elux-admin-antd/stage/utils/resource';
5 | import {SearchFromItems} from '@elux-admin-antd/stage/utils/tools';
6 | import {Input, Select} from 'ant-design-vue';
7 | import {defineComponent} from 'vue';
8 | import {DStatus, ListSearch, ListSearchFormData, defaultListSearch} from '../entity';
9 |
10 | interface Props {
11 | listPathname: string;
12 | listSearch: ListSearch;
13 | }
14 |
15 | const formItems: SearchFromItems = [
16 | {name: 'title', label: '标题', formItem: },
17 | {
18 | name: 'author',
19 | label: '作者',
20 | formItem: placeholder="请选择作者" selectorPathname="/admin/member/list/selector" limit={1} showSearch> ,
21 | },
22 | {
23 | name: 'editor',
24 | label: '责任编辑',
25 | formItem: (
26 |
27 | placeholder="请选择责任编辑"
28 | selectorPathname="/admin/member/list/selector"
29 | fixedSearch={{role: Role.责任编辑, status: Status.启用}}
30 | limit={1}
31 | showSearch
32 | >
33 | ),
34 | },
35 | {
36 | name: 'status',
37 | label: '状态',
38 | formItem: ,
39 | },
40 | ];
41 |
42 | const Component = defineComponent({
43 | props: ['listPathname', 'listSearch'] as any,
44 | setup(props) {
45 | const {onSearch} = useSearch(props, defaultListSearch);
46 |
47 | return () => {
48 | const {listSearch} = props;
49 | return values={listSearch} items={formItems} onSearch={onSearch} />;
50 | };
51 | },
52 | });
53 |
54 | export default Component;
55 |
--------------------------------------------------------------------------------
/src/modules/member/views/Detail.tsx:
--------------------------------------------------------------------------------
1 | import DateTime from '@elux-admin-antd/stage/components/DateTime';
2 | import DialogPage from '@elux-admin-antd/stage/components/DialogPage';
3 | import {Link} from '@elux/vue-web';
4 | import {Descriptions, Skeleton} from 'ant-design-vue';
5 | import {DGender, DRole, DStatus, ItemDetail} from '../entity';
6 |
7 | const DescriptionsItem = Descriptions.Item;
8 |
9 | interface Props {
10 | itemDetail?: ItemDetail;
11 | }
12 |
13 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
14 | const Component = ({itemDetail}: Props) => {
15 | return (
16 |
17 |
18 | {itemDetail ? (
19 |
20 | {itemDetail.name}
21 | {itemDetail.nickname}
22 | {DGender.valueToLabel[itemDetail.gender]}
23 | {DRole.valueToLabel[itemDetail.role]}
24 |
25 |
26 | {itemDetail.articles}
27 |
28 |
29 |
30 | {DStatus.valueToLabel[itemDetail.status]}
31 |
32 | {itemDetail.email}
33 |
34 |
35 |
36 |
37 | ) : (
38 |
39 | )}
40 |
41 |
42 | );
43 | };
44 |
45 | export default Component;
46 |
--------------------------------------------------------------------------------
/src/modules/stage/views/Agreement/index.tsx:
--------------------------------------------------------------------------------
1 | import {Link} from '@elux/vue-web';
2 | import {Button} from 'ant-design-vue';
3 | import DialogPage from '../../components/DialogPage';
4 | import styles from './index.module.less';
5 |
6 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
7 | const Component = () => {
8 | return (
9 |
10 |
11 |
12 | 您在使用百度公司提供的各项服务之前,请您务必审慎阅读、充分理解本协议各条款内容,特别是以粗体标注的部分,包括但不限于免除或者限制责任的条款。如您不同意本服务协议及/或随时对其的修改,您可以主动停止使用百度公司提供的服务;您一旦使用百度公司提供的服务,即视为您已了解并完全同意本服务协议各项内容,包括百度公司对服务协议随时所做的任何修改,并成为我们的用户。
13 | 您使用部分百度产品或服务时需要注册百度帐号,当您注册和使用百度帐号时应遵守下述要求:
14 | 1.
15 | 用户注册成功后,百度公司将给予每个用户一个用户帐号,用户可以自主设置帐号密码。该用户帐号和密码由用户负责保管;用户应当对以其用户帐号进行的所有活动和事件负法律责任。
16 |
17 | 2.
18 | 您按照注册页面提示填写信息、阅读并同意本协议且完成全部注册程序后,除百度平台的具体产品对帐户有单独的注册要求外,您可获得百度平台(baidu.com网站及客户端)帐号并成为百度用户,可通过百度帐户使用百度平台的各项产品和服务。
19 |
20 | 3.
21 | 为了方便您在百度产品中享有一致性的服务,如您已经在某一百度产品中登录百度帐号,在您首次使用其他百度产品时可能同步您的登录状态。此环节并不会额外收集、使用您的个人信息。如您想退出帐号登录,可在产品设置页面退出登录。
22 |
23 | 4.
24 | 百度帐号(即百度用户ID)的所有权归百度公司,用户按注册页面引导填写信息,阅读并同意本协议且完成全部注册程序后,即可获得百度帐号并成为用户。用户应提供及时、详尽及准确的个人资料,并不断更新注册资料,符合及时、详尽准确的要求。所有原始键入的资料将引用为注册资料。如果因注册信息不真实或更新不及时而引发的相关问题,百度公司不负任何责任。您可以通过百度帐号设置页面查询、更正您的信息,百度帐号设置页面地址:
25 | 5.
26 | 百度帐号(即百度用户ID)的所有权归百度公司,用户按注册页面引导填写信息,阅读并同意本协议且完成全部注册程序后,即可获得百度帐号并成为用户。用户应提供及时、详尽及准确的个人资料,并不断更新注册资料,符合及时、详尽准确的要求。所有原始键入的资料将引用为注册资料。如果因注册信息不真实或更新不及时而引发的相关问题,百度公司不负任何责任。您可以通过百度帐号设置页面查询、更正您的信息,百度帐号设置页面地址:
27 |
28 |
35 |
36 |
37 | );
38 | };
39 |
40 | Component.displayName = styles.root;
41 |
42 | export default Component;
43 |
--------------------------------------------------------------------------------
/src/modules/stage/views/Main.tsx:
--------------------------------------------------------------------------------
1 | import 'ant-design-vue/dist/antd.less';
2 | import '../assets/css/var.less';
3 | import '../assets/css/global.module.less';
4 | import {DocumentHead, LoadingState, Switch, connectStore, exportView} from '@elux/vue-web';
5 | import {ConfigProvider} from 'ant-design-vue';
6 | import zhCN from 'ant-design-vue/es/locale/zh_CN';
7 | import {defineComponent} from 'vue';
8 | import {APPState, LoadComponent} from '@/Global';
9 | import ErrorPage from '../components/ErrorPage';
10 | import LoadingPanel from '../components/LoadingPanel';
11 | import {CurView, SubModule} from '../entity';
12 | import Agreement from './Agreement';
13 | import ForgotPassword from './ForgotPassword';
14 | import LoginForm from './LoginForm';
15 | import RegistryForm from './RegistryForm';
16 |
17 | const Admin = LoadComponent('admin', 'main');
18 |
19 | export interface StoreProps {
20 | subModule?: SubModule;
21 | curView?: CurView;
22 | globalLoading?: LoadingState;
23 | error?: string;
24 | }
25 |
26 | function mapStateToProps(appState: APPState): StoreProps {
27 | const {subModule, curView, globalLoading, error} = appState.stage!;
28 | return {
29 | subModule,
30 | curView,
31 | globalLoading,
32 | error,
33 | };
34 | }
35 |
36 | const Component = defineComponent({
37 | setup() {
38 | const storeProps = connectStore(mapStateToProps);
39 |
40 | return () => {
41 | const {curView, globalLoading, error, subModule} = storeProps;
42 | return (
43 |
44 |
45 | }>
46 | {!!error && }
47 | {subModule === 'admin' && }
48 | {curView === 'login' && }
49 | {curView === 'registry' && }
50 | {curView === 'agreement' && }
51 | {curView === 'forgetPassword' && }
52 |
53 |
54 |
55 | );
56 | };
57 | },
58 | });
59 |
60 | export default exportView(Component);
61 |
--------------------------------------------------------------------------------
/src/modules/admin/api.ts:
--------------------------------------------------------------------------------
1 | import {FavoritesUrlStorageKey} from '@elux-admin-antd/stage/utils/const';
2 | import request from '@elux-admin-antd/stage/utils/request';
3 | import {arrayToMap} from '@elux-admin-antd/stage/utils/tools';
4 | import {IGetMenu, IGetNotices, MenuData, MenuItem, Tab, TabData} from './entity';
5 |
6 | function memoMenuData(items: MenuItem[]): MenuData {
7 | const keyToParents: {[key: string]: string[]} = {};
8 | const matchToKey: {[match: string]: string} = {};
9 | const keyToLink: {[key: string]: string} = {};
10 | const checkData = (item: MenuItem, parentKey?: string) => {
11 | const {key, match, link} = item;
12 | if (keyToParents[key]) {
13 | throw `Menu ${key} 重复!`;
14 | }
15 | if (link) {
16 | keyToLink[key] = link;
17 | }
18 | const parents = [];
19 | if (parentKey) {
20 | parents.push(parentKey, ...keyToParents[parentKey]);
21 | }
22 | keyToParents[key] = parents;
23 | if (match) {
24 | const arr = typeof match === 'string' ? [match] : match;
25 | arr.forEach((rule) => {
26 | matchToKey[rule] = key;
27 | });
28 | }
29 | if (item.children) {
30 | item.children.forEach((subItem) => checkData(subItem, key));
31 | }
32 | };
33 | items.forEach((subItem) => checkData(subItem));
34 | return {items, keyToLink, keyToParents, matchToKey};
35 | }
36 |
37 | class API {
38 | public getNotices(): Promise {
39 | return request.get('/api/session/notices').then((res) => {
40 | return res.data;
41 | });
42 | }
43 | public getMenuData(): Promise {
44 | return request.get('/api/session/menu').then((res) => {
45 | return memoMenuData(res.data);
46 | });
47 | }
48 | public getTabData(): Promise {
49 | const list: Tab[] = JSON.parse(localStorage.getItem(FavoritesUrlStorageKey) || '[]');
50 | const maps = arrayToMap(list);
51 | return Promise.resolve({list, maps});
52 | }
53 | public updateTabData(tabData: TabData): Promise {
54 | localStorage.setItem(FavoritesUrlStorageKey, JSON.stringify(tabData.list));
55 | return Promise.resolve();
56 | }
57 | }
58 |
59 | export default new API();
60 |
--------------------------------------------------------------------------------
/src/modules/admin/views/Tabs/index.tsx:
--------------------------------------------------------------------------------
1 | import {PlusOutlined, ReloadOutlined} from '@ant-design/icons-vue';
2 | import {Link, connectStore} from '@elux/vue-web';
3 | import {Tabs} from 'ant-design-vue';
4 | import {defineComponent} from 'vue';
5 | import {APPState, GetActions} from '@/Global';
6 | import {TabData} from '../../entity';
7 | import Editor from './Editor';
8 | import styles from './index.module.less';
9 |
10 | const {TabPane} = Tabs;
11 |
12 | const {admin: adminActions} = GetActions('admin');
13 |
14 | export interface StoreProps {
15 | tabData: TabData;
16 | tabSelected: string;
17 | }
18 |
19 | function mapStateToProps(appState: APPState): StoreProps {
20 | const {tabData, curTab} = appState.admin!;
21 | return {
22 | tabData,
23 | tabSelected: curTab.id,
24 | };
25 | }
26 |
27 | const tabSlots = {
28 | addIcon: () => (
29 |
30 |
31 | 收藏
32 |
33 | ),
34 | rightExtra: () => (
35 |
36 |
37 |
38 | ),
39 | };
40 |
41 | const Component = defineComponent({
42 | setup() {
43 | const storeProps = connectStore(mapStateToProps);
44 |
45 | const onTabClick = (key: string | number) => {
46 | storeProps.dispatch(adminActions.clickTab(key as string));
47 | };
48 |
49 | const onTabEdit = (key: any, action: 'add' | 'remove') => {
50 | if (action === 'add') {
51 | storeProps.dispatch(adminActions.clickTab(''));
52 | } else {
53 | storeProps.dispatch(adminActions.deleteTab(key));
54 | }
55 | };
56 |
57 | return () => {
58 | const {tabData, tabSelected} = storeProps;
59 | return (
60 | <>
61 |
70 | {tabData.list.map((item) => (
71 |
72 | ))}
73 |
74 |
75 | >
76 | );
77 | };
78 | },
79 | });
80 |
81 | export default Component;
82 |
--------------------------------------------------------------------------------
/src/modules/stage/assets/imgs/logo-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
30 |
--------------------------------------------------------------------------------
/src/modules/stage/assets/css/global.module.less:
--------------------------------------------------------------------------------
1 | @import './var.less';
2 |
3 | :global {
4 | .elux-window._dialog {
5 | > :local(.component-loading),
6 | > :local(.component-error) {
7 | width: 400px !important;
8 | height: 200px !important;
9 | }
10 | }
11 |
12 | .ant-descriptions-view .ant-descriptions-item-label {
13 | color: #666;
14 | white-space: nowrap;
15 | }
16 |
17 | :local(.component-loading) {
18 | position: relative;
19 | width: 100%;
20 | height: 100%;
21 | font-size: 0;
22 |
23 | &::before {
24 | position: absolute;
25 | top: 50%;
26 | left: 50%;
27 | width: 24px;
28 | height: 24px;
29 | margin-top: -12px;
30 | margin-left: -12px;
31 | content: '';
32 | background: url('../imgs/loading48x48.gif') no-repeat;
33 | background-size: 24px 24px;
34 | }
35 | }
36 |
37 | :local(.component-error) {
38 | position: relative;
39 | width: 100%;
40 | height: 100%;
41 | padding: 10px;
42 | font-size: 12px;
43 | color: red;
44 | }
45 |
46 | :local(.dialog-content) {
47 | margin: 20px 25px 25px;
48 | }
49 |
50 | :local(.page-content) {
51 | padding: 30px 30px 50px;
52 | }
53 |
54 | :local(.actions) {
55 | > * {
56 | display: inline-block;
57 | padding-right: 10px;
58 | margin-right: 10px;
59 | line-height: 1;
60 | border-right: 1px solid #ddd;
61 |
62 | &:last-child {
63 | margin-right: 0;
64 | border: none;
65 | }
66 | }
67 | }
68 |
69 | :local(.table-actions) {
70 | > * {
71 | margin: 0 5px;
72 | }
73 | }
74 |
75 | :local(.form-actions) {
76 | text-align: center;
77 |
78 | > * {
79 | margin: 0 6px;
80 | }
81 | }
82 |
83 | :local(.control) {
84 | display: flex;
85 | justify-content: space-between;
86 |
87 | .ant-btn {
88 | width: 100%;
89 | padding-right: 0;
90 | padding-left: 0;
91 | }
92 |
93 | > * {
94 | flex: 1;
95 | width: 100%;
96 | margin-left: 10px;
97 |
98 | &:first-child {
99 | margin-left: 0;
100 | }
101 | }
102 | }
103 |
104 | :local(.disable) {
105 | color: @warning-color;
106 | }
107 |
108 | :local(.enable) {
109 | color: @success-color;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/modules/article/entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BaseCurRender,
3 | BaseCurView,
4 | BaseListItem,
5 | BaseListSearch,
6 | BaseListSummary,
7 | BaseModuleState,
8 | BaseRouteParams,
9 | DefineResource,
10 | IRequest,
11 | enumOptions,
12 | } from '@elux-admin-antd/stage/utils/base';
13 |
14 | export enum Status {
15 | '待审核' = 'pending',
16 | '审核通过' = 'resolved',
17 | '审核拒绝' = 'rejected',
18 | }
19 | export const DStatus = enumOptions(Status);
20 |
21 | export type CurView = BaseCurView;
22 | export type CurRender = BaseCurRender;
23 |
24 | export interface ListSearch extends BaseListSearch {
25 | title?: string;
26 | author?: string;
27 | editor?: string;
28 | status?: Status;
29 | }
30 | export interface ListItem extends BaseListItem {
31 | title: string;
32 | summary: string;
33 | content: string;
34 | author: string;
35 | editors: string[];
36 | status: Status;
37 | createdTime: number;
38 | }
39 | export interface ListSummary extends BaseListSummary {}
40 | export interface ItemDetail extends ListItem {}
41 | export type UpdateItem = Pick;
42 |
43 | export type ListSearchFormData = Omit;
44 |
45 | export interface ArticleResource extends DefineResource {
46 | RouteParams: RouteParams;
47 | ModuleState: ModuleState;
48 | ListSearch: ListSearch;
49 | ListItem: ListItem;
50 | ListSummary: ListSummary;
51 | ItemDetail: ItemDetail;
52 | UpdateItem: UpdateItem;
53 | CreateItem: UpdateItem;
54 | CurView: CurView;
55 | CurRender: CurRender;
56 | }
57 |
58 | export type RouteParams = BaseRouteParams;
59 | export type ModuleState = BaseModuleState;
60 |
61 | export const defaultListSearch: ListSearch = {
62 | pageCurrent: 1,
63 | pageSize: 10,
64 | sorterOrder: undefined,
65 | sorterField: '',
66 | title: '',
67 | author: '',
68 | editor: '',
69 | status: undefined,
70 | };
71 |
72 | export type IGetList = IRequest;
73 | export type IGetItem = IRequest<{id: string}, ItemDetail>;
74 | export type IAlterItems = IRequest<{id: string | string[]; data: Partial}, void>;
75 | export type IDeleteItems = IRequest<{id: string | string[]}, void>;
76 | export type IUpdateItem = IRequest<{id: string | string[]; data: UpdateItem}, void>;
77 | export type ICreateItem = IRequest;
78 |
--------------------------------------------------------------------------------
/src/modules/admin/views/Tabs/Editor.tsx:
--------------------------------------------------------------------------------
1 | import {getFormDecorators} from '@elux-admin-antd/stage/utils/tools';
2 | import {connectStore} from '@elux/vue-web';
3 | import {Button, Form, FormInstance, Input, Modal} from 'ant-design-vue';
4 | import {defineComponent, reactive, ref, watch} from 'vue';
5 | import {APPState, GetActions} from '@/Global';
6 | import {Tab} from '../../entity';
7 |
8 | interface HFormData {
9 | title: string;
10 | }
11 |
12 | const formDecorators = getFormDecorators({
13 | title: {rules: [{required: true, message: '请输入书签名'}]},
14 | });
15 |
16 | const {admin: adminActions} = GetActions('admin');
17 |
18 | export interface StoreProps {
19 | tabEdit?: Tab;
20 | }
21 | function mapStateToProps(appState: APPState): StoreProps {
22 | const {tabEdit} = appState.admin!;
23 | return {
24 | tabEdit,
25 | };
26 | }
27 |
28 | const Component = defineComponent({
29 | setup() {
30 | const formRef = ref();
31 | const storeProps = connectStore(mapStateToProps);
32 | const formState = reactive({
33 | title: '',
34 | });
35 | watch(
36 | () => storeProps.tabEdit,
37 | (tabEdit) => {
38 | if (tabEdit) {
39 | formState.title = tabEdit.title;
40 | formRef.value && formRef.value.validate();
41 | }
42 | }
43 | );
44 | const onSubmit = (values: HFormData) => {
45 | const {title} = values;
46 | storeProps.dispatch(adminActions.updateTab({...storeProps.tabEdit!, title}));
47 | };
48 |
49 | const onCancel = () => {
50 | storeProps.dispatch(adminActions.closeTabEditor());
51 | };
52 |
53 | return () => {
54 | const {tabEdit} = storeProps;
55 | return (
56 |
57 |
59 |
60 |
61 |
62 |
63 |
66 |
67 |
68 |
69 |
70 |
71 | );
72 | };
73 | },
74 | });
75 |
76 | export default Component;
77 |
--------------------------------------------------------------------------------
/src/modules/article/views/Detail.tsx:
--------------------------------------------------------------------------------
1 | import DateTime from '@elux-admin-antd/stage/components/DateTime';
2 | import DialogPage from '@elux-admin-antd/stage/components/DialogPage';
3 | import {splitIdName} from '@elux-admin-antd/stage/utils/tools';
4 | import {Link} from '@elux/vue-web';
5 | import {Descriptions, Skeleton} from 'ant-design-vue';
6 | import {DStatus, ItemDetail} from '../entity';
7 |
8 | const DescriptionsItem = Descriptions.Item;
9 |
10 | interface Props {
11 | itemDetail?: ItemDetail;
12 | }
13 |
14 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
15 | const Component = ({itemDetail}: Props) => {
16 | let id = '',
17 | name = '';
18 | return (
19 |
20 |
21 | {itemDetail ? (
22 |
23 |
24 | {itemDetail.title}
25 |
26 |
27 | {({id, name} = splitIdName(itemDetail.author)) && (
28 |
29 | {name}
30 |
31 | )}
32 |
33 |
34 |
35 | {itemDetail.editors.map(
36 | (editor) =>
37 | ({id, name} = splitIdName(editor)) && (
38 |
39 | {name}
40 |
41 | )
42 | )}
43 |
44 |
45 | {DStatus.valueToLabel[itemDetail.status]}
46 |
47 |
48 |
49 |
50 | {itemDetail.summary}
51 |
52 |
53 | {itemDetail.content}
54 |
55 |
56 | ) : (
57 |
58 | )}
59 |
60 |
61 | );
62 | };
63 |
64 | export default Component;
65 |
--------------------------------------------------------------------------------
/public/client/imgs/logo-icon-rotate.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "elux-admin-antd",
3 | "version": "1.0.0",
4 | "description": "",
5 | "author": "hiisea ",
6 | "license": "MIT",
7 | "engines": {
8 | "node": ">=14.0.0"
9 | },
10 | "private": true,
11 | "workspaces": [
12 | "./mock",
13 | "./public",
14 | "./src/modules/*"
15 | ],
16 | "browserslist": [
17 | "chrome >= 80"
18 | ],
19 | "scripts": {
20 | "lint:type": "tsc --project ./src --noEmit --emitDeclarationOnly false",
21 | "lint:es": "cross-env NODE_ENV=production eslint --fix --cache \"**/*.{js,ts,tsx}\"",
22 | "lint:css": "cross-env NODE_ENV=production stylelint --fix --cache \"**/*.{css,less}\"",
23 | "lint:json": "prettier --write **/*.json",
24 | "recommit": "git commit --amend --no-edit",
25 | "demote": "elux demote",
26 | "dev": "elux webpack-dev",
27 | "build": "elux webpack-build",
28 | "build:localhost": "cross-env APP_ENV=localhost elux webpack-build",
29 | "build:demo": "elux webpack-build demo",
30 | "build:mock": "tsc --project ./mock",
31 | "mock": "elux mock --watch",
32 | "start": "run-p mock dev",
33 | "dist": "node ./dist/local/start.js",
34 | "demo": "run-p mock dist"
35 | },
36 | "husky": {
37 | "hooks": {
38 | "pre-commit": "lint-staged",
39 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
40 | }
41 | },
42 | "config": {
43 | "commitizen": {
44 | "path": "cz-conventional-changelog"
45 | }
46 | },
47 | "lint-staged": {
48 | "*.{js,jsx,ts,tsx,vue}": "cross-env NODE_ENV=production eslint --fix --quiet --cache",
49 | "*.{css,less}": "cross-env NODE_ENV=production stylelint --fix --quiet --cache",
50 | "*.json": "prettier --write"
51 | },
52 | "bundledDependencies": [
53 | "dayjs",
54 | "@ant-design/icons-vue"
55 | ],
56 | "dependencies": {
57 | "@elux/vue-web": "^2.6.3",
58 | "vue": "^3.2.22",
59 | "axios": "^0.21.1",
60 | "query-string": "^7.1.1",
61 | "path-to-regexp": "^6.2.0",
62 | "ant-design-vue": "^3.2.10"
63 | },
64 | "devDependencies": {
65 | "@elux/cli": "^2.5.2",
66 | "@elux/cli-utils": "^2.3.1",
67 | "@elux/cli-webpack": "^2.2.1",
68 | "@elux/cli-mock": "^2.1.0",
69 | "@elux/babel-preset": "^1.0.2",
70 | "@elux/eslint-plugin": "^1.2.2",
71 | "@elux/stylelint-config": "^1.1.1",
72 | "marked": "^4.0.18",
73 | "npm-run-all": "~4.1.5",
74 | "cross-env": "~7.0.0",
75 | "typescript": "~4.7.0",
76 | "autoprefixer": "~10.4.0",
77 | "less": "~3.12.2",
78 | "less-loader": "~7.1.0",
79 | "@commitlint/cli": "~12.1.1",
80 | "@commitlint/config-conventional": "~12.1.1",
81 | "husky": "~4.3.8",
82 | "commitizen": "~4.2.3",
83 | "cz-lerna-changelog": "~2.0.3",
84 | "lint-staged": "~10.5.4"
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/modules/admin/views/Menu/index.tsx:
--------------------------------------------------------------------------------
1 | import {DashboardOutlined, ProfileOutlined, TeamOutlined} from '@ant-design/icons-vue';
2 | import {CurUser} from '@elux-admin-antd/stage/entity';
3 | import {connectStore} from '@elux/vue-web';
4 | import {Menu} from 'ant-design-vue';
5 | import {computed, defineComponent, ref, watch} from 'vue';
6 | import {APPState, GetActions} from '@/Global';
7 | import {MenuItem} from '../../entity';
8 | import styles from './index.module.less';
9 |
10 | export type Key = string | number;
11 |
12 | const ICONS: {[key: string]: any} = {
13 | dashboard: ,
14 | user: ,
15 | post: ,
16 | };
17 |
18 | function generateMenu(menuData: MenuItem[]) {
19 | return menuData.map(({label, key, icon, children}) => {
20 | const Icon = icon ? ICONS[icon] || ICONS['dashboard'] : null;
21 | if (children && children.length) {
22 | return (
23 |
24 | {generateMenu(children)}
25 |
26 | );
27 | }
28 | return (
29 |
30 | {label}
31 |
32 | );
33 | });
34 | }
35 |
36 | export interface StoreProps {
37 | curUser: CurUser;
38 | siderCollapsed?: boolean;
39 | menuData: MenuItem[];
40 | menuSelected: {selected: string[]; open: string[]};
41 | }
42 |
43 | function mapStateToProps(appState: APPState): StoreProps {
44 | const {curUser} = appState.stage!;
45 | const {siderCollapsed, menuData, menuSelected} = appState.admin!;
46 | return {curUser, siderCollapsed, menuData: menuData.items, menuSelected};
47 | }
48 |
49 | const {admin: adminActions} = GetActions('admin');
50 |
51 | const Component = defineComponent({
52 | name: styles.root,
53 | setup() {
54 | const storeProps = connectStore(mapStateToProps);
55 | const menuItems = computed(() => generateMenu(storeProps.menuData));
56 | const openKeys = ref([]);
57 | watch(
58 | () => storeProps.menuSelected.open,
59 | (val) => (openKeys.value = val),
60 | {immediate: true}
61 | );
62 |
63 | const onClick = ({key}: any) => storeProps.dispatch(adminActions.clickMenu(key));
64 | const onOpenChange = (keys: Key[]) => (openKeys.value = keys);
65 |
66 | return () => {
67 | const {menuSelected} = storeProps;
68 | return (
69 |
70 |
80 |
81 | );
82 | };
83 | },
84 | });
85 |
86 | export default Component;
87 |
--------------------------------------------------------------------------------
/src/modules/member/entity.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BaseCurRender,
3 | BaseCurView,
4 | BaseListItem,
5 | BaseListSearch,
6 | BaseListSummary,
7 | BaseLocationState,
8 | BaseModuleState,
9 | BaseRouteParams,
10 | DefineResource,
11 | IRequest,
12 | enumOptions,
13 | } from '@elux-admin-antd/stage/utils/base';
14 |
15 | export enum Gender {
16 | '男' = 'male',
17 | '女' = 'female',
18 | '未知' = 'unknow',
19 | }
20 |
21 | export const DGender = enumOptions(Gender);
22 |
23 | export enum Status {
24 | '启用' = 'enable',
25 | '禁用' = 'disable',
26 | }
27 | export const DStatus = enumOptions(Status);
28 |
29 | export enum Role {
30 | '普通用户' = 'consumer',
31 | '管理员' = 'admin',
32 | '责任编辑' = 'editor',
33 | }
34 |
35 | export const DRole = enumOptions(Role);
36 |
37 | export type CurView = BaseCurView;
38 | export type CurRender = BaseCurRender;
39 |
40 | export type LocationState = BaseLocationState;
41 |
42 | export interface ListSearch extends BaseListSearch {
43 | name?: string;
44 | nickname?: string;
45 | email?: string;
46 | role?: Role;
47 | status?: Status;
48 | }
49 | export interface ListItem extends BaseListItem {
50 | name: string;
51 | nickname: string;
52 | gender: Gender;
53 | role: Role;
54 | status: Status;
55 | articles: number;
56 | email: string;
57 | createdTime: number;
58 | }
59 | export interface ListSummary extends BaseListSummary {}
60 | export interface ItemDetail extends ListItem {}
61 | export type UpdateItem = Omit;
62 |
63 | export type ListSearchFormData = Omit;
64 |
65 | export interface MemberResource extends DefineResource {
66 | RouteParams: RouteParams;
67 | ModuleState: ModuleState;
68 | ListSearch: ListSearch;
69 | ListItem: ListItem;
70 | ListSummary: ListSummary;
71 | ItemDetail: ItemDetail;
72 | UpdateItem: UpdateItem;
73 | CreateItem: UpdateItem;
74 | CurView: CurView;
75 | CurRender: CurRender;
76 | }
77 |
78 | export type RouteParams = BaseRouteParams;
79 | export type ModuleState = BaseModuleState;
80 |
81 | export const defaultListSearch: ListSearch = {
82 | pageCurrent: 1,
83 | pageSize: 10,
84 | sorterOrder: undefined,
85 | sorterField: '',
86 | name: '',
87 | nickname: '',
88 | email: '',
89 | role: undefined,
90 | status: undefined,
91 | };
92 |
93 | export type IGetList = IRequest;
94 | export type IGetItem = IRequest<{id: string}, ItemDetail>;
95 | export type IAlterItems = IRequest<{id: string | string[]; data: Partial}, void>;
96 | export type IDeleteItems = IRequest<{id: string | string[]}, void>;
97 | export type IUpdateItem = IRequest<{id: string | string[]; data: UpdateItem}, void>;
98 | export type ICreateItem = IRequest;
99 |
--------------------------------------------------------------------------------
/src/modules/admin/views/Main/index.tsx:
--------------------------------------------------------------------------------
1 | //通常模块可以定义一个根视图,根视图中显示什么由模块自行决定,父级不干涉,相当于子路由
2 | import ErrorPage from '@elux-admin-antd/stage/components/ErrorPage';
3 | import {CurUser} from '@elux-admin-antd/stage/entity';
4 | import {Switch, connectStore, exportView} from '@elux/vue-web';
5 | import {Layout} from 'ant-design-vue';
6 | import {computed, defineComponent} from 'vue';
7 | import {APPState, LoadComponent} from '@/Global';
8 | import {SubModule} from '../../entity';
9 | import Flag from '../Flag';
10 | import Header from '../Header';
11 | import Menu from '../Menu';
12 | import Tabs from '../Tabs';
13 | import styles from './index.module.less';
14 |
15 | //LoadComponent是懒执行的,不用担心
16 | const SubModuleViews: {[moduleName: string]: () => JSX.Element} = Object.keys(SubModule).reduce((cache, moduleName) => {
17 | cache[moduleName] = LoadComponent(moduleName as any, 'main');
18 | return cache;
19 | }, {});
20 |
21 | export interface StoreProps {
22 | curUser: CurUser;
23 | dialogMode: boolean;
24 | subModule?: SubModule;
25 | siderCollapsed?: boolean;
26 | }
27 |
28 | function mapStateToProps(appState: APPState): StoreProps {
29 | const {curUser} = appState.stage!;
30 | const {subModule, dialogMode, siderCollapsed} = appState.admin!;
31 | return {curUser, subModule, dialogMode, siderCollapsed};
32 | }
33 |
34 | const Component = defineComponent({
35 | name: styles.root,
36 | setup() {
37 | const storeProps = connectStore(mapStateToProps);
38 | const content = computed(() => {
39 | const {subModule} = storeProps;
40 | return (
41 | }>
42 | {subModule &&
43 | Object.keys(SubModule).map((moduleName) => {
44 | if (subModule === moduleName) {
45 | const SubView = SubModuleViews[subModule];
46 | return ;
47 | } else {
48 | return null;
49 | }
50 | })}
51 |
52 | );
53 | });
54 | return () => {
55 | const {curUser, dialogMode, siderCollapsed} = storeProps;
56 | if (!curUser.hasLogin) {
57 | return null;
58 | }
59 | if (dialogMode) {
60 | return content.value;
61 | }
62 | return (
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | {content.value}
75 |
76 |
77 |
78 | );
79 | };
80 | },
81 | });
82 |
83 | export default exportView(Component);
84 |
--------------------------------------------------------------------------------
/mock/src/database.ts:
--------------------------------------------------------------------------------
1 | import {CurUser} from '@elux-admin-antd/stage/entity';
2 | import mockjs from 'mockjs';
3 | import type {ItemDetail as Article} from '@elux-admin-antd/article/entity';
4 | import type {ItemDetail as Member} from '@elux-admin-antd/member/entity';
5 |
6 | const timestamp = Date.now();
7 |
8 | export const guestUser: CurUser = {
9 | id: '0',
10 | username: 'guest',
11 | hasLogin: false,
12 | avatar: 'imgs/guest.png',
13 | mobile: '',
14 | };
15 | export const adminUser: CurUser = {
16 | id: '1',
17 | username: 'admin',
18 | hasLogin: true,
19 | avatar: 'imgs/admin.jpg',
20 | mobile: '18498982234',
21 | };
22 |
23 | function createMembers(): {[id: string]: Member} {
24 | const listData = {};
25 | mockjs
26 | .mock({
27 | 'list|50': [
28 | {
29 | 'id|+1': 1,
30 | name: '@last',
31 | nickname: '@cname',
32 | 'gender|1': ['male', 'female', 'unknow'],
33 | 'role|1': ['consumer', 'admin', 'editor'],
34 | 'status|1': ['enable', 'disable', 'enable'],
35 | articles: 0,
36 | email: '@email',
37 | loginTime: timestamp,
38 | createdTime: timestamp,
39 | },
40 | ],
41 | })
42 | .list.forEach((item: Member, index: number) => {
43 | item.createdTime = timestamp + index * 1000;
44 | item.id = `${item.id}`;
45 | listData[item.id] = item;
46 | });
47 | return listData;
48 | }
49 |
50 | const members = createMembers();
51 |
52 | function createArticles(): {[id: string]: Article} {
53 | const authors: string[] = [];
54 | const editors: string[] = [];
55 |
56 | for (const id in members) {
57 | const member = members[id];
58 | authors.push([id, member.name].join(','));
59 | if (member.role === 'editor' && member.status === 'enable') {
60 | editors.push([id, member.name].join(','));
61 | }
62 | authors.splice(0, authors.length - 5);
63 | editors.splice(0, editors.length - 5);
64 | }
65 |
66 | const listData = {};
67 |
68 | mockjs
69 | .mock({
70 | 'list|50': [
71 | {
72 | 'id|+1': 1,
73 | title: '@ctitle(10, 20)',
74 | summary: '@cparagraph(5, 8)',
75 | content: '@cparagraph(10, 20)',
76 | 'author|1': authors,
77 | editors: () => {
78 | const start = Math.floor(Math.random() * (editors.length - 1));
79 | return editors.slice(start, start + 2);
80 | },
81 | createdTime: timestamp,
82 | 'status|1': ['pending', 'resolved', 'rejected'],
83 | },
84 | ],
85 | })
86 | .list.forEach((item: Article, index: number) => {
87 | item.createdTime = timestamp + index * 1000;
88 | item.id = `${item.id}`;
89 | const authorId = item.author.split(',', 1)[0];
90 | members[authorId].articles++;
91 | listData[item.id] = item;
92 | });
93 | return listData;
94 | }
95 |
96 | const articles = createArticles();
97 |
98 | export const database = {
99 | curUser: guestUser,
100 | members,
101 | articles,
102 | };
103 |
--------------------------------------------------------------------------------
/src/modules/admin/views/Header/index.tsx:
--------------------------------------------------------------------------------
1 | //通常模块可以定义一个根视图,根视图中显示什么由模块自行决定,父级不干涉,相当于子路由
2 | import {BellOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, QuestionCircleOutlined, UserOutlined} from '@ant-design/icons-vue';
3 | import {CurUser} from '@elux-admin-antd/stage/entity';
4 | import {AdminHomeUrl} from '@elux-admin-antd/stage/utils/const';
5 | import {connectStore} from '@elux/vue-web';
6 | import {Avatar, Badge, Dropdown, Menu} from 'ant-design-vue';
7 | import {computed, defineComponent} from 'vue';
8 | import {APPState, GetActions, StaticPrefix, useRouter} from '@/Global';
9 | import {Notices} from '../../entity';
10 | import styles from './index.module.less';
11 |
12 | const {stage: stageActions, admin: adminActions} = GetActions('stage', 'admin');
13 |
14 | export interface StoreProps {
15 | curUser: CurUser;
16 | siderCollapsed?: boolean;
17 | notices?: Notices;
18 | }
19 |
20 | function mapStateToProps(appState: APPState): StoreProps {
21 | const {curUser} = appState.stage!;
22 | const {notices, siderCollapsed} = appState.admin!;
23 | return {curUser, notices, siderCollapsed};
24 | }
25 |
26 | const Component = defineComponent({
27 | name: styles.root,
28 | setup() {
29 | const router = useRouter();
30 | const storeProps = connectStore(mapStateToProps);
31 | const toggleSider = () => {
32 | storeProps.dispatch(adminActions.putSiderCollapsed(!storeProps.siderCollapsed));
33 | };
34 | const onUserMenuClick = ({key}: {key: string | number}) => {
35 | if (key === 'logout') {
36 | storeProps.dispatch(stageActions.logout());
37 | } else if (key === 'home') {
38 | router.relaunch({url: AdminHomeUrl}, 'window');
39 | }
40 | };
41 | const userMenu = computed(() => {
42 | return (
43 |
51 | );
52 | });
53 |
54 | return () => {
55 | const {siderCollapsed, notices, curUser} = storeProps;
56 | return (
57 |
58 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | {curUser.username}
76 |
77 |
78 |
79 |
80 | );
81 | };
82 | },
83 | });
84 |
85 | export default Component;
86 |
--------------------------------------------------------------------------------
/src/modules/stage/components/DialogPage/index.module.less:
--------------------------------------------------------------------------------
1 | @import '../../assets/css/var.less';
2 |
3 | :global {
4 | ._dialog > :local(.root) > .control {
5 | display: block;
6 | }
7 |
8 | :local(.root) {
9 | position: relative;
10 | background-color: #fff;
11 |
12 | &.show-brand {
13 | padding-top: 45px !important;
14 |
15 | > .content > .subject {
16 | margin: 20px 0 0;
17 | font-size: 25px;
18 | text-align: center;
19 | border-bottom: none;
20 | }
21 | }
22 |
23 | > .control {
24 | position: absolute;
25 | top: 0;
26 | right: 0;
27 | z-index: 99999999;
28 | display: none;
29 | padding: 0 10px;
30 | font-size: 13px;
31 | text-align: center;
32 | background-color: #4d5763;
33 | border-radius: 0 0 0 15px;
34 |
35 | > * {
36 | display: inline-block;
37 | width: 30px;
38 | height: 30px;
39 | line-height: 30px;
40 | color: #fff;
41 | cursor: pointer;
42 | opacity: 0.6;
43 |
44 | &:last-child {
45 | opacity: 1;
46 | }
47 |
48 | &:hover {
49 | opacity: 1;
50 | }
51 | }
52 | }
53 |
54 | > .resize {
55 | position: absolute;
56 | top: 5px;
57 | left: 5px;
58 | z-index: 99999999;
59 | font-size: 10px;
60 | line-height: 0;
61 | cursor: zoom-in;
62 | background-color: #fff;
63 | border-radius: 100px;
64 |
65 | .btn {
66 | display: block;
67 | width: 1.6em;
68 | height: 1.6em;
69 | overflow: hidden;
70 | line-height: 1.4em;
71 | transform: rotate(45deg);
72 | }
73 |
74 | .anticon {
75 | color: #999;
76 |
77 | &:last-child {
78 | margin-left: -0.4em;
79 | }
80 | }
81 |
82 | &:hover {
83 | background-color: #4d5763;
84 |
85 | .anticon {
86 | color: #fff;
87 | }
88 | }
89 | }
90 |
91 | > .brand {
92 | position: absolute;
93 | top: 0;
94 | left: 0;
95 | width: 100%;
96 | padding-left: 35px;
97 | line-height: 35px;
98 | color: #777;
99 | background: rgba(0, 0, 0, 0.06) url('../../assets/imgs/logo-icon.svg') no-repeat 10px center;
100 | background-size: 20px;
101 | border-bottom: rgba(255, 255, 255, 0.5) 1px solid;
102 |
103 | .ver {
104 | font-size: 12px;
105 | font-weight: bold;
106 | }
107 | }
108 |
109 | > .content {
110 | box-sizing: border-box;
111 | height: 100%;
112 | overflow: auto;
113 | background: #fff;
114 |
115 | .subject {
116 | padding-bottom: 10px;
117 | margin: 20px;
118 | font-size: 18px;
119 | font-weight: bold;
120 | border-bottom: 1px solid @border-color-split;
121 | }
122 | }
123 |
124 | &.size-max {
125 | > .resize {
126 | cursor: zoom-out;
127 |
128 | .anticon {
129 | transform: scale(-1);
130 | }
131 | }
132 | }
133 | }
134 |
135 | :local(.mask) {
136 | position: absolute;
137 | top: 0;
138 | right: 0;
139 | bottom: 0;
140 | left: 0;
141 | z-index: -1;
142 | cursor: default;
143 | background: rgba(0, 0, 0, 0.2);
144 |
145 | &.no-mask {
146 | background-color: rgba(0, 0, 0, 0);
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/mock/src/routes/session.ts:
--------------------------------------------------------------------------------
1 | import {Router} from 'express';
2 | import {adminUser, database, guestUser} from '../database';
3 | import type {IGetMenu, IGetNotices} from '@elux-admin-antd/admin/entity';
4 | import type {IGetCurUser, ILogin, ILogout, IRegistry, IResetPassword, ISendCaptcha} from '@elux-admin-antd/stage/entity';
5 |
6 | const router = Router();
7 |
8 | router.get('/', function (req, res, next) {
9 | const result: IGetCurUser['Response'] = database.curUser;
10 | setTimeout(() => res.json(result), 500);
11 | });
12 |
13 | router.put('/', function ({body}: {body: ILogin['Request']}, res, next) {
14 | const {username = '', password = ''} = body;
15 | if (username === 'admin' && password === '123456') {
16 | database.curUser = adminUser;
17 | const result: ILogin['Response'] = adminUser;
18 | setTimeout(() => res.json(result), 500);
19 | } else {
20 | res.status(422).json({
21 | message: '用户名或密码错误!',
22 | });
23 | }
24 | });
25 |
26 | router.post('/', function ({body}: {body: IRegistry['Request']}, res, next) {
27 | const {username = '', password = ''} = body;
28 | if (username && password) {
29 | const curUser = {...adminUser, username, password};
30 | database.curUser = curUser;
31 | const result: IRegistry['Response'] = curUser;
32 | setTimeout(() => res.json(result), 500);
33 | } else {
34 | res.status(422).json({
35 | message: '用户名或密码错误!',
36 | });
37 | }
38 | });
39 |
40 | router.delete('/', function (req, res, next) {
41 | database.curUser = guestUser;
42 | const result: ILogout['Response'] = database.curUser;
43 | setTimeout(() => res.json(result), 500);
44 | });
45 |
46 | router.put('/resetPassword', function ({body}: {body: IResetPassword['Request']}, res, next) {
47 | const {phone = '', password = '', captcha = ''} = body;
48 | if (phone && password && captcha) {
49 | setTimeout(() => res.json({}), 500);
50 | } else {
51 | res.status(422).json({
52 | message: '参数错误!',
53 | });
54 | }
55 | });
56 |
57 | router.post('/sendCaptcha', function ({body}: {body: ISendCaptcha['Request']}, res, next) {
58 | const {phone = ''} = body;
59 | if (phone) {
60 | setTimeout(() => res.json({}), 500);
61 | } else {
62 | res.status(422).json({
63 | message: '参数错误!',
64 | });
65 | }
66 | });
67 |
68 | router.get('/notices', function (req, res, next) {
69 | const result: IGetNotices['Response'] = {num: Math.floor(Math.random() * 100)};
70 | setTimeout(() => res.json(result), 500);
71 | });
72 |
73 | router.get('/menu', function (req, res, next) {
74 | const result: IGetMenu['Response'] = [
75 | {
76 | key: 'dashboard',
77 | label: '概要总览',
78 | icon: 'dashboard',
79 | match: '/admin/dashboard/',
80 | link: '/admin/dashboard/workplace',
81 | },
82 | {
83 | key: 'member',
84 | label: '用户管理',
85 | icon: 'user',
86 | match: '/admin/member/',
87 | link: '/admin/member/list/maintain',
88 | },
89 | {
90 | key: 'article',
91 | label: '文章管理',
92 | icon: 'post',
93 | children: [
94 | {
95 | key: 'articleList',
96 | label: '文章列表',
97 | match: '/admin/article/',
98 | link: '/admin/article/list/maintain',
99 | },
100 | {
101 | key: 'commentList',
102 | label: '评论列表',
103 | match: '/admin/comment/',
104 | link: '/admin/comment/list/maintain',
105 | },
106 | ],
107 | },
108 | ];
109 | setTimeout(() => res.json(result), 500);
110 | });
111 |
112 | export default router;
113 |
--------------------------------------------------------------------------------
/src/modules/stage/utils/base.ts:
--------------------------------------------------------------------------------
1 | import {LoadingState} from '@elux/vue-web';
2 |
3 | export interface IRequest {
4 | Request: Req;
5 | Response: Res;
6 | }
7 | export type BaseCurView = 'list' | 'item';
8 | export type BaseCurRender = 'maintain' | 'index' | 'selector' | 'edit' | 'detail';
9 | export interface BaseListSearch {
10 | pageCurrent?: number;
11 | pageSize?: number;
12 | sorterOrder?: 'ascend' | 'descend';
13 | sorterField?: string;
14 | }
15 |
16 | export interface BaseListItem {
17 | id: string;
18 | }
19 |
20 | export interface BaseListSummary {
21 | pageCurrent: number;
22 | pageSize: number;
23 | totalItems: number;
24 | totalPages: number;
25 | categorys?: {id: string; name: string; ids: string[]}[];
26 | }
27 |
28 | export interface BaseItemDetail extends BaseListItem {}
29 |
30 | export interface BaseLocationState {
31 | selectLimit?: number | [number, number];
32 | selectedRows?: Partial[];
33 | showSearch?: boolean;
34 | fixedSearch?: {[field: string]: any};
35 | onEditSubmit?: (id: string, data: Record) => Promise;
36 | onSelectedSubmit?: (rows: Partial[]) => void;
37 | }
38 | export interface BaseModuleState {
39 | prefixPathname: string;
40 | curView?: TDefineResource['CurView'];
41 | curRender?: TDefineResource['CurRender'];
42 | listSearch: TDefineResource['ListSearch'];
43 | list?: TDefineResource['ListItem'][];
44 | listSummary?: TDefineResource['ListSummary'];
45 | listLoading?: LoadingState;
46 | itemId?: string;
47 | itemDetail?: TDefineResource['ItemDetail'];
48 | }
49 |
50 | export interface BaseRouteParams {
51 | prefixPathname: string;
52 | curView?: TDefineResource['CurView'];
53 | curRender?: TDefineResource['CurRender'];
54 | listSearch?: TDefineResource['ListSearch'];
55 | itemId?: string;
56 | }
57 |
58 | export interface BaseApi {
59 | getList?(params: BaseListSearch): Promise<{list: BaseListItem[]; listSummary: BaseListSummary}>;
60 | getItem?(params: {id: string}): Promise;
61 | alterItems?(params: {id: string | string[]; data: Record}): Promise;
62 | updateItem?(params: {id: string; data: any}): Promise;
63 | createItem?(params: Record): Promise<{id: string}>;
64 | deleteItems?(params: {id: string | string[]}): Promise;
65 | }
66 |
67 | export interface DefineResource {
68 | RouteParams: BaseRouteParams;
69 | ModuleState: BaseModuleState;
70 | ListSearch: BaseListSearch;
71 | ListItem: BaseListItem;
72 | ListSummary: BaseListSummary;
73 | CurView: BaseCurView;
74 | CurRender: BaseCurRender;
75 | ItemDetail: BaseItemDetail;
76 | UpdateItem: any;
77 | CreateItem: any;
78 | }
79 |
80 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
81 | export function enumOptions(data: T) {
82 | const options: {value: string; label: string}[] = [];
83 | const labelToValue: {[key in keyof T]: T[key]} = {} as any;
84 | const valueToLabel: {[key in T[keyof T]]: string} = {} as any;
85 | Object.keys(data).forEach((label) => {
86 | options.push({label, value: data[label]});
87 | (labelToValue as any)[label] = data[label];
88 | valueToLabel[data[label]] = label;
89 | });
90 | return {
91 | valueToLabel,
92 | labelToValue,
93 | options,
94 | };
95 | }
96 |
97 | export function arrayToMap(arr: T[], key = 'id'): {[key: string]: T} {
98 | return arr.reduce((pre, cur) => {
99 | pre[cur[key]] = cur;
100 | return pre;
101 | }, {});
102 | }
103 |
--------------------------------------------------------------------------------
/src/modules/member/views/Maintain.tsx:
--------------------------------------------------------------------------------
1 | import {PlusOutlined} from '@ant-design/icons-vue';
2 | import {MBatchActions} from '@elux-admin-antd/stage/components/MTable';
3 | import {useAlter, useShowDetail} from '@elux-admin-antd/stage/utils/resource';
4 | import {DocumentHead, connectStore} from '@elux/vue-web';
5 | import {Button, Popconfirm} from 'ant-design-vue';
6 | import {ColumnProps} from 'ant-design-vue/lib/table';
7 | import {defineComponent} from 'vue';
8 | import {APPState, GetActions} from '@/Global';
9 | import {ListItem, ListSearch, Status} from '../entity';
10 | import ListTable from './ListTable';
11 | import SearchForm from './SearchForm';
12 |
13 | interface StoreProps {
14 | prefixPathname: string;
15 | listSearch: ListSearch;
16 | curRender?: string;
17 | }
18 |
19 | const mapStateToProps: (state: APPState) => StoreProps = (state) => {
20 | const {prefixPathname, curRender, listSearch} = state.member!;
21 | return {prefixPathname, curRender, listSearch};
22 | };
23 |
24 | const {member: memberActions} = GetActions('member');
25 |
26 | const Component = defineComponent({
27 | setup() {
28 | const storeProps = connectStore(mapStateToProps);
29 | const {selectedRows, deleteItems, alterItems, updateItem} = useAlter(storeProps, memberActions);
30 | const {onShowDetail, onShowEditor} = useShowDetail(storeProps);
31 | const commActions = (
32 | } onClick={() => onShowEditor('', updateItem)}>
33 | 新建
34 |
35 | );
36 | const batchActions: MBatchActions = {
37 | actions: [
38 | {key: 'delete', label: '批量删除', confirm: true},
39 | {key: 'enable', label: '批量启用', confirm: true},
40 | {key: 'disable', label: '批量禁用', confirm: true},
41 | ],
42 | handler: (item: {key: string}, ids: (string | number)[]) => {
43 | if (item.key === 'delete') {
44 | deleteItems(ids as string[]);
45 | } else if (item.key === 'enable') {
46 | alterItems(ids as string[], {status: Status.启用});
47 | } else if (item.key === 'disable') {
48 | alterItems(ids as string[], {status: Status.禁用});
49 | }
50 | },
51 | };
52 | const actionColumns: ColumnProps = {
53 | title: '操作',
54 | dataIndex: 'id',
55 | width: '200px',
56 | align: 'center',
57 | customRender: ({value, record}) => {
58 | return (
59 |
60 | onShowDetail(value)}>详细
61 | alterItems(value, {status: record.status === Status.启用 ? Status.禁用 : Status.启用})}>
62 | {record.status === Status.启用 ? '禁用' : '启用'}
63 |
64 | onShowEditor(value, updateItem)}>修改
65 | deleteItems(value)}>
66 | 删除
67 |
68 |
69 | );
70 | },
71 | };
72 |
73 | return () => {
74 | const {prefixPathname, curRender, listSearch} = storeProps;
75 | return (
76 |
77 |
78 |
79 |
80 |
87 |
88 |
89 | );
90 | };
91 | },
92 | });
93 |
94 | export default Component;
95 |
--------------------------------------------------------------------------------
/src/modules/member/views/EditorForm.tsx:
--------------------------------------------------------------------------------
1 | // import {useUpdateItem} from '@elux-admin-antd/stage/utils/resource';
2 | import {getFormDecorators} from '@elux-admin-antd/stage/utils/tools';
3 | import {Dispatch} from '@elux/vue-web';
4 | import {Button, Form, FormInstance, Input, Select} from 'ant-design-vue';
5 | import {defineComponent, ref, shallowReactive} from 'vue';
6 | import {GetActions, useRouter} from '@/Global';
7 | import {DGender, DRole, DStatus, ItemDetail, UpdateItem} from '../entity';
8 |
9 | const FormItem = Form.Item;
10 |
11 | export const formItemLayout = {
12 | labelCol: {
13 | span: 4,
14 | },
15 | wrapperCol: {
16 | span: 19,
17 | },
18 | };
19 |
20 | const fromDecorators = getFormDecorators({
21 | name: {label: '用户名', rules: [{required: true, message: '请输入用户名'}]},
22 | nickname: {label: '呢称', rules: [{required: true, message: '请输入呢称'}]},
23 | role: {label: '角色', rules: [{required: true, message: '请选择角色'}]},
24 | gender: {label: '性别', rules: [{required: true, message: '请选择性别'}]},
25 | email: {label: 'Email', rules: [{required: true, type: 'email', message: '请输入Email'}]},
26 | status: {label: '状态', rules: [{required: true, message: '请选择用户状态'}]},
27 | });
28 |
29 | interface Props {
30 | dispatch: Dispatch;
31 | itemDetail: ItemDetail;
32 | }
33 |
34 | const {member: memberActions} = GetActions('member');
35 |
36 | const Component = defineComponent({
37 | props: ['dispatch', 'itemDetail'] as any,
38 | setup(props) {
39 | const router = useRouter();
40 | const formRef = ref();
41 | const formState = shallowReactive({
42 | ...props.itemDetail,
43 | });
44 |
45 | const onFinish = (values: UpdateItem) => {
46 | const id = props.itemDetail.id;
47 | if (id) {
48 | props.dispatch(memberActions.updateItem(id, values));
49 | } else {
50 | props.dispatch(memberActions.createItem(values));
51 | }
52 | };
53 |
54 | const onReset = () => {
55 | formRef.value?.resetFields();
56 | };
57 |
58 | const goBack = () => router.back(1, 'window');
59 |
60 | // const {loading, onFinish} = useUpdateItem(itemDetail.id, dispatch, memberActions);
61 |
62 | return () => {
63 | return (
64 |
93 | );
94 | };
95 | },
96 | });
97 |
98 | export default Component;
99 |
--------------------------------------------------------------------------------
/src/modules/article/views/EditorForm.tsx:
--------------------------------------------------------------------------------
1 | // import {useUpdateItem} from '@elux-admin-antd/stage/utils/resource';
2 | import {ListSearch as MemberListSearch, Role, Status} from '@elux-admin-antd/member/entity';
3 | import MSelect from '@elux-admin-antd/stage/components/MSelect';
4 | import {getFormDecorators} from '@elux-admin-antd/stage/utils/tools';
5 | import {Dispatch} from '@elux/vue-web';
6 | import {Button, Form, FormInstance, Input} from 'ant-design-vue';
7 | import {defineComponent, ref, shallowReactive} from 'vue';
8 | import {GetActions, useRouter} from '@/Global';
9 | import {ItemDetail, UpdateItem} from '../entity';
10 |
11 | const FormItem = Form.Item;
12 |
13 | export const formItemLayout = {
14 | labelCol: {
15 | span: 4,
16 | },
17 | wrapperCol: {
18 | span: 19,
19 | },
20 | };
21 |
22 | const fromDecorators = getFormDecorators({
23 | title: {label: '标题', rules: [{required: true, message: '请输入标题'}]},
24 | summary: {label: '摘要', rules: [{required: true, message: '请输入摘要'}]},
25 | content: {label: '内容', rules: [{required: true, message: '请输入内容'}]},
26 | editors: {label: '责任编辑', rules: [{required: true, message: '请选择责任编辑'}]},
27 | });
28 |
29 | interface Props {
30 | dispatch: Dispatch;
31 | itemDetail: ItemDetail;
32 | }
33 |
34 | const {article: articleActions} = GetActions('article');
35 |
36 | const Component = defineComponent({
37 | props: ['dispatch', 'itemDetail'] as any,
38 | setup(props) {
39 | const router = useRouter();
40 | const formRef = ref();
41 | const formState = shallowReactive({
42 | ...props.itemDetail,
43 | });
44 |
45 | const onFinish = (values: UpdateItem) => {
46 | const id = props.itemDetail.id;
47 | if (id) {
48 | props.dispatch(articleActions.updateItem(id, values));
49 | } else {
50 | props.dispatch(articleActions.createItem(values));
51 | }
52 | };
53 |
54 | const onReset = () => {
55 | formRef.value?.resetFields();
56 | };
57 |
58 | const goBack = () => router.back(1, 'window');
59 |
60 | // const {loading, onFinish} = useUpdateItem(itemDetail.id, dispatch, memberActions);
61 |
62 | return () => {
63 | return (
64 |
95 | );
96 | };
97 | },
98 | });
99 |
100 | export default Component;
101 |
--------------------------------------------------------------------------------
/src/modules/article/views/Maintain.tsx:
--------------------------------------------------------------------------------
1 | import {PlusOutlined} from '@ant-design/icons-vue';
2 | import {MBatchActions} from '@elux-admin-antd/stage/components/MTable';
3 | import {useAlter, useShowDetail} from '@elux-admin-antd/stage/utils/resource';
4 | import {DocumentHead, connectStore} from '@elux/vue-web';
5 | import {Button, Dropdown, Menu, Popconfirm} from 'ant-design-vue';
6 | import {ColumnProps} from 'ant-design-vue/lib/table';
7 | import {defineComponent} from 'vue';
8 | import {APPState, GetActions} from '@/Global';
9 | import {ListItem, ListSearch, Status} from '../entity';
10 | import ListTable from './ListTable';
11 | import SearchForm from './SearchForm';
12 |
13 | interface StoreProps {
14 | prefixPathname: string;
15 | listSearch: ListSearch;
16 | curRender?: string;
17 | }
18 |
19 | const mapStateToProps: (state: APPState) => StoreProps = (state) => {
20 | const {prefixPathname, curRender, listSearch} = state.article!;
21 | return {prefixPathname, curRender, listSearch};
22 | };
23 |
24 | const {article: articleActions} = GetActions('article');
25 |
26 | const Component = defineComponent({
27 | setup() {
28 | const storeProps = connectStore(mapStateToProps);
29 | const {selectedRows, deleteItems, alterItems, updateItem} = useAlter(storeProps, articleActions);
30 | const {onShowDetail, onShowEditor} = useShowDetail(storeProps);
31 | const commActions = (
32 | } onClick={() => onShowEditor('', updateItem)}>
33 | 新建
34 |
35 | );
36 | const batchActions: MBatchActions = {
37 | actions: [
38 | {key: 'delete', label: '批量删除', confirm: true},
39 | {key: 'resolved', label: '批量通过', confirm: true},
40 | {key: 'rejected', label: '批量拒绝', confirm: true},
41 | ],
42 | handler: (item: {key: string}, ids: (string | number)[]) => {
43 | if (item.key === 'delete') {
44 | deleteItems(ids as string[]);
45 | } else if (item.key === 'resolved') {
46 | alterItems(ids as string[], {status: Status.审核通过});
47 | } else if (item.key === 'rejected') {
48 | alterItems(ids as string[], {status: Status.审核拒绝});
49 | }
50 | },
51 | };
52 | const actionColumns: ColumnProps = {
53 | title: '操作',
54 | dataIndex: 'id',
55 | width: '200px',
56 | align: 'center',
57 | customRender: ({value, record}) => {
58 | return (
59 |
60 | onShowDetail(value)}>详细
61 | {
65 | alterItems([value], {status: key});
66 | }}
67 | >
68 | 审核通过
69 | 审核拒绝
70 |
71 | }
72 | >
73 | 审核
74 |
75 | onShowEditor(value, updateItem)}>修改
76 | deleteItems(value)}>
77 | 删除
78 |
79 |
80 | );
81 | },
82 | };
83 |
84 | return () => {
85 | const {prefixPathname, curRender, listSearch} = storeProps;
86 | return (
87 |
88 |
89 |
90 |
91 |
98 |
99 |
100 | );
101 | };
102 | },
103 | });
104 |
105 | export default Component;
106 |
--------------------------------------------------------------------------------
/src/modules/stage/components/DialogPage/index.tsx:
--------------------------------------------------------------------------------
1 | import {ArrowLeftOutlined, CaretLeftOutlined, CaretRightOutlined, CloseOutlined, ReloadOutlined} from '@ant-design/icons-vue';
2 | import {DocumentHead, Link} from '@elux/vue-web';
3 | import {Tooltip} from 'ant-design-vue';
4 | import {computed, defineComponent, ref} from 'vue';
5 | import styles from './index.module.less';
6 |
7 | export interface Props {
8 | class?: string;
9 | title?: string;
10 | subject?: string;
11 | showBrand?: boolean;
12 | showControls?: boolean;
13 | maskClosable?: boolean;
14 | mask?: boolean;
15 | size?: 'max' | 'auto';
16 | minSize?: (number | string)[];
17 | backOverflowRedirect?: string;
18 | }
19 |
20 | const Component = defineComponent({
21 | name: styles.root,
22 | // eslint-disable-next-line vue/require-prop-types
23 | props: ['class', 'title', 'subject', 'showBrand', 'showControls', 'maskClosable', 'mask', 'size', 'minSize', 'backOverflowRedirect'] as any,
24 | setup(props, context) {
25 | const size = ref(props.size || 'auto');
26 |
27 | const toggleSize = () => {
28 | size.value = size.value === 'auto' ? 'max' : 'auto';
29 | };
30 |
31 | const controls = computed(() => {
32 | const showControls = props.showControls !== undefined ? props.showControls : !props.showBrand;
33 | return showControls ? (
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | ) : null;
52 | });
53 |
54 | const resize = computed(() => {
55 | const minSize = props.minSize || [];
56 | const showResize = minSize[0];
57 | return showResize ? (
58 |
59 |
60 |
61 |
62 |
63 |
64 | ) : null;
65 | });
66 |
67 | const style = computed(() => {
68 | const minSize = props.minSize || [];
69 | const style = {};
70 | if (minSize[0] && size.value === 'auto') {
71 | style['width'] = typeof minSize[0] === 'number' ? minSize[0] + 'px' : minSize[0];
72 | }
73 | if (minSize[1] && size.value === 'auto') {
74 | style['height'] = typeof minSize[1] === 'number' ? minSize[1] + 'px' : minSize[1];
75 | }
76 | return style;
77 | });
78 |
79 | return () => {
80 | const {class: className = '', title, subject, showBrand, maskClosable = true, mask, backOverflowRedirect} = props;
81 | return (
82 | <>
83 |
84 | {title && }
85 | {resize.value}
86 | {controls.value}
87 | {showBrand && (
88 |
89 | Elux-管理系统 V1.0
90 |
91 | )}
92 |
93 | {subject && {subject}
}
94 | {context.slots.default!()}
95 |
96 |
97 |
105 | >
106 | );
107 | };
108 | },
109 | });
110 |
111 | export default Component;
112 |
--------------------------------------------------------------------------------
/src/modules/stage/views/LoginForm/index.tsx:
--------------------------------------------------------------------------------
1 | import {AlipayCircleOutlined, AliwangwangFilled, DingtalkCircleFilled, LockOutlined, MobileFilled, UserOutlined} from '@ant-design/icons-vue';
2 | import {Link, connectStore} from '@elux/vue-web';
3 | import {Button, Checkbox, Form, FormInstance, Input} from 'ant-design-vue';
4 | import {defineComponent, reactive, ref} from 'vue';
5 | import {APPState, GetActions} from '@/Global';
6 | import DialogPage from '../../components/DialogPage';
7 | import {LoginParams} from '../../entity';
8 | import {getFormDecorators} from '../../utils/tools';
9 | import styles from './index.module.less';
10 |
11 | type HFormData = Required;
12 |
13 | const {stage: stageActions} = GetActions('stage');
14 |
15 | interface StoreProps {
16 | fromUrl?: string;
17 | }
18 |
19 | function mapStateToProps(appState: APPState): StoreProps {
20 | const {fromUrl} = appState.stage!;
21 | return {
22 | fromUrl,
23 | };
24 | }
25 |
26 | const Component = defineComponent({
27 | name: styles.root,
28 | setup() {
29 | const storeProps = connectStore(mapStateToProps);
30 | const formRef = ref();
31 | const formState = reactive({
32 | username: 'admin',
33 | password: '123456',
34 | keep: false,
35 | });
36 |
37 | const fromDecorators = getFormDecorators({
38 | username: {rules: [{required: true, message: '请输入用户名!', whitespace: true}]},
39 | password: {
40 | rules: [{required: true, message: '请输入密码!', whitespace: true}],
41 | },
42 | keep: {valuePropName: 'checked'},
43 | });
44 | const onSubmit = (values: HFormData) => {
45 | const result = storeProps.dispatch(stageActions.login(values)) as Promise;
46 | result.catch(({message}) => {
47 | //console.log(message);
48 | });
49 | };
50 | const onCancel = () => {
51 | storeProps.dispatch(stageActions.cancelLogin());
52 | };
53 |
54 | return () => {
55 | const {fromUrl} = storeProps;
56 | return (
57 |
58 |
59 |
61 | } placeholder="请输入用户名" />
62 |
63 |
64 | } placeholder="请输入密码" />
65 |
66 |
67 |
68 | 记住登录
69 |
70 |
71 | 忘记密码?
72 |
73 |
74 |
75 |
76 |
79 |
82 |
83 |
84 |
85 |
102 |
103 |
104 | );
105 | };
106 | },
107 | });
108 |
109 | export default Component;
110 |
--------------------------------------------------------------------------------
/src/modules/stage/components/MSearch/index.tsx:
--------------------------------------------------------------------------------
1 | import {DownOutlined, UpOutlined} from '@ant-design/icons-vue';
2 | import {Button, Form} from 'ant-design-vue';
3 | import {cloneVNode, computed, defineComponent, ref, shallowReactive} from 'vue';
4 | import {SearchFromItems} from '../../utils/tools';
5 | import styles from './index.module.less';
6 |
7 | interface Props> {
8 | class?: string;
9 | items: SearchFromItems;
10 | values: TFormData;
11 | onSearch: (values: Partial) => void;
12 | fixedFields?: Partial; //固定搜索值
13 | senior?: number; //未展开时显示多少项
14 | cols?: number; //每行显示多少项
15 | expand?: boolean;
16 | }
17 |
18 | const Component = defineComponent({
19 | name: styles.root,
20 | props: ['class', 'items', 'values', 'onSearch', 'fixedFields', 'senior', 'cols', 'expand'] as any,
21 | emits: ['search'],
22 | setup(props, {emit}) {
23 | const cols = computed(() => {
24 | const {cols = 4, items} = props;
25 | const cArr: number[] = [];
26 | let cur = 0;
27 | items.forEach((item) => {
28 | // eslint-disable-next-line no-control-regex
29 | const label = Math.ceil(item.label!.replace(/[^\x00-\xff]/g, 'aa').length / 2);
30 | const col = item.col || 1;
31 | if (cur + col > cols) {
32 | cur = 0;
33 | }
34 | item.cite = cur;
35 | if (label > (cArr[cur] || 0)) {
36 | cArr[cur] = label;
37 | }
38 | cur += col;
39 | });
40 | return cArr;
41 | });
42 | const colWidth = computed(() => {
43 | const {cols = 4} = props;
44 | return parseFloat((100 / cols).toFixed(2));
45 | });
46 | const expand = ref(!!props.expand);
47 |
48 | const config = computed(() => {
49 | const {senior = 4} = props;
50 | const shrink = expand.value ? props.items.length : senior;
51 | return shallowReactive({senior, shrink});
52 | });
53 | const formStateRef = computed(() => {
54 | const data = props.items.reduce((obj, item) => {
55 | obj[item.name] = props.values[item.name];
56 | return obj;
57 | }, {});
58 | return shallowReactive(data);
59 | });
60 | const onClear = () => {
61 | emit('search', props.fixedFields || {});
62 | };
63 | const onFinish = (vals: Record) => {
64 | Object.assign(vals, props.fixedFields);
65 | emit('search', vals);
66 | };
67 | const toggle = () => {
68 | expand.value = !expand.value;
69 | };
70 |
71 | return () => {
72 | const {class: className = '', items, fixedFields} = props;
73 | const {shrink, senior} = config.value;
74 | const formState = formStateRef.value;
75 | return (
76 |
77 | = shrink ? 'none' : 'flex', width: `${colWidth.value * (item.col || 1)}%`}}
84 | key={item.name}
85 | label={
86 |
87 | {item.label}
88 |
89 | }
90 | >
91 | {cloneVNode(item.formItem, {
92 | disabled: !!(fixedFields && fixedFields[item.name]),
93 | value: formState[item.name],
94 | 'onUpdate:value': ($event: any) => (formState[item.name] = $event),
95 | })}
96 |
97 | );
98 | })}
99 |
100 |
103 |
104 | {items.length > senior && (
105 |
106 | {expand.value ? '收起' : '展开'} {expand.value ? : }
107 |
108 | )}
109 |
110 |
111 |
112 | );
113 | };
114 | },
115 | }) as any;
116 |
117 | export default Component as (props: Props) => JSX.Element;
118 |
--------------------------------------------------------------------------------
/mock/src/routes/article.ts:
--------------------------------------------------------------------------------
1 | import {Router} from 'express';
2 | import {database} from '../database';
3 | import type {IAlterItems, ICreateItem, IGetItem, IGetList, ListItem, Status} from '@elux-admin-antd/article/entity';
4 |
5 | type Query = {[K in keyof T]: string};
6 |
7 | const router = Router();
8 |
9 | router.get('/', function ({query}: {query: Query}, res, next) {
10 | const {pageCurrent, pageSize, title, author, editor, status, sorterField, sorterOrder} = {
11 | pageCurrent: parseInt(query.pageCurrent || '1'),
12 | pageSize: parseInt(query.pageSize || '10'),
13 | title: query.title || '',
14 | author: (query.author || '').split(',', 1)[0],
15 | editor: (query.editor || '').split(',', 1)[0],
16 | status: query.status || '',
17 | sorterField: query.sorterField || '',
18 | sorterOrder: query.sorterOrder || '',
19 | };
20 |
21 | const start = (pageCurrent - 1) * pageSize;
22 | const end = start + pageSize;
23 |
24 | const dataMap = database.articles;
25 | let listData = Object.keys(dataMap)
26 | .reverse()
27 | .map((id) => {
28 | return dataMap[id];
29 | });
30 |
31 | if (title) {
32 | listData = listData.filter((item) => item.title.includes(title));
33 | }
34 | if (author) {
35 | listData = listData.filter((item) => item.author.split(',', 1)[0] === author);
36 | }
37 | if (editor) {
38 | listData = listData.filter(
39 | (item) => item.editors[0].split(',', 1)[0] === editor || (item.editors[1] && item.editors[1].split(',', 1)[0] === editor)
40 | );
41 | }
42 | if (status) {
43 | listData = listData.filter((item) => item.status === status);
44 | }
45 |
46 | if (sorterField === 'createdTime') {
47 | if (sorterOrder === 'ascend') {
48 | listData.sort((a: ListItem, b: ListItem) => {
49 | return a.createdTime - b.createdTime;
50 | });
51 | } else if (sorterOrder === 'descend') {
52 | listData.sort((a, b) => {
53 | return b.createdTime - a.createdTime;
54 | });
55 | }
56 | }
57 | if (sorterField === 'author') {
58 | if (sorterOrder === 'ascend') {
59 | listData.sort((a: ListItem, b: ListItem) => {
60 | return a.author.split(',')[1].charCodeAt(0) - b.author.split(',')[1].charCodeAt(0);
61 | });
62 | } else if (sorterOrder === 'descend') {
63 | listData.sort((a, b) => {
64 | return b.author.split(',')[1].charCodeAt(0) - a.author.split(',')[1].charCodeAt(0);
65 | });
66 | }
67 | }
68 |
69 | const result: IGetList['Response'] = {
70 | listSummary: {
71 | pageCurrent,
72 | pageSize,
73 | totalItems: listData.length,
74 | totalPages: Math.ceil(listData.length / pageSize),
75 | },
76 | list: listData.slice(start, end).map((item) => ({...item, summary: '', content: ''})),
77 | };
78 |
79 | setTimeout(() => res.json(result), 500);
80 | });
81 |
82 | router.get('/:id', function ({params}: {params: IGetItem['Request']}, res, next) {
83 | const {id} = params;
84 | const item = database.articles[id];
85 | if (!item) {
86 | res.status(404).end();
87 | } else {
88 | const result: IGetItem['Response'] = item;
89 | setTimeout(() => res.json(result), 500);
90 | }
91 | });
92 |
93 | router.put('/:id', function ({params, body}: {params: {id: string}; body: IAlterItems['Request']['data']}, res, next) {
94 | const ids = params.id.split(',');
95 | ids.forEach((id) => {
96 | const item = database.articles[id];
97 | if (item) {
98 | Object.assign(item, body);
99 | }
100 | });
101 | setTimeout(() => res.json({}), 500);
102 | });
103 |
104 | router.post('/', function ({body}: {body: ICreateItem['Request']}, res, next) {
105 | const id = (Object.keys(database.articles).length + 1).toString();
106 | const members = database.members;
107 | const memberId = Object.keys(database.members).pop() as string;
108 | const author = members[memberId];
109 | database.articles[id] = {...body, author: [author.id, author.name].join(','), createdTime: Date.now(), status: 'pending' as Status, id};
110 | author.articles++;
111 | const result: ICreateItem['Response'] = {id};
112 | setTimeout(() => res.json(result), 500);
113 | });
114 |
115 | router.delete('/:id', function ({params}: {params: {id: string}}, res, next) {
116 | const ids = params.id.split(',');
117 | ids.forEach((id) => {
118 | delete database.articles[id];
119 | });
120 | setTimeout(() => res.json({}), 500);
121 | });
122 |
123 | export default router;
124 |
--------------------------------------------------------------------------------
/mock/src/routes/member.ts:
--------------------------------------------------------------------------------
1 | import {Router} from 'express';
2 | import {database} from '../database';
3 | import type {IAlterItems, ICreateItem, IGetItem, IGetList, ListItem} from '@elux-admin-antd/member/entity';
4 |
5 | type Query = {[K in keyof T]: string};
6 |
7 | const router = Router();
8 |
9 | router.get('/', function ({query}: {query: Query}, res, next) {
10 | const {pageCurrent, pageSize, name, nickname, role, status, email, sorterField, sorterOrder} = {
11 | pageCurrent: parseInt(query.pageCurrent || '1'),
12 | pageSize: parseInt(query.pageSize || '10'),
13 | name: query.name || '',
14 | nickname: query.nickname || '',
15 | role: query.role || '',
16 | status: query.status || '',
17 | email: query.email || '',
18 | sorterField: query.sorterField || '',
19 | sorterOrder: query.sorterOrder || '',
20 | };
21 |
22 | const start = (pageCurrent - 1) * pageSize;
23 | const end = start + pageSize;
24 |
25 | const dataMap = database.members;
26 | let listData = Object.keys(dataMap)
27 | .reverse()
28 | .map((id) => {
29 | return dataMap[id];
30 | });
31 |
32 | if (name) {
33 | listData = listData.filter((item) => item.name.includes(name));
34 | }
35 | if (nickname) {
36 | listData = listData.filter((item) => item.nickname.includes(nickname));
37 | }
38 | if (role) {
39 | listData = listData.filter((item) => item.role === role);
40 | }
41 | if (status) {
42 | listData = listData.filter((item) => item.status === status);
43 | }
44 | if (email) {
45 | listData = listData.filter((item) => item.email === email);
46 | }
47 |
48 | if (sorterField === 'createdTime') {
49 | if (sorterOrder === 'ascend') {
50 | listData.sort((a: ListItem, b: ListItem) => {
51 | return a.createdTime - b.createdTime;
52 | });
53 | } else if (sorterOrder === 'descend') {
54 | listData.sort((a, b) => {
55 | return b.createdTime - a.createdTime;
56 | });
57 | }
58 | }
59 | if (sorterField === 'name') {
60 | if (sorterOrder === 'ascend') {
61 | listData.sort((a: ListItem, b: ListItem) => {
62 | return a.name.charCodeAt(0) - b.name.charCodeAt(0);
63 | });
64 | } else if (sorterOrder === 'descend') {
65 | listData.sort((a, b) => {
66 | return b.name.charCodeAt(0) - a.name.charCodeAt(0);
67 | });
68 | }
69 | }
70 | if (sorterField === 'articles') {
71 | if (sorterOrder === 'ascend') {
72 | listData.sort((a: ListItem, b: ListItem) => {
73 | return a.articles - b.articles;
74 | });
75 | } else if (sorterOrder === 'descend') {
76 | listData.sort((a, b) => {
77 | return b.articles - a.articles;
78 | });
79 | }
80 | }
81 |
82 | const result: IGetList['Response'] = {
83 | listSummary: {
84 | pageCurrent,
85 | pageSize,
86 | totalItems: listData.length,
87 | totalPages: Math.ceil(listData.length / pageSize),
88 | },
89 | list: listData.slice(start, end).map((item) => ({...item, content: ''})),
90 | };
91 |
92 | setTimeout(() => res.json(result), 500);
93 | });
94 |
95 | router.get('/:id', function ({params}: {params: IGetItem['Request']}, res, next) {
96 | const {id} = params;
97 | const item = database.members[id];
98 | if (!item) {
99 | res.status(404).end();
100 | } else {
101 | const result: IGetItem['Response'] = item;
102 | setTimeout(() => res.json(result), 500);
103 | }
104 | });
105 |
106 | router.put('/:id', function ({params, body}: {params: {id: string}; body: IAlterItems['Request']['data']}, res, next) {
107 | const ids = params.id.split(',');
108 | ids.forEach((id) => {
109 | const item = database.members[id];
110 | if (item) {
111 | Object.assign(item, body);
112 | }
113 | });
114 | setTimeout(() => res.json({}), 500);
115 | });
116 |
117 | router.post('/', function ({body}: {body: ICreateItem['Request']}, res, next) {
118 | const id = (Object.keys(database.members).length + 1).toString();
119 | database.members[id] = {...body, articles: 0, createdTime: Date.now(), id};
120 | const result: ICreateItem['Response'] = {id};
121 | setTimeout(() => res.json(result), 500);
122 | });
123 |
124 | router.delete('/:id', function ({params}: {params: {id: string}}, res, next) {
125 | const ids = params.id.split(',');
126 | ids.forEach((id) => {
127 | delete database.members[id];
128 | });
129 | setTimeout(() => res.json({}), 500);
130 | });
131 |
132 | export default router;
133 |
--------------------------------------------------------------------------------
/src/modules/stage/views/ForgotPassword/index.tsx:
--------------------------------------------------------------------------------
1 | import {LockOutlined, MobileOutlined, NumberOutlined} from '@ant-design/icons-vue';
2 | import {Link, connectStore} from '@elux/vue-web';
3 | import {Button, Form, FormInstance, Input} from 'ant-design-vue';
4 | import {defineComponent, onBeforeUnmount, reactive, ref} from 'vue';
5 | import {GetActions} from '@/Global';
6 | import DialogPage from '../../components/DialogPage';
7 | import {ResetPasswordParams} from '../../entity';
8 | import {getFormDecorators} from '../../utils/tools';
9 | import styles from './index.module.less';
10 |
11 | interface HFormData extends Required {
12 | confirm: string;
13 | }
14 |
15 | const {stage: stageActions} = GetActions('stage');
16 |
17 | const Component = defineComponent({
18 | name: styles.root,
19 | setup() {
20 | const storeProps = connectStore();
21 | const formRef = ref();
22 | const formState = reactive({
23 | phone: '',
24 | password: '',
25 | confirm: '',
26 | captcha: '',
27 | });
28 | const countDown = ref(0);
29 | onBeforeUnmount(() => (countDown.value = 0));
30 | const checkCountDown = () => {
31 | if (countDown.value > 0) {
32 | countDown.value--;
33 | setTimeout(checkCountDown, 1000);
34 | }
35 | };
36 |
37 | const sendCaptcha = async () => {
38 | const phone = formState.phone;
39 | if (!phone) {
40 | formRef.value?.validateFields('phone');
41 | } else {
42 | await storeProps.dispatch(stageActions.sendCaptcha({phone}));
43 | countDown.value = 60;
44 | setTimeout(checkCountDown, 1000);
45 | }
46 | };
47 | const onSubmit = (values: HFormData) => {
48 | storeProps.dispatch(stageActions.resetPassword(values));
49 | };
50 | const confirmValidator = (rules: any, value: string) => {
51 | if (!value || formState.password === value) {
52 | return Promise.resolve();
53 | }
54 | return Promise.reject('2次密码输入不一致!');
55 | };
56 | const fromDecorators = getFormDecorators({
57 | phone: {rules: [{required: true, message: '请输入注册手机号!', whitespace: true}]},
58 | password: {rules: [{required: true, message: '请输入新密码!', whitespace: true}]},
59 | captcha: {rules: [{required: true, message: '请输入短信验证码!', whitespace: true}]},
60 | confirm: {rules: [{required: true, message: '请再次输入密码!', whitespace: true}, {validator: confirmValidator}]},
61 | });
62 | return () => {
63 | return (
64 |
65 |
66 |
68 | } placeholder="注册手机" />
69 |
70 |
71 | }
75 | placeholder="新密码"
76 | autocomplete="new-password"
77 | />
78 |
79 |
80 | }
84 | placeholder="确认密码"
85 | autocomplete="new-password"
86 | />
87 |
88 |
89 |
90 | } placeholder="短信验证码" style="width:250px" />
91 |
92 |
95 |
96 |
97 |
98 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | );
110 | };
111 | },
112 | });
113 |
114 | export default Component;
115 |
--------------------------------------------------------------------------------
/src/modules/stage/views/RegistryForm/index.tsx:
--------------------------------------------------------------------------------
1 | import {AliwangwangFilled, LockOutlined, UserOutlined} from '@ant-design/icons-vue';
2 | import {Link, connectStore} from '@elux/vue-web';
3 | import {Button, Checkbox, Form, FormInstance, Input} from 'ant-design-vue';
4 | import {defineComponent, reactive, ref} from 'vue';
5 | import {APPState, GetActions} from '@/Global';
6 | import DialogPage from '../../components/DialogPage';
7 | import {RegisterParams} from '../../entity';
8 | import {LoginUrl} from '../../utils/const';
9 | import {getFormDecorators} from '../../utils/tools';
10 | import styles from './index.module.less';
11 |
12 | interface HFormData extends Required {
13 | confirm: string;
14 | agreement: boolean;
15 | }
16 |
17 | const agreementChecked = (rule: any, value: string) => {
18 | if (!value) {
19 | return Promise.reject('您必须同意注册协议!');
20 | }
21 | return Promise.resolve();
22 | };
23 |
24 | const {stage: stageActions} = GetActions('stage');
25 |
26 | interface StoreProps {
27 | fromUrl?: string;
28 | }
29 |
30 | function mapStateToProps(appState: APPState): StoreProps {
31 | const {fromUrl} = appState.stage!;
32 | return {
33 | fromUrl,
34 | };
35 | }
36 |
37 | const Component = defineComponent({
38 | name: styles.root,
39 | setup() {
40 | const storeProps = connectStore(mapStateToProps);
41 | const formRef = ref();
42 | const formState = reactive({
43 | username: '',
44 | password: '',
45 | confirm: '',
46 | agreement: false,
47 | });
48 | const confirmValidator = (rules: any, value: string) => {
49 | if (!value || formState.password === value) {
50 | return Promise.resolve();
51 | }
52 | return Promise.reject('2次密码输入不一致!');
53 | };
54 | const fromDecorators = getFormDecorators({
55 | username: {rules: [{required: true, message: '请输入用户名!', whitespace: true}]},
56 | password: {rules: [{required: true, message: '请输入密码!', whitespace: true}]},
57 | agreement: {rules: [{validator: agreementChecked}]},
58 | confirm: {rules: [{required: true, message: '请再次输入密码!', whitespace: true}, {validator: confirmValidator}]},
59 | });
60 | const onSubmit = (values: HFormData) => {
61 | storeProps.dispatch(stageActions.registry(values));
62 | };
63 |
64 | return () => {
65 | const {fromUrl} = storeProps;
66 | return (
67 |
68 |
69 |
71 | } placeholder="用户名" />
72 |
73 |
74 | }
78 | placeholder="密码"
79 | autocomplete="new-password"
80 | />
81 |
82 |
83 | }
87 | placeholder="确认密码"
88 | autocomplete="new-password"
89 | />
90 |
91 |
92 |
93 | 我已阅读并同意
94 |
95 |
96 | 注册协议
97 |
98 |
99 |
100 |
101 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
116 |
117 |
118 | );
119 | };
120 | },
121 | });
122 |
123 | export default Component;
124 |
--------------------------------------------------------------------------------
/src/modules/stage/utils/tools.ts:
--------------------------------------------------------------------------------
1 | import {Rule} from 'ant-design-vue/lib/form';
2 | import {VNode, shallowReactive, watch} from 'vue';
3 |
4 | export {message} from 'ant-design-vue';
5 |
6 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
7 | export function usePropsState>(map: () => T) {
8 | const limit = shallowReactive({} as T);
9 | watch(
10 | map,
11 | (val) => {
12 | Object.assign(limit, val);
13 | },
14 | {immediate: true}
15 | );
16 | return limit;
17 | }
18 |
19 | export interface FormDecorator {
20 | label?: string;
21 | dependencies?: T[];
22 | rules?: Rule[];
23 | valuePropName?: string;
24 | }
25 |
26 | export interface SearchFormDecorator {
27 | name: T;
28 | formItem: VNode;
29 | label: string;
30 | rules?: Rule[];
31 | col?: number;
32 | cite?: number;
33 | }
34 |
35 | export type SearchFromItems = Array>;
36 |
37 | export function getFormDecorators(items: {[key in keyof TFormData]: FormDecorator}): {
38 | [key in keyof TFormData]: FormDecorator & {name: string};
39 | } {
40 | Object.entries(items).forEach(([key, item]: [string, any]) => {
41 | item.name = key;
42 | });
43 | return items as any;
44 | }
45 |
46 | export function arrayToMap(arr: T[], key = 'id'): {[key: string]: T} {
47 | return arr.reduce((pre, cur) => {
48 | pre[cur[key]] = cur;
49 | return pre;
50 | }, {});
51 | }
52 |
53 | export function loadingPlaceholder(val: string | undefined): string {
54 | return val || '...';
55 | }
56 |
57 | export function splitIdName(str: string): {id: string; name: string} {
58 | if (str.includes(',')) {
59 | const [id] = str.split(',', 1);
60 | return {id, name: str.replace(id + ',', '')};
61 | } else {
62 | return {id: str, name: str};
63 | }
64 | }
65 |
66 | // export function urlPushQuery(url: string, query: {[key: string]: any} = {}): string {
67 | // const queryStr = stringify(query);
68 | // return queryStr ? `${url}${url.indexOf('?') > -1 ? '&' : '?'}${queryStr}` : url;
69 | // }
70 |
71 | function isMapObject(obj: any): Boolean {
72 | return typeof obj === 'object' && obj !== null && !Array.isArray(obj);
73 | }
74 |
75 | export function mergeDefaultParams(defaultParams: T, targetParams: {[key: string]: any}): T {
76 | return Object.keys(defaultParams).reduce((result, key) => {
77 | const defVal = defaultParams[key];
78 | const tgtVal = targetParams[key];
79 | if (tgtVal == undefined) {
80 | result[key] = defVal;
81 | } else if (isMapObject(defVal) && isMapObject(tgtVal)) {
82 | result[key] = mergeDefaultParams(defVal, tgtVal);
83 | } else {
84 | result[key] = tgtVal;
85 | }
86 | return result;
87 | }, {}) as any;
88 | }
89 |
90 | export function excludeDefaultParams(defaultParams: {[key: string]: any}, targetParams: {[key: string]: any}): {[key: string]: any} | undefined {
91 | const result: any = {};
92 | let hasSub = false;
93 | Object.keys(targetParams).forEach((key) => {
94 | let tgtVal = targetParams[key];
95 | const defVal = defaultParams[key];
96 | if (tgtVal !== defVal) {
97 | if (isMapObject(defVal) && isMapObject(tgtVal)) {
98 | tgtVal = excludeDefaultParams(defVal, tgtVal);
99 | }
100 | if (tgtVal !== undefined) {
101 | hasSub = true;
102 | result[key] = tgtVal;
103 | }
104 | }
105 | });
106 | return hasSub ? result : undefined;
107 | }
108 |
109 | // fork from 'fast-deep-equal'
110 | export function deepEqual(a: any, b: any): boolean {
111 | if (a === b) return true;
112 |
113 | if (a && b && typeof a == 'object' && typeof b == 'object') {
114 | if (a.constructor !== b.constructor) return false;
115 |
116 | let length: number, i: number;
117 | if (Array.isArray(a)) {
118 | length = a.length;
119 | if (length != b.length) return false;
120 | for (i = length; i-- !== 0; ) if (!deepEqual(a[i], b[i])) return false;
121 | return true;
122 | }
123 |
124 | if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags;
125 | if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf();
126 | if (a.toString !== Object.prototype.toString) return a.toString() === b.toString();
127 |
128 | const keys = Object.keys(a);
129 | length = keys.length;
130 | if (length !== Object.keys(b).length) return false;
131 |
132 | for (i = length; i-- !== 0; ) if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false;
133 |
134 | for (i = length; i-- !== 0; ) {
135 | const key = keys[i];
136 |
137 | if (!deepEqual(a[key], b[key])) return false;
138 | }
139 |
140 | return true;
141 | }
142 |
143 | return a !== a && b !== b;
144 | }
145 |
--------------------------------------------------------------------------------
/src/modules/stage/components/MSelect/index.tsx:
--------------------------------------------------------------------------------
1 | import {CloseCircleFilled, CloseOutlined, FullscreenOutlined} from '@ant-design/icons-vue';
2 | import {Form} from 'ant-design-vue';
3 | import {computed, defineComponent} from 'vue';
4 | import {useRouter} from '@/Global';
5 | import {BaseLocationState} from '../../utils/base';
6 | import {DialogPageClassname} from '../../utils/const';
7 | import styles from './index.module.less';
8 |
9 | interface Props> {
10 | class?: string;
11 | selectorPathname: string;
12 | showSearch?: boolean;
13 | fixedSearch?: Partial;
14 | limit?: number | [number, number];
15 | placeholder?: string;
16 | rowKey?: string;
17 | rowName?: string;
18 | value?: string | string[];
19 | returnArray?: boolean;
20 | onChange?: (value?: string | string[]) => void;
21 | }
22 |
23 | const Component = defineComponent({
24 | name: styles.root,
25 | props: [
26 | 'class',
27 | 'selectorPathname',
28 | 'showSearch',
29 | 'fixedSearch',
30 | 'limit',
31 | 'placeholder',
32 | 'rowKey',
33 | 'rowName',
34 | 'value',
35 | 'returnArray',
36 | 'onChange',
37 | ] as any,
38 | emits: ['update:value'],
39 | setup(props, {emit}) {
40 | const formItemContext = Form.useInjectFormItemContext();
41 | const triggerChange = (changedValue: string | string[]) => {
42 | emit('update:value', changedValue);
43 | formItemContext.onFieldChange();
44 | };
45 | const selectedRows = computed(() => {
46 | const {value, rowKey = 'id', rowName = 'name'} = props;
47 | const arr = value ? (typeof value === 'string' ? [value] : value) : [];
48 | return arr.map((item) => {
49 | const [id, ...others] = item.split(',');
50 | const name = others.join(',');
51 | return {[rowKey]: id, [rowName]: name || id};
52 | });
53 | });
54 | const removeItem = (index: number) => {
55 | const {rowKey = 'id', rowName = 'name', returnArray} = props;
56 | const rows = selectedRows.value
57 | .slice(0, index)
58 | .concat(selectedRows.value.slice(index + 1))
59 | .map((row) => [row[rowKey], row[rowName]].join(','));
60 | triggerChange(rows.length === 1 && !returnArray ? rows[0] : rows);
61 | };
62 | const onSelectedSubmit = (selectedItems: Record[]) => {
63 | const {rowKey = 'id', rowName = 'name', returnArray} = props;
64 | const rows = selectedItems.map((item) => [item[rowKey], item[rowName]].filter(Boolean).join(','));
65 | triggerChange(rows.length === 1 && !returnArray ? rows[0] : rows);
66 | };
67 | const removeAll = () => {
68 | const {returnArray} = props;
69 | triggerChange(returnArray ? [] : '');
70 | };
71 | const router = useRouter();
72 |
73 | const onSelect = () => {
74 | const {limit, selectorPathname, showSearch, fixedSearch} = props;
75 | const state: BaseLocationState = {selectLimit: limit, selectedRows: selectedRows.value, showSearch, fixedSearch, onSelectedSubmit};
76 | router.push({pathname: selectorPathname, searchQuery: fixedSearch, classname: DialogPageClassname, state}, 'window');
77 | };
78 |
79 | const children = computed(() => {
80 | const {rowKey = 'id', rowName = 'name', placeholder} = props;
81 | return (
82 | <>
83 |
84 |
85 | {selectedRows.value.map((row, index) => (
86 |
87 |
88 | {row[rowName]}
89 |
100 |
101 |
102 | ))}
103 |
104 | {placeholder && selectedRows.value.length < 1 && {placeholder}}
105 |
106 |
109 | {selectedRows.value.length > 0 && (
110 |
113 | )}
114 | >
115 | );
116 | });
117 |
118 | return () => {
119 | const {class: className = ''} = props;
120 | return (
121 | {children.value}
122 | );
123 | };
124 | },
125 | }) as any;
126 |
127 | export default Component as (props: Props) => JSX.Element;
128 |
--------------------------------------------------------------------------------
/src/modules/article/views/ListTable.tsx:
--------------------------------------------------------------------------------
1 | import MTable, {MBatchActions, MColumns, MSelection} from '@elux-admin-antd/stage/components/MTable';
2 | import {DialogPageClassname} from '@elux-admin-antd/stage/utils/const';
3 | import {useSingleWindow, useTableChange, useTableSize} from '@elux-admin-antd/stage/utils/resource';
4 | import {splitIdName} from '@elux-admin-antd/stage/utils/tools';
5 | import {Link, LoadingState, connectStore} from '@elux/vue-web';
6 | import {Tooltip} from 'ant-design-vue';
7 | import {ColumnProps} from 'ant-design-vue/lib/table';
8 | import {VNode, computed, defineComponent} from 'vue';
9 | import {APPState} from '@/Global';
10 | import {DStatus, ListItem, ListSearch, ListSummary, Status, defaultListSearch} from '../entity';
11 |
12 | interface StoreProps {
13 | listSearch: ListSearch;
14 | list?: ListItem[];
15 | listSummary?: ListSummary;
16 | listLoading?: LoadingState;
17 | }
18 |
19 | interface Props {
20 | listPathname: string;
21 | mergeColumns?: {[field: string]: MColumns};
22 | actionColumns?: ColumnProps;
23 | commonActions?: VNode;
24 | batchActions?: MBatchActions;
25 | selectedRows?: Partial[];
26 | selection?: MSelection;
27 | }
28 |
29 | const mapStateToProps: (state: APPState) => StoreProps = (state) => {
30 | const {listSearch, list, listSummary, listLoading} = state.article!;
31 | return {listSearch, list, listSummary, listLoading};
32 | };
33 |
34 | const Component = defineComponent({
35 | props: ['listPathname', 'mergeColumns', 'actionColumns', 'commonActions', 'batchActions', 'selectedRows', 'selection'] as any,
36 | setup(props) {
37 | const storeProps = connectStore(mapStateToProps);
38 | const onTableChange = useTableChange(storeProps, props.listPathname, defaultListSearch);
39 | const singleWindow = useSingleWindow();
40 | const tableSize = useTableSize();
41 | const columns = computed[]>(() => {
42 | const {actionColumns, mergeColumns} = props;
43 | const cols: MColumns[] = [
44 | {
45 | title: '标题',
46 | dataIndex: 'title',
47 | ellipsis: {showTitle: false},
48 | customRender: ({value, record}: {value: string; record: {id: string}}) => (
49 |
50 |
51 | {value}
52 |
53 |
54 | ),
55 | },
56 | {
57 | title: '作者',
58 | dataIndex: 'author',
59 | width: '10%',
60 | sorter: true,
61 | customRender: ({value}: {value: string}) => {
62 | const {id, name} = splitIdName(value);
63 | return (
64 |
65 | {name}
66 |
67 | );
68 | },
69 | },
70 | {
71 | title: '责任编辑',
72 | dataIndex: 'editors',
73 | width: '20%',
74 | className: 'g-actions',
75 | customRender: ({value}: {value: string[]}) =>
76 | value.map((editor) => {
77 | const {id, name} = splitIdName(editor);
78 | return (
79 |
80 | {name}
81 |
82 | );
83 | }),
84 | },
85 | {
86 | title: '创建时间',
87 | dataIndex: 'createdTime',
88 | width: '200px',
89 | sorter: true,
90 | timestamp: true,
91 | },
92 | {
93 | title: '状态',
94 | dataIndex: 'status',
95 | width: '100px',
96 | customRender: ({value}: {value: string}) => (
97 |
98 | {DStatus.valueToLabel[value]}
99 |
100 | ),
101 | },
102 | ];
103 |
104 | if (actionColumns) {
105 | cols.push(actionColumns);
106 | }
107 | if (mergeColumns) {
108 | cols.forEach((col) => {
109 | const field = col.dataIndex as string;
110 | if (field && mergeColumns[field]) {
111 | Object.assign(col, mergeColumns[field]);
112 | }
113 | });
114 | }
115 | return cols;
116 | });
117 |
118 | return () => {
119 | const {commonActions, batchActions, selectedRows, selection} = props;
120 | const {listSearch, list, listSummary, listLoading} = storeProps;
121 | return (
122 |
123 | size={tableSize}
124 | commonActions={commonActions}
125 | batchActions={batchActions}
126 | onChange={onTableChange}
127 | selectedRows={selectedRows}
128 | columns={columns.value}
129 | listSearch={listSearch}
130 | dataSource={list}
131 | listSummary={listSummary}
132 | selection={selection}
133 | loading={listLoading === 'Start' || listLoading === 'Depth'}
134 | />
135 | );
136 | };
137 | },
138 | });
139 |
140 | export default Component;
141 |
--------------------------------------------------------------------------------
/public/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
--------------------------------------------------------------------------------
/src/modules/member/views/ListTable.tsx:
--------------------------------------------------------------------------------
1 | import MTable, {MBatchActions, MColumns, MSelection} from '@elux-admin-antd/stage/components/MTable';
2 | import {DialogPageClassname} from '@elux-admin-antd/stage/utils/const';
3 | import {useSingleWindow, useTableChange, useTableSize} from '@elux-admin-antd/stage/utils/resource';
4 | import {Link, LoadingState, connectStore} from '@elux/vue-web';
5 | import {Tooltip} from 'ant-design-vue';
6 | import {ColumnProps} from 'ant-design-vue/lib/table';
7 | import {VNode, computed, defineComponent} from 'vue';
8 | import {APPState} from '@/Global';
9 | import {DGender, DRole, DStatus, ListItem, ListSearch, ListSummary, defaultListSearch} from '../entity';
10 |
11 | interface StoreProps {
12 | listSearch: ListSearch;
13 | list?: ListItem[];
14 | listSummary?: ListSummary;
15 | listLoading?: LoadingState;
16 | }
17 |
18 | interface Props {
19 | listPathname: string;
20 | mergeColumns?: {[field: string]: MColumns};
21 | actionColumns?: ColumnProps;
22 | commonActions?: VNode;
23 | batchActions?: MBatchActions;
24 | selectedRows?: Partial[];
25 | selection?: MSelection;
26 | }
27 |
28 | const mapStateToProps: (state: APPState) => StoreProps = (state) => {
29 | const {listSearch, list, listSummary, listLoading} = state.member!;
30 | return {listSearch, list, listSummary, listLoading};
31 | };
32 |
33 | const Component = defineComponent({
34 | props: ['listPathname', 'mergeColumns', 'actionColumns', 'commonActions', 'batchActions', 'selectedRows', 'selection'] as any,
35 | setup(props) {
36 | const storeProps = connectStore(mapStateToProps);
37 | const onTableChange = useTableChange(storeProps, props.listPathname, defaultListSearch);
38 | const singleWindow = useSingleWindow();
39 | const tableSize = useTableSize();
40 | const columns = computed[]>(() => {
41 | const {actionColumns, mergeColumns} = props;
42 | const cols: MColumns[] = [
43 | {
44 | title: '用户名',
45 | dataIndex: 'name',
46 | width: '10%',
47 | sorter: true,
48 | customRender: ({value, record}: {value: string; record: {id: string}}) => (
49 |
50 | {value}
51 |
52 | ),
53 | },
54 | {
55 | title: '呢称',
56 | dataIndex: 'nickname',
57 | width: '10%',
58 | },
59 | {
60 | title: '角色',
61 | dataIndex: 'role',
62 | width: '10%',
63 | customRender: ({value}: {value: string}) => DRole.valueToLabel[value],
64 | },
65 | {
66 | title: '性别',
67 | dataIndex: 'gender',
68 | align: 'center',
69 | width: '100px',
70 | customRender: ({value}: {value: string}) => DGender.valueToLabel[value],
71 | },
72 | {
73 | title: '文章数',
74 | dataIndex: 'articles',
75 | align: 'center',
76 | sorter: true,
77 | width: '120px',
78 | customRender: ({value, record}: {value: string; record: {id: string}}) => (
79 |
80 | {value}
81 |
82 | ),
83 | },
84 | {
85 | title: 'Email',
86 | dataIndex: 'email',
87 | ellipsis: {showTitle: false},
88 | customRender: ({value}: {value: string}) => (
89 |
90 | {value}
91 |
92 | ),
93 | },
94 | {
95 | title: '注册时间',
96 | dataIndex: 'createdTime',
97 | width: '200px',
98 | sorter: true,
99 | timestamp: true,
100 | },
101 | {
102 | title: '状态',
103 | dataIndex: 'status',
104 | width: '100px',
105 | customRender: ({value}: {value: string}) => {DStatus.valueToLabel[value]},
106 | },
107 | ];
108 | if (actionColumns) {
109 | cols.push(actionColumns);
110 | }
111 | if (mergeColumns) {
112 | cols.forEach((col) => {
113 | const field = col.dataIndex as string;
114 | if (field && mergeColumns[field]) {
115 | Object.assign(col, mergeColumns[field]);
116 | }
117 | });
118 | }
119 | return cols;
120 | });
121 |
122 | return () => {
123 | const {commonActions, batchActions, selectedRows, selection} = props;
124 | const {listSearch, list, listSummary, listLoading} = storeProps;
125 | return (
126 |
127 | size={tableSize}
128 | commonActions={commonActions}
129 | batchActions={batchActions}
130 | onChange={onTableChange}
131 | selectedRows={selectedRows}
132 | columns={columns.value}
133 | listSearch={listSearch}
134 | dataSource={list}
135 | listSummary={listSummary}
136 | selection={selection}
137 | loading={listLoading === 'Start' || listLoading === 'Depth'}
138 | />
139 | );
140 | };
141 | },
142 | });
143 |
144 | export default Component;
145 |
--------------------------------------------------------------------------------
/src/modules/admin/model.ts:
--------------------------------------------------------------------------------
1 | //定义本模块的业务模型
2 | import {BaseModel, effect, reducer} from '@elux/vue-web';
3 | import {pathToRegexp} from 'path-to-regexp';
4 | import {APPState} from '@/Global';
5 | import api from './api';
6 | import {MenuData, Notices, SubModule} from './entity';
7 | import type {Tab, TabData} from './entity';
8 |
9 | //定义本模块的状态结构
10 | export interface ModuleState {
11 | subModule?: SubModule; //该字段用来记录当前路由下展示哪个子Module
12 | notices?: Notices; //该字段用来记录实时通知信息
13 | siderCollapsed?: boolean; //左边菜单是否折叠
14 | tabEdit?: Tab;
15 | tabData: TabData;
16 | curTab: Tab;
17 | menuData: MenuData;
18 | menuSelected: {selected: string[]; open: string[]};
19 | //该字段用来记录当前Page是否用Dialog模式展示(非全屏)
20 | //Dialog模式时将不渲染Layout
21 | dialogMode: boolean;
22 | }
23 |
24 | //定义路由中的本模块感兴趣的信息
25 | export interface RouteParams {
26 | subModule?: SubModule;
27 | }
28 |
29 | export class Model extends BaseModel {
30 | protected routeParams!: RouteParams; //保存从当前路由中提取的信息结果
31 |
32 | //因为要尽量避免使用public方法,所以构建this.privateActions来引用私有actions
33 | protected privateActions = this.getPrivateActions({});
34 |
35 | //提取当前路由中的本模块感兴趣的信息
36 | protected getRouteParams(): RouteParams {
37 | const {pathname} = this.getRouter().location;
38 | const [, , subModuleStr = ''] = pathToRegexp('/:admin/:subModule', undefined, {end: false}).exec(pathname) || [];
39 | const subModule: SubModule | undefined = SubModule[subModuleStr] || undefined;
40 | return {subModule};
41 | }
42 |
43 | //初始化或路由变化时都需要重新挂载Model
44 | //在此钩子中必需完成ModuleState的初始赋值,可以异步
45 | public onMount(env: 'init' | 'route' | 'update'): void | Promise {
46 | this.routeParams = this.getRouteParams();
47 | const {
48 | location: {classname},
49 | } = this.getRouter();
50 | const {subModule} = this.routeParams;
51 | const curTab = this.getCurTab();
52 | const dialogMode = classname.startsWith('_');
53 | const prevState = this.getPrevState();
54 | const initCallback = ([tabData, menuData]: [TabData, MenuData]) => {
55 | const menuSelected = this.matchMenu(menuData);
56 | const initState: ModuleState = {
57 | ...prevState,
58 | curTab,
59 | subModule,
60 | dialogMode,
61 | tabData,
62 | menuData,
63 | menuSelected,
64 | tabEdit: undefined,
65 | };
66 | this.dispatch(this.privateActions._initState(initState));
67 | };
68 | if (prevState) {
69 | //复用之前的state
70 | return initCallback([prevState.tabData, prevState.menuData]);
71 | } else {
72 | return Promise.all([api.getTabData(), api.getMenuData()]).then(initCallback);
73 | }
74 | }
75 |
76 | @effect(null)
77 | public clickMenu(key: string): void {
78 | const link = this.state.menuData.keyToLink[key];
79 | if (link) {
80 | if (link.startsWith('/')) {
81 | this.getRouter().push({url: link}, 'page');
82 | } else {
83 | window.open(link);
84 | }
85 | }
86 | }
87 |
88 | @reducer
89 | public putSiderCollapsed(siderCollapsed: boolean): void {
90 | this.state.siderCollapsed = siderCollapsed;
91 | }
92 |
93 | @effect(null)
94 | public clickTab(id: string): void {
95 | if (id) {
96 | const item = this.state.tabData.maps[id];
97 | if (id === this.state.curTab.id) {
98 | this.dispatch(this.privateActions._updateState('showTabEditor', {tabEdit: item}));
99 | } else {
100 | this.getRouter().push({url: item.url}, 'page');
101 | }
102 | } else {
103 | this.dispatch(this.privateActions._updateState('showTabEditor', {tabEdit: {...this.state.curTab, title: document.title}}));
104 | }
105 | }
106 |
107 | @reducer
108 | public closeTabEditor(): void {
109 | this.state.tabEdit = undefined;
110 | }
111 |
112 | @effect(null)
113 | public async updateTab(item: Tab): Promise {
114 | let {list, maps} = this.state.tabData;
115 | if (maps[item.id]) {
116 | list = list.map((tab) => (tab.id === item.id ? item : tab));
117 | } else {
118 | list = [item, ...list];
119 | }
120 | maps = {...maps, [item.id]: item};
121 | const tabData = {list, maps};
122 | await api.updateTabData(tabData);
123 | this.dispatch(this.privateActions._updateState('updateTab', {tabData, tabEdit: undefined}));
124 | }
125 |
126 | @effect(null)
127 | public async deleteTab(id: string): Promise {
128 | let {list, maps} = this.state.tabData;
129 | if (maps[id]) {
130 | list = list.filter((tab) => tab.id !== id);
131 | maps = {...maps};
132 | delete maps[id];
133 | const tabData = {list, maps};
134 | await api.updateTabData(tabData);
135 | this.dispatch(this.privateActions._updateState('updateTab', {tabData}));
136 | }
137 | }
138 |
139 | private getCurTab(): Tab {
140 | const {pathname, search, url} = this.getRouter().location;
141 | let id = pathname;
142 | if (search) {
143 | id += `?${search.split('&').sort().join('&')}`;
144 | }
145 | return {id, url, title: ''};
146 | }
147 |
148 | private matchMenu({matchToKey, keyToParents}: MenuData): {selected: string[]; open: string[]} {
149 | const pathname = this.getRouter().location.pathname;
150 | for (const rule in matchToKey) {
151 | if (pathname.startsWith(rule)) {
152 | const selected = matchToKey[rule];
153 | return {selected: [selected], open: [...keyToParents[selected]]};
154 | }
155 | }
156 | return {selected: [], open: []};
157 | }
158 | private hasLogin(): boolean {
159 | return this.getRootState().stage!.curUser.hasLogin;
160 | }
161 |
162 | private getNotices = () => {
163 | if (this.hasLogin()) {
164 | api.getNotices().then((notices) => {
165 | curNotices = notices;
166 | this.dispatch(this.privateActions._updateState('updataNotices', {notices}));
167 | });
168 | }
169 | };
170 |
171 | //页面被激活(变为显示页面)时调用
172 | onActive(): void {
173 | if (curNotices && this.state.notices !== curNotices) {
174 | this.dispatch(this.privateActions._updateState('updataNotices', {notices: curNotices}));
175 | }
176 | if (!curLoopTask) {
177 | curLoopTask = this.getNotices;
178 | curLoopTask();
179 | } else {
180 | curLoopTask = this.getNotices;
181 | }
182 | }
183 | }
184 |
185 | let curLoopTask: (() => void) | undefined;
186 | let curNotices: Notices | undefined;
187 | setInterval(() => curLoopTask && curLoopTask(), 10000);
188 |
--------------------------------------------------------------------------------
/src/modules/stage/model.ts:
--------------------------------------------------------------------------------
1 | //定义本模块的业务模型
2 | import {BaseModel, ErrorCodes, LoadingState, effect, reducer} from '@elux/vue-web';
3 | import {pathToRegexp} from 'path-to-regexp';
4 | import {APPState} from '@/Global';
5 | import api, {guest} from './api';
6 | import {CurView, SubModule} from './entity';
7 | import {AdminHomeUrl, GuestHomeUrl, LoginUrl} from './utils/const';
8 | import {CommonErrorCode, CustomError} from './utils/errors';
9 | import {message} from './utils/tools';
10 | import type {CurUser, LoginParams, RegisterParams, ResetPasswordParams, SendCaptchaParams} from './entity';
11 |
12 | //定义本模块的状态结构
13 | export interface ModuleState {
14 | curUser: CurUser;
15 | subModule?: SubModule; //该字段用来记录当前路由下展示哪个子Module
16 | curView?: CurView; //该字段用来记录当前路由下展示本模块的哪个View
17 | fromUrl?: string; //登录或注册后返回的原来页面
18 | globalLoading?: LoadingState; //该字段用来记录一个全局的loading状态
19 | error?: string; //该字段用来记录启动错误,如果该字段有值,则不渲染其它UI
20 | }
21 |
22 | //定义路由中的本模块感兴趣的信息
23 | export interface RouteParams {
24 | pathname: string;
25 | subModule?: SubModule;
26 | curView?: CurView;
27 | fromUrl?: string;
28 | }
29 |
30 | //定义本模块的业务模型,必需继承BaseModel
31 | //模型中的属性和方法尽量使用非public
32 | export class Model extends BaseModel {
33 | protected routeParams!: RouteParams; //保存从当前路由中提取的信息结果
34 |
35 | //因为要尽量避免使用public方法,所以构建this.privateActions来引用私有actions
36 | protected privateActions = this.getPrivateActions({putCurUser: this.putCurUser});
37 |
38 | //提取当前路由中的本模块感兴趣的信息
39 | protected getRouteParams(): RouteParams {
40 | const {pathname, searchQuery} = this.getRouter().location;
41 | const [, subModuleStr = '', curViewStr = ''] = pathToRegexp('/:subModule/:curView', undefined, {end: false}).exec(pathname) || [];
42 | const subModule: SubModule | undefined = SubModule[subModuleStr] || undefined;
43 | const curView: CurView | undefined = CurView[curViewStr] || undefined;
44 | const fromUrl: string | undefined = searchQuery.from;
45 | return {pathname, subModule, curView, fromUrl};
46 | }
47 |
48 | //每次路由发生变化,都会引起Model重新挂载到Store
49 | //在此钩子中必需完成ModuleState的初始赋值,可以异步
50 | //在此钩子执行完成之前,本模块的View将不会Render
51 | //在此钩子中可以await数据请求,这样等所有数据拉取回来后,一次性Render
52 | //在此钩子中也可以await子模块的mount,这样等所有子模块都挂载好了,一次性Render
53 | //也可以不做任何await,直接Render,此时需要设计Loading界面
54 | //这样也形成了2种不同的路由风格:
55 | //一种是数据前置,路由后置(所有数据全部都准备好了再跳转、展示界面)
56 | //一种是路由前置,数据后置(路由先跳转,展示设计好的loading界面)
57 | //SSR时只能使用"数据前置"风格
58 | public async onMount(env: 'init' | 'route' | 'update'): Promise {
59 | this.routeParams = this.getRouteParams();
60 | const {subModule, curView, fromUrl} = this.routeParams;
61 | //getPrevState()可以获取路由跳转前的状态
62 | //以下意思是:如果curUser已经存在(之前获取过了),就直接使用,不再调用API获取
63 | //你也可以利用这个方法,复用路由之前的任何有效状态,从而减少数据请求
64 | const {curUser: _curUser} = this.getPrevState() || {};
65 | try {
66 | //如果用户信息不存在(第一次),等待获取当前用户信息
67 | const curUser = _curUser || (await api.getCurUser());
68 | const initState: ModuleState = {curUser, subModule, curView, fromUrl};
69 | //_initState是基类BaseModel中内置的一个reducer
70 | //this.dispatch是this.store.dispatch的快捷方式
71 | //以下语句等于this.store.dispatch({type: 'stage._initState', payload: initState})
72 | this.dispatch(this.privateActions._initState(initState));
73 | } catch (err: any) {
74 | //如果根模块初始化中出现错误,将错误放入ModuleState.error字段中,此时将展示该错误信息
75 | const initState: ModuleState = {curUser: {...guest}, subModule, curView, fromUrl, error: err.message || err.toString()};
76 | this.dispatch(this.privateActions._initState(initState));
77 | }
78 | }
79 |
80 | //定义一个reducer,用来更新当前用户状态
81 | //注意该render不希望对外输出,所以定义为protected
82 | @reducer
83 | protected putCurUser(curUser: CurUser): void {
84 | this.state.curUser = curUser;
85 | }
86 |
87 | //定义一个effect,用来执行登录逻辑
88 | //effect(参数),参数可以用来将该effect的执行进度注入ModuleState中,如effect('this.loginLoading')
89 | //effect()参数为空,默认等于effect('stage.globalLoading'),表示将执行进度注入stage模块的globalLoading状态中
90 | //如果不需要跟踪该effect的执行进度,请使用effect(null)
91 | @effect()
92 | public async login(args: LoginParams): Promise {
93 | const curUser = await api.login(args);
94 | this.dispatch(this.privateActions.putCurUser(curUser));
95 | //用户登录后清空所有路由栈,并跳回原地
96 | this.getRouter().relaunch({url: this.state.fromUrl || AdminHomeUrl}, 'window');
97 | }
98 |
99 | @effect()
100 | public async cancelLogin(): Promise {
101 | //在历史栈中找到第一条不需要登录的记录
102 | //如果简单的back(1),前一个页面需要登录时会引起循环
103 | this.getRouter().back((record) => {
104 | return !this.checkNeedsLogin(record.location.pathname);
105 | }, 'window');
106 | }
107 |
108 | @effect()
109 | public async logout(): Promise {
110 | const curUser = await api.logout();
111 | this.dispatch(this.privateActions.putCurUser(curUser));
112 | //用户登出后清空所有路由栈,并跳首页
113 | this.getRouter().relaunch({url: GuestHomeUrl}, 'window');
114 | }
115 |
116 | @effect()
117 | public async registry(args: RegisterParams): Promise {
118 | const curUser = await api.registry(args);
119 | this.dispatch(this.privateActions.putCurUser(curUser));
120 | //用户登录后清空所有路由栈,并跳回原地
121 | this.getRouter().relaunch({url: this.state.fromUrl || AdminHomeUrl}, 'window');
122 | }
123 |
124 | @effect()
125 | public async resetPassword(args: ResetPasswordParams): Promise {
126 | await api.resetPassword(args);
127 | message.success('您的密码已修改,请重新登录!');
128 | this.getRouter().relaunch({url: LoginUrl(this.state.fromUrl)}, 'window');
129 | }
130 |
131 | @effect()
132 | public async sendCaptcha(args: SendCaptchaParams): Promise {
133 | await api.sendCaptcha(args);
134 | message.success('短信验证码已发送,请查收!');
135 | }
136 |
137 | //ActionHandler运行中的出现的任何错误都会自动派发'stage._error'的Action
138 | //可以通过effect来监听这个Action,用来处理错误,
139 | //如果继续抛出错误,则Action停止继续传播,Handler链条将终止执行
140 | //注意如果继续抛出,请抛出原错误,不要创建新的错误,以防止无穷递归
141 | @effect(null)
142 | protected async ['this._error'](error: CustomError): Promise {
143 | if (error.code === CommonErrorCode.unauthorized) {
144 | this.getRouter().push({url: LoginUrl(error.detail)}, 'window');
145 | } else if (error.code === ErrorCodes.ROUTE_BACK_OVERFLOW) {
146 | //用户后退溢出时会触发这个错误
147 | const redirect: string = error.detail.redirect || (this.state.curUser.hasLogin ? AdminHomeUrl : GuestHomeUrl);
148 | if (this.getRouter().location.url === redirect && window.confirm('确定要退出本站吗?')) {
149 | //注意: back('')可以退出本站
150 | setTimeout(() => this.getRouter().back(''), 0);
151 | } else {
152 | setTimeout(() => this.getRouter().relaunch({url: redirect}, 'window'), 0);
153 | }
154 | } else if (!error.quiet) {
155 | message.error(error.message);
156 | }
157 | throw error;
158 | }
159 |
160 | private checkNeedsLogin(pathname: string): boolean {
161 | return ['/admin/'].some((prefix) => pathname.startsWith(prefix));
162 | }
163 |
164 | //支持路由守卫
165 | //路由跳转前会自动派发'stage._testRouteChange'的Action
166 | //可以通过effect来监听这个Action,并决定是否阻止,如果想阻止跳转,可以抛出一个错误
167 | //注意:小程序中如果使用原生路由跳转是无法拦截的
168 | @effect(null)
169 | protected async ['this._testRouteChange']({url, pathname}: {url: string; pathname: string}): Promise {
170 | if (!this.state.curUser.hasLogin && this.checkNeedsLogin(pathname)) {
171 | throw new CustomError(CommonErrorCode.unauthorized, '请登录!', url, true);
172 | }
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/src/modules/stage/utils/resource.ts:
--------------------------------------------------------------------------------
1 | import {Action, BaseModel, Dispatch, RouteTarget, StoreState, effect, reducer} from '@elux/vue-web';
2 | import {pathToRegexp} from 'path-to-regexp';
3 | import {shallowRef, watch} from 'vue';
4 | import {useRouter} from '@/Global';
5 | import {BaseApi, BaseListSearch, DefineResource} from './base';
6 | import {DialogPageClassname} from './const';
7 | import {excludeDefaultParams, mergeDefaultParams, message} from './tools';
8 |
9 | export abstract class BaseResource extends BaseModel<
10 | TDefineResource['ModuleState'],
11 | TStoreState
12 | > {
13 | protected abstract api: BaseApi;
14 | protected abstract defaultListSearch: TDefineResource['ListSearch'];
15 | protected routeParams!: TDefineResource['RouteParams']; //保存从当前路由中提取的信息结果
16 | //因为要尽量避免使用public方法,所以构建this.privateActions来引用私有actions
17 | protected privateActions = this.getPrivateActions({putList: this.putList, putCurrentItem: this.putCurrentItem});
18 |
19 | protected parseListQuery(query: Record): Record {
20 | const data = {...query} as Record;
21 | if (query.pageCurrent) {
22 | data.pageCurrent = parseInt(query.pageCurrent) || undefined;
23 | }
24 | return data;
25 | }
26 |
27 | //提取当前路由中的本模块感兴趣的信息
28 | protected getRouteParams(): TDefineResource['RouteParams'] {
29 | const {pathname, searchQuery} = this.getRouter().location;
30 | const [, admin = '', subModule = '', curViewStr = '', curRenderStr = '', id = ''] =
31 | pathToRegexp('/:admin/:subModule/:curView/:curRender/:id?').exec(pathname) || [];
32 | const curView = curViewStr as TDefineResource['CurView'];
33 | const curRender = curRenderStr as TDefineResource['CurRender'];
34 | const prefixPathname = ['', admin, subModule].join('/');
35 | const routeParams: TDefineResource['RouteParams'] = {prefixPathname, curView};
36 | if (curView === 'list') {
37 | routeParams.curRender = curRender || 'maintain';
38 | const listQuery = this.parseListQuery(searchQuery);
39 | routeParams.listSearch = mergeDefaultParams(this.defaultListSearch, listQuery);
40 | } else if (curView === 'item') {
41 | routeParams.curRender = curRender || 'detail';
42 | routeParams.itemId = id;
43 | }
44 | return routeParams;
45 | }
46 |
47 | //每次路由发生变化,都会引起Model重新挂载到Store
48 | //在此钩子中必需完成ModuleState的初始赋值,可以异步
49 | //在此钩子执行完成之前,本模块的View将不会Render
50 | //在此钩子中可以await数据请求,等所有数据拉取回来后,一次性Render
51 | //在此钩子中也可以await子模块的mount,等所有子模块都挂载好了,一次性Render
52 | //也可以不做任何await,直接Render,此时需要设计Loading界面
53 | public onMount(env: 'init' | 'route' | 'update'): void {
54 | this.routeParams = this.getRouteParams();
55 | const {prefixPathname, curView, curRender, listSearch, itemId} = this.routeParams;
56 | this.dispatch(
57 | this.privateActions._initState({
58 | prefixPathname,
59 | curView,
60 | curRender,
61 | listSearch,
62 | itemId,
63 | } as TDefineResource['ModuleState'])
64 | );
65 | if (curView === 'list') {
66 | this.dispatch(this.actions.fetchList(listSearch));
67 | } else if (curView === 'item') {
68 | this.dispatch(this.actions.fetchItem(itemId || ''));
69 | }
70 | }
71 |
72 | @reducer
73 | public putList(listSearch: TDefineResource['ListSearch'], list: TDefineResource['ListItem'][], listSummary: TDefineResource['ListSummary']): void {
74 | Object.assign(this.state, {listSearch, list, listSummary});
75 | }
76 |
77 | @effect('this.listLoading')
78 | public async fetchList(listSearchData?: TDefineResource['ListSearch']): Promise {
79 | const listSearch = listSearchData || this.state.listSearch || this.defaultListSearch;
80 | const {list, listSummary} = await this.api.getList!(listSearch);
81 | this.dispatch(this.privateActions.putList(listSearch, list, listSummary));
82 | }
83 |
84 | @reducer
85 | public putCurrentItem(itemId = '', itemDetail: TDefineResource['ItemDetail']): void {
86 | Object.assign(this.state, {itemId, itemDetail});
87 | }
88 |
89 | @effect()
90 | public async fetchItem(itemId: string): Promise {
91 | const item = await this.api.getItem!({id: itemId});
92 | this.dispatch(this.actions.putCurrentItem(itemId, item));
93 | }
94 |
95 | @effect()
96 | public async alterItems(id: string | string[], data: Partial): Promise {
97 | await this.api.alterItems!({id, data});
98 | message.success('修改成功!');
99 | this.state.curView === 'list' && this.dispatch(this.actions.fetchList());
100 | }
101 |
102 | @effect()
103 | public async deleteItems(id: string | string[]): Promise {
104 | await this.api.deleteItems!({id});
105 | message.success('删除成功!');
106 | this.state.curView === 'list' && this.dispatch(this.actions.fetchList());
107 | }
108 |
109 | @effect()
110 | public async updateItem(id: string, data: TDefineResource['UpdateItem']): Promise {
111 | await this.api.updateItem!({id, data});
112 | await this.getRouter().back(1, 'window');
113 | message.success('编辑成功!');
114 | this.getRouter().back(0);
115 | }
116 |
117 | @effect()
118 | public async createItem(data: TDefineResource['CreateItem']): Promise {
119 | await this.api.createItem!(data);
120 | await this.getRouter().back(1, 'window');
121 | message.success('创建成功!');
122 | this.getRouter().back(0);
123 | }
124 | }
125 |
126 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
127 | export function useSearch(props: {listPathname: string}, defaultListSearch: Partial) {
128 | const router = useRouter();
129 | const onSearch = (values: Partial) => {
130 | const searchQuery = excludeDefaultParams(defaultListSearch, {...values, pageCurrent: 1});
131 | router.push({pathname: props.listPathname, searchQuery, state: router.location.state}, 'page');
132 | };
133 |
134 | return {onSearch};
135 | }
136 |
137 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
138 | export function useShowDetail(props: {prefixPathname: string}) {
139 | const router = useRouter();
140 | const onShowDetail = (id: string) => {
141 | router.push({url: `${props.prefixPathname}/item/detail/${id}`, classname: DialogPageClassname}, 'window');
142 | };
143 | const onShowEditor = (id: string, onSubmit: (id: string, data: Record) => Promise) => {
144 | router.push({url: `${props.prefixPathname}/item/edit/${id}`, classname: DialogPageClassname, state: {onSubmit}}, 'window');
145 | };
146 |
147 | return {onShowDetail, onShowEditor};
148 | }
149 |
150 | //eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
151 | export function useAlter(
152 | props: {
153 | dispatch: Dispatch;
154 | selectedRows?: T[];
155 | },
156 | actions: {
157 | deleteItems?(id: string | string[]): Action;
158 | alterItems?(id: string | string[], data: Record): Action;
159 | updateItem?(id: string, data: Record): Action;
160 | createItem?(data: Record): Action;
161 | }
162 | ) {
163 | const selectedRows = shallowRef(props.selectedRows);
164 | watch(
165 | () => props.selectedRows,
166 | (val) => (selectedRows.value = val)
167 | );
168 |
169 | const deleteItems = async (id: string | string[]) => {
170 | await props.dispatch(actions.deleteItems!(id));
171 | selectedRows.value = [];
172 | };
173 |
174 | const alterItems = async (id: string | string[], data: Record) => {
175 | await props.dispatch(actions.alterItems!(id, data));
176 | selectedRows.value = [];
177 | };
178 |
179 | const updateItem = async (id: string, data: Record) => {
180 | if (id) {
181 | await props.dispatch(actions.updateItem!(id, data));
182 | } else {
183 | await props.dispatch(actions.createItem!(data));
184 | }
185 | selectedRows.value = [];
186 | };
187 |
188 | return {selectedRows, deleteItems, alterItems, updateItem};
189 | }
190 |
191 | export function useSingleWindow(): RouteTarget {
192 | const router = useRouter();
193 | return router.location.classname.startsWith('_') ? 'page' : 'window';
194 | }
195 | export function useTableSize(): 'middle' | 'large' {
196 | const router = useRouter();
197 | return router.location.classname.startsWith('_') ? 'middle' : 'large';
198 | }
199 | //eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
200 | export function useTableChange(props: {listSearch?: T}, listPathname: string, defaultListSearch: T) {
201 | const router = useRouter();
202 | return (pagination: any, filter: any, _sorter: any) => {
203 | const {listSearch} = props;
204 | const sorterStr = [listSearch?.sorterField, listSearch?.sorterOrder].join('');
205 | const sorter = _sorter as {field: string; order: 'ascend' | 'descend' | undefined};
206 | const {current, pageSize} = pagination as {current: number; pageSize: number};
207 | const sorterField = (sorter.order && sorter.field) || undefined;
208 | const sorterOrder = sorter.order || undefined;
209 | const currentSorter = [sorterField, sorterOrder].join('');
210 | const pageCurrent = currentSorter !== sorterStr ? 1 : current;
211 |
212 | const searchQuery = excludeDefaultParams(defaultListSearch, {...listSearch, pageCurrent, pageSize, sorterField, sorterOrder});
213 |
214 | router.push({pathname: listPathname, searchQuery, state: router.location.state}, 'page');
215 | };
216 | }
217 |
218 | //也可以使用回调到方法创建和编辑,但使用await 路由跳转更简单
219 | // export function useUpdateItem(
220 | // id: string,
221 | // dispatch: Dispatch,
222 | // actions: {updateItem?(id: string, data: Record): Action; createItem?(data: Record): Action}
223 | // ) {
224 | // const loading = shallowRef(false);
225 | // const router = useRouter();
226 | // const onFinish = (values: Record) => {
227 | // const {onSubmit} = (router.location.state || {}) as {onSubmit?: (id: string, data: Record) => Promise};
228 | // let result: Promise;
229 | // loading.value = true;
230 | // if (onSubmit) {
231 | // result = onSubmit(id, values);
232 | // } else {
233 | // if (id) {
234 | // result = dispatch(actions.updateItem!(id, values)) as Promise;
235 | // } else {
236 | // result = dispatch(actions.createItem!(values)) as Promise;
237 | // }
238 | // }
239 | // result.finally(() => (loading.value = false)).then(() => router.back(1, 'window'));
240 | // };
241 |
242 | // return {loading, onFinish};
243 | // }
244 |
--------------------------------------------------------------------------------