warrior 8 mesiacov pred
rodič
commit
990a13c664
23 zmenil súbory, kde vykonal 1027 pridanie a 284 odobranie
  1. 1 1
      projects/textbook/src/app/comp-manage/comp-manage.component.html
  2. 6 6
      projects/textbook/src/app/comp-manage/comp-manage.component.ts
  3. 2 2
      projects/textbook/src/app/comp-nav/comp-nav.component.html
  4. 4 2
      projects/textbook/src/app/comp-table/comp-table-list/comp-table-list.component.ts
  5. 2 2
      projects/textbook/src/app/comp-table/parse-data.service.ts
  6. 117 52
      projects/textbook/src/modules/login/account-info/account-info.component.html
  7. 17 13
      projects/textbook/src/modules/login/account-info/account-info.component.scss
  8. 194 19
      projects/textbook/src/modules/login/account-info/account-info.component.ts
  9. 12 11
      projects/textbook/src/modules/login/login/login.component.ts
  10. 14 3
      projects/textbook/src/modules/nav-author/apply/apply.component.html
  11. 29 12
      projects/textbook/src/modules/nav-author/apply/apply.component.ts
  12. 1 1
      projects/textbook/src/modules/nav-author/components/attachment/attachment.component.ts
  13. 37 30
      projects/textbook/src/modules/nav-author/components/basic-in/basic-in.component.html
  14. 158 43
      projects/textbook/src/modules/nav-author/components/basic-in/basic-in.component.ts
  15. 2 3
      projects/textbook/src/modules/nav-author/components/page-textbook/page-textbook.component.ts
  16. 2 1
      projects/textbook/src/modules/nav-author/components/textbook-content/textbook-content.component.ts
  17. 82 17
      projects/textbook/src/modules/nav-author/components/textbook-pertain/textbook-pertain.component.ts
  18. 10 5
      projects/textbook/src/schemas/EduTextbook.ts
  19. 100 55
      projects/textbook/src/services/auth.service.ts
  20. 153 4
      server/api/textbook/routes.js
  21. 1 0
      server/db/index.js
  22. 10 2
      server/db/schemas/EduTextbook.js
  23. 73 0
      server/db/schemas/Profile.js

+ 1 - 1
projects/textbook/src/app/comp-manage/comp-manage.component.html

@@ -15,7 +15,7 @@
       nzMode="inline"
       style="width: 100%; background: #f9eaea"
     >
-      @for (item of optionsMap[textbook.profile?.type]; track item.id;let index = $index) { @if(item.child)
+      @for (item of optionsMap[textbook.profile?.identity]; track item.id;let index = $index) { @if(item.child)
       {
       <li nz-submenu [nzTitle]="item.name" nzIcon="api" [nzOpen]="true">
         <ul>

+ 6 - 6
projects/textbook/src/app/comp-manage/comp-manage.component.ts

@@ -24,7 +24,7 @@ export class CompManageComponent implements OnInit {
   </svg>
   `
   optionsMap: any = {
-    "navAdmin":[
+    "国家级管理员":[
       {
         name:'教材管理',
         id:'1',
@@ -79,13 +79,13 @@ export class CompManageComponent implements OnInit {
         id:'4',
       },
     ],
-    "navProvinceSubmit":[
+    "省级教育行政部门":[
       {
         name:'省内合集',
         id:'1',
       },
     ],
-    "navProvinceContact":[
+    "流程管理员登录":[
       {
         name:'报送流程',
         id:'1',
@@ -109,7 +109,7 @@ export class CompManageComponent implements OnInit {
         ]
       }
     ],
-    "navProvinceSchoolContact":[
+    "省属高校联系人":[
       {
         name:'校内空间',
         id:'1',
@@ -133,7 +133,7 @@ export class CompManageComponent implements OnInit {
         id:'3',
       },
     ],
-    "navReview":[
+    "教材评审组成员":[
       {
         name:'报送流程',
         id:'1',
@@ -145,7 +145,7 @@ export class CompManageComponent implements OnInit {
         path: '/nav-review/profile',
       },
     ],
-    "navAuthor":[
+    "作者/教师/主编":[
       {
         name: '个人空间',
         id: '1',

+ 2 - 2
projects/textbook/src/app/comp-nav/comp-nav.component.html

@@ -6,8 +6,8 @@
   ></nz-avatar>
   @if(!viewCollapsed){
   <div class="user">
-    <div class="name">{{ tbookSer.profile?.name || "彭秀龙" }}</div>
-    <div class="email">{{ tbookSer.profile?.email || "dayi@edu.com" }}</div>
+    <div class="name">{{ tbookSer.profile?.name || "未认证用户" }}</div>
+    <div class="email">{{ tbookSer.profile?.email || "test@edu.com" }}</div>
   </div>
   <div class="more">
     <button

+ 4 - 2
projects/textbook/src/app/comp-table/comp-table-list/comp-table-list.component.ts

@@ -21,6 +21,7 @@ import { NzIconModule } from 'ng-zorro-antd/icon';
 import { NzPageHeaderModule } from 'ng-zorro-antd/page-header';
 import { NzBreadCrumbModule } from 'ng-zorro-antd/breadcrumb';
 import { NzTagModule } from 'ng-zorro-antd/tag';
+import { Router } from '@angular/router';
 
 interface SchemaFiled{
   key:string,
@@ -67,6 +68,7 @@ export class CompTableListComponent {
     private parseData:ParseDataService,
     // private clusterServ:ClusterService,
     private dialog:MatDialog,
+    private router:Router,
     // public translate:TranslateService
   ){
     this.currentLang = "cn"// this.translate.getDefaultLang();
@@ -97,7 +99,7 @@ export class CompTableListComponent {
   buttonHandle(button:any,object:any){
     let that = this
     if(button?.handle){
-      button?.handle({object:object,dialog:this.dialog,callback:this.onButtonHandleCallBack},that.currentLang)
+      button?.handle({object:object,dialog:this.dialog,router:this.router,callback:this.onButtonHandleCallBack},that.currentLang)
     }
   }
   refresh(page?:number){
@@ -153,7 +155,7 @@ export class CompTableListComponent {
     await this.parseData.cachePointerList(Object.values(this.pointerMap))
     Object.keys(this.pointerMap).forEach(async id=>{
       let obj = this.parseData.ParseObjectCacheMap[id]
-      let field = this.fieldsArray?.find(item=>item?.className==obj?.className)
+      let field = this.fieldsArray?.find(item=>item?.targetClass==obj?.className)
       this.pointerShowMap[id] = await this.parseData.showNameByObj(obj,field?.showName)
     })
   }

+ 2 - 2
projects/textbook/src/app/comp-table/parse-data.service.ts

@@ -40,11 +40,11 @@ export class ParseDataService {
     }
 
     // 获取模板标记数组
-    console.log(matches);
+    // console.log(matches);
     matches.forEach(keyword=>{
       text = text.replaceAll("${"+keyword+"}",obj?.get(keyword)||"")
     })
-    console.log(text)
+    // console.log(text)
 
     // 特殊数据名称设定
     // if(obj.className=="MinerCluster"){

+ 117 - 52
projects/textbook/src/modules/login/account-info/account-info.component.html

@@ -45,9 +45,10 @@
         >
         <nz-input-group>
           <input
+            nz-input
             type="text"
             formControlName="name"
-            placeholder="请输入用户名"
+            placeholder="请填写姓名"
           />
         </nz-input-group>
       </nz-form-item>
@@ -57,9 +58,12 @@
         >
         <nz-input-group>
           <input
+            nz-input
             type="text"
-            formControlName="mobile"
-            placeholder="请输入用户名"
+            [disabled]="true"
+            [ngModel]="mobile"
+            [ngModelOptions]="{standalone: true}"
+            placeholder="请填写手机号"
           />
         </nz-input-group> </nz-form-item
       ><nz-form-item>
@@ -68,119 +72,180 @@
         >
         <nz-input-group>
           <input
-            type="text"
+            nz-input
+            type="email"
             formControlName="email"
-            placeholder="请输入用户名"
+            placeholder="请填写电子邮箱"
           />
         </nz-input-group> </nz-form-item
       ><nz-form-item>
-        <nz-form-label class="label" [nzNoColon]="true" nzRequired
-          >公共电话</nz-form-label
-        >
+        <nz-form-label class="label" [nzNoColon]="true">办公电话</nz-form-label>
         <nz-input-group>
           <input
+            nz-input
             type="text"
-            formControlName="workPhone"
-            placeholder="请输入用户名"
+            [(ngModel)]="nonRequired.workPhone"
+            [ngModelOptions]="{standalone: true}"
+            placeholder="请填写办公电话"
           />
-        </nz-input-group> </nz-form-item
-      >
+        </nz-input-group>
+      </nz-form-item>
       <nz-form-item>
         <nz-form-label class="label" [nzNoColon]="true" nzRequired
           >省份</nz-form-label
         >
         <nz-input-group>
-          <input
+          <!-- <input
+            nz-input
             type="text"
             formControlName="province"
-            placeholder="请输入用户名"
-          />
-        </nz-input-group> </nz-form-item
-      >
+            placeholder="请填写用户名"
+          /> -->
+          <nz-select
+            style="width: 100%"
+            nzShowSearch
+            nzAllowClear
+            nzPlaceHolder="请选择省份"
+            formControlName="province"
+          >
+            @for(item of provinces; track item;let index = $index){
+            <nz-option nzCustomContent [nzValue]="item" [nzLabel]="item">{{
+              item
+            }}</nz-option>
+            }
+          </nz-select>
+        </nz-input-group>
+      </nz-form-item>
       <nz-form-item>
         <nz-form-label class="label" [nzNoColon]="true" nzRequired
           >单位类型</nz-form-label
         >
         <nz-input-group>
-          <input
+          <!-- <input
+            nz-input
             type="text"
             formControlName="unitType"
-            placeholder="请输入用户名"
-          />
-        </nz-input-group> </nz-form-item
-      >
+            placeholder="请填写用户名"
+          /> -->
+          <nz-select
+            style="width: 100%"
+            nzShowSearch
+            nzAllowClear
+            nzPlaceHolder="请选择单位类型"
+            formControlName="unitType"
+          >
+            @for(item of unitTypes; track item;let index = $index){
+            <nz-option nzCustomContent [nzValue]="item" [nzLabel]="item">{{
+              item
+            }}</nz-option>
+            }
+          </nz-select>
+        </nz-input-group>
+      </nz-form-item>
       <nz-form-item>
         <nz-form-label class="label" [nzNoColon]="true" nzRequired
           >单位名称</nz-form-label
         >
         <nz-input-group>
           <input
+            nz-input
             type="text"
             formControlName="unit"
-            placeholder="请输入用户名"
+            placeholder="请填写单位名称"
           />
-        </nz-input-group> </nz-form-item
-      >
+        </nz-input-group>
+      </nz-form-item>
       <nz-form-item>
-        <nz-form-label class="label" [nzNoColon]="true" nzRequired
-          >所在部门</nz-form-label
-        >
+        <nz-form-label class="label" [nzNoColon]="true">所在部门</nz-form-label>
         <nz-input-group>
           <input
+            nz-input
             type="text"
-            formControlName="department"
-            placeholder="请输入用户名"
+            [(ngModel)]="nonRequired.branch"
+            [ngModelOptions]="{standalone: true}"
+            placeholder="请填写所在部门"
           />
-        </nz-input-group> </nz-form-item
-      >
+        </nz-input-group>
+      </nz-form-item>
       <nz-form-item>
-        <nz-form-label class="label" [nzNoColon]="true" nzRequired
-          >职务</nz-form-label
-        >
+        <nz-form-label class="label" [nzNoColon]="true">职务</nz-form-label>
         <nz-input-group>
           <input
+            nz-input
             type="text"
-            formControlName="job"
-            placeholder="请输入用户名"
+            [(ngModel)]="nonRequired.job"
+            [ngModelOptions]="{standalone: true}"
+            placeholder="请填写职务"
           />
-        </nz-input-group> </nz-form-item
-      >
+        </nz-input-group>
+      </nz-form-item>
       <nz-form-item>
         <nz-form-label class="label" [nzNoColon]="true" nzRequired
           >身份证号</nz-form-label
         >
         <nz-input-group>
           <input
+            nz-input
             type="text"
             formControlName="idcard"
-            placeholder="请输入用户名"
+            placeholder="请填写身份证号"
           />
-        </nz-input-group> </nz-form-item
-      >
+        </nz-input-group>
+      </nz-form-item>
       <nz-form-item>
         <nz-form-label class="label" [nzNoColon]="true" nzRequired
           >用户类型</nz-form-label
         >
         <nz-input-group>
-          <input
+          <!-- <input
+            nz-input
             type="text"
             formControlName="identity"
-            placeholder="请输入用户名"
-          />
-        </nz-input-group> </nz-form-item
-      >
+            placeholder="请选择用户类型"
+          /> -->
+          <nz-select
+            style="width: 100%"
+            nzShowSearch
+            nzAllowClear
+            nzPlaceHolder="请选择单位类型"
+            formControlName="identity"
+          >
+            @for(item of identitys; track item;let index = $index){
+            <nz-option nzCustomContent [nzValue]="item" [nzLabel]="item">{{
+              item
+            }}</nz-option>
+            }
+          </nz-select>
+        </nz-input-group>
+      </nz-form-item>
       <nz-form-item>
         <nz-form-label class="label" [nzNoColon]="true" nzRequired
           >单位联系人认证文件</nz-form-label
         >
         <nz-input-group>
-          <input
+          <!-- <input
+            nz-input
             type="text"
             formControlName="file"
-            placeholder="请输入用户名"
-          />
-        </nz-input-group> </nz-form-item
-      >
+            placeholder="请填写用户名"
+          /> -->
+          <nz-upload
+            formControlName="file"
+            nzAction="https://www.mocky.io/v2/5cc8019d300000980a055e76"
+            [nzHeaders]="{ authorization: 'authorization-text' }"
+            (nzChange)="handleChange($event)"
+          >
+            <div style="color: #3e49b3">
+              <span nz-icon nzType="upload"></span>上传认证文件
+            </div>
+          </nz-upload>
+          <div class="text" style="margin: 10px 0; color: #231c1f99">
+            请下载单位联系人认证<a href="">文件模板</a
+            >,填写盖章后上传。支持上传 PDF、JPG、JPEG、PNG
+            格式,单个文件大小不超过 2M
+          </div>
+        </nz-input-group>
+      </nz-form-item>
     </form>
 
     <button

+ 17 - 13
projects/textbook/src/modules/login/account-info/account-info.component.scss

@@ -37,21 +37,25 @@
     margin-bottom: 10px;
   }
 }
-input {
+.ant-input-group{
   text-align: left;
-  border: none;
-  width: 100%;
-  padding: 4px 6px;
-  background: #f9f9f9;
-  border-radius: 4px;
-  font-family: PingFang SC;
-  font-size: 14px;
-  font-weight: 400;
-  line-height: 22px;
-}
-input {
-  outline: none;
+
 }
+// input {
+//   text-align: left;
+//   border: none;
+//   width: 100%;
+//   padding: 4px 6px;
+//   background: #f9f9f9;
+//   border-radius: 4px;
+//   font-family: PingFang SC;
+//   font-size: 14px;
+//   font-weight: 400;
+//   line-height: 22px;
+// }
+// input {
+//   outline: none;
+// }
 .form-button {
   width: 100%;
   height: 46px;

+ 194 - 19
projects/textbook/src/modules/login/account-info/account-info.component.ts

@@ -3,6 +3,8 @@ import { ReactiveFormsModule } from '@angular/forms';
 import { HttpClient } from '@angular/common/http';
 import { Router } from '@angular/router';
 import { catchError } from 'rxjs/operators';
+import { NzUploadChangeParam } from 'ng-zorro-antd/upload';
+import { NzUploadModule } from 'ng-zorro-antd/upload';
 import {
   FormControl,
   FormGroup,
@@ -18,38 +20,99 @@ import Parse from 'parse';
 @Component({
   selector: 'app-account-info',
   standalone: true,
-  imports: [ReactiveFormsModule, CommonCompModule, CaptchaComponent],
+  imports: [
+    ReactiveFormsModule,
+    CommonCompModule,
+    CaptchaComponent,
+    NzUploadModule,
+  ],
   templateUrl: './account-info.component.html',
   styleUrls: ['./account-info.component.scss'],
 })
 export class AccountInfoComponent implements OnInit {
+  user: Parse.Object = Parse.User.current()!;
   validateForm: FormGroup<{
-    name: FormControl<string>;//姓名
-    mobile: FormControl<string>;//手机号
-    email: FormControl<string>;//电子邮箱
-    workPhone: FormControl<string>;//公共电话
-    province: FormControl<string>;//省份
-    unitType: FormControl<string>;//单位类型
-    unit: FormControl<string>;//单位名称
-    department: FormControl<string>;//所在部门
-    job: FormControl<string>;//职务
-    idcard: FormControl<string>;//身份证号
-    identity: FormControl<string>;//用户类型
+    name: FormControl<string>; //姓名
+    // mobile: FormControl<string>; //手机号
+    email: FormControl<string>; //电子邮箱
+    province: FormControl<string>; //省份
+    unitType: FormControl<string>; //单位类型
+    unit: FormControl<string>; //单位名称
+    idcard: FormControl<string>; //身份证号
+    identity: FormControl<string>; //用户类型
     file: FormControl<string>; //单位联系人认证文件
   }> = this.fb.group({
     name: ['', [Validators.required]],
-    mobile: ['', [Validators.required]],
+    // mobile: ['', [Validators.required]],
     email: ['', [Validators.required]],
-    workPhone: ['', [Validators.required]],
     province: ['', [Validators.required]],
     unitType: ['', [Validators.required]],
     unit: ['', [Validators.required]],
-    department: ['', [Validators.required]],
-    job: ['', [Validators.required]],
     idcard: ['', [Validators.required]],
     identity: ['', [Validators.required]],
-    file: ['', [Validators.required]],
+    file: ['暂无', [Validators.required]],
   });
+  nonRequired: any = {
+    workPhone: '',
+    branch: '',
+    job: '',
+  };
+  mobile: string = '';
+
+  //省份
+  provinces: Array<string> = [
+    '北京市',
+    '天津市',
+    '河北省',
+    '山西省',
+    '内蒙古自治区',
+    '辽宁省',
+    '吉林省',
+    '黑龙江省',
+    '上海市',
+    '江苏省',
+    '浙江省',
+    '安徽省',
+    '福建省',
+    '江西省',
+    '山东省',
+    '河南省',
+    '湖北省',
+    '湖南省',
+    '广东省',
+    '广西壮族自治区',
+    '海南省',
+    '重庆市',
+    '四川省',
+    '贵州省',
+    '云南省',
+    '西藏自治区',
+    '陕西省',
+    '甘肃省',
+    '青海省',
+    '宁夏回族自治区',
+    '新疆维吾尔自治区',
+    '台湾省',
+    '香港特别行政区',
+    '澳门特别行政区',
+  ];
+  unitTypes: Array<string> = [
+    '个体工商户',
+    '企业公司',
+    '国有机构或事业单位',
+    '非营利组织或公益机构',
+    '教育机构',
+    '自由职业者或个人',
+    '其他特定单位类型',
+  ];
+  identitys: Array<string> = [
+    '国家级管理员',
+    '省级教育行政部门',
+    '合集管理员',
+    '省属高校联系人',
+    '教材评审组成员',
+    '作者/教师/主编',
+  ];
   constructor(
     public tbookSer: textbookServer,
     private fb: NonNullableFormBuilder,
@@ -59,10 +122,79 @@ export class AccountInfoComponent implements OnInit {
     private http: HttpClient
   ) {}
 
-  ngOnInit() {}
+  ngOnInit() {
+    if (Parse.User.current()?.id) {
+      // this.validateForm.value.mobile = Parse.User.current()?.get('mobile');
+      this.mobile = this.user.get('mobile');
+      this.authProfile();
+    } else {
+      this.router.navigate(['user/login']);
+    }
+  }
+  async authProfile() {
+    let query = new Parse.Query('Profile');
+    query.equalTo('user', Parse.User.current()?.id);
+    query.notEqualTo('isDeleted', true);
+    let r = await query.first();
+    if (r?.id) {
+      if (!r.get('verify')) {
+        this.message.info('您已完成身份认证,即将跳转到管理后台');
+        return;
+      }
+      this.validateForm = this.fb.group({
+        name: [r.get('name'), [Validators.required]],
+        // mobile: [Parse.User.current()?.get('mobile') || r.get('mobile'), [Validators.required]],
+        email: [r.get('email'), [Validators.required]],
+        province: [r.get('province'), [Validators.required]],
+        unitType: [r.get('unitType'), [Validators.required]],
+        unit: [r.get('unit'), [Validators.required]],
+        idcard: [r.get('idcard'), [Validators.required]],
+        identity: [r.get('identity'), [Validators.required]],
+        file: [r.get('file'), [Validators.required]],
+      });
+      this.nonRequired = {
+        workPhone: r.get('workPhone'),
+        branch: r.get('branch'),
+        job: r.get('job'),
+      };
+    }
+  }
   submitForm(): void {
+    console.log(this.validateForm.value);
     if (this.validateForm.valid) {
-      let { name } = this.validateForm.value;
+      let {
+        name,
+        // mobile,
+        email,
+        province,
+        unitType,
+        unit,
+        idcard,
+        identity,
+        file,
+      } = this.validateForm.value;
+  
+      let a =
+        /^\d{6}((((((19|20)\d{2})(0[13-9]|1[012])(0[1-9]|[12]\d|30))|(((19|20)\d{2})(0[13578]|1[02])31)|((19|20)\d{2})02(0[1-9]|1\d|2[0-8])|((((19|20)([13579][26]|[2468][048]|0[48]))|(2000))0229))\d{3})|((((\d{2})(0[13-9]|1[012])(0[1-9]|[12]\d|30))|((\d{2})(0[13578]|1[02])31)|((\d{2})02(0[1-9]|1\d|2[0-8]))|(([13579][26]|[2468][048]|0[048])0229))\d{2}))(\d|X|x)$/;
+      if (!String(idcard).match(a)) {
+        this.message.error('身份证格式错误');
+        return;
+      }
+      let params = {
+        name,
+        mobile: this.mobile,
+        email,
+        province,
+        unitType,
+        unit,
+        idcard,
+        identity,
+        file,
+        workPhone: this.nonRequired.workPhone,
+        branch: this.nonRequired.branch,
+        job: this.nonRequired.job,
+      };
+      this.profileSave(params);
     } else {
       this.message.warning('请填写完整的信息');
       Object.values(this.validateForm.controls).forEach((control) => {
@@ -73,4 +205,47 @@ export class AccountInfoComponent implements OnInit {
       });
     }
   }
+
+  async profileSave(params: any) {
+    console.log(params);
+    let query = new Parse.Query('Profile');
+    query.equalTo('user', Parse.User.current()?.id);
+    query.notEqualTo('isDeleted', true);
+    let profile = await query.first();
+    if (!profile?.id) {
+      let obj = Parse.Object.extend('Profile');
+      profile = new obj();
+    }
+    profile?.set('user', Parse.User.current()?.toPointer());
+    profile?.set('company', {
+      __type: 'Pointer',
+      className: 'Company',
+      objectId: this.tbookSer.company,
+    });
+    profile?.set('name', params.name);
+    profile?.set('email', params.email);
+    profile?.set('mobile', params.email);
+    profile?.set('province', params.province);
+    profile?.set('unitType', params.unitType);
+    profile?.set('unit', params.unit);
+    profile?.set('idcard', params.idcard);
+    profile?.set('identity', params.identity);
+    profile?.set('file', params.file);
+    profile?.set('workPhone', params.workPhone);
+    profile?.set('branch', params.branch);
+    profile?.set('job', params.job);
+    profile?.set('verify', true);
+    let res = await profile?.save();
+    this.authServr.profileVerify()
+  }
+  handleChange(info: NzUploadChangeParam): void {
+    if (info.file.status !== 'uploading') {
+      console.log(info.file, info.fileList);
+    }
+    if (info.file.status === 'done') {
+      this.message.success(`${info.file.name} file uploaded successfully`);
+    } else if (info.file.status === 'error') {
+      this.message.error(`${info.file.name} file upload failed.`);
+    }
+  }
 }

+ 12 - 11
projects/textbook/src/modules/login/login/login.component.ts

@@ -93,10 +93,12 @@ export class LoginComponent {
           this.message.warning('请勾选隐私协议与服务条款')
           return
         }
-        this.authServr.login(userName, password, this.tbookSer.company).catch(data=>{
-          console.log(data);
+        this.authServr.login(userName, password, this.tbookSer.company).then(()=>{
+          this.codelogin.updateDrawCode();
+        }) .catch(err=>{
+          console.warn(err);
+          this.message.error(err?.message || '登录失败')
         })
-        this.codelogin.updateDrawCode();
       } else {
         this.message.warning('填写信息不正确')
         Object.values(this.validateForm.controls).forEach((control) => {
@@ -109,19 +111,18 @@ export class LoginComponent {
     }else{//手机号登录/注册
       console.log(this.validateFormPhone.value);
       if (this.validateFormPhone.valid) {
-        this.codeloginSign.updateDrawCode();
         let {phoneNumber, code } = this.validateFormPhone.value
-        this.router.navigate(['/user/account_info']);
-
+        console.log(phoneNumber, code);
         if(this.code.toLowerCase() != code?.toLowerCase()){
           this.message.warning('验证码错误')
           return
         }
-        console.log(phoneNumber, code);
-        
-        // this.authServr.login(userName, password, this.tbookSer.company).then(() => {
-          // this.router.navigate(['/user/account_info']);
-        // });
+        this.authServr.register(phoneNumber, code, this.tbookSer.company).then(()=>{
+          this.codeloginSign.updateDrawCode();
+        }).catch((err) => {
+          console.warn(err);
+          this.message.error(err?.message || '登录失败')
+        });
       } else {
         this.message.warning('填写信息不正确')
         Object.values(this.validateFormPhone.controls).forEach((control) => {

+ 14 - 3
projects/textbook/src/modules/nav-author/apply/apply.component.html

@@ -1,7 +1,9 @@
 <div class="site-page-header">
-  <nz-page-header nzTitle="创建/编辑空间" nzSubtitle="" style="padding: 0;">
+  <nz-page-header nzTitle="创建/编辑空间" nzSubtitle="" style="padding: 0">
     <nz-breadcrumb nz-page-header-breadcrumb>
-      <div class="back" (click)="back()"><span nz-icon nzType="left" nzTheme="outline"></span>返回</div>
+      <div class="back" (click)="back()">
+        <span nz-icon nzType="left" nzTheme="outline"></span>返回
+      </div>
     </nz-breadcrumb>
   </nz-page-header>
   <div class="steps">
@@ -16,27 +18,36 @@
 
 <div class="content">
   <div class="state-title">{{ stateMap }}</div>
+  @if (showFrom) {
   <div class="submit-block" #submitComp>
     @switch (state) { @case (0) {
-    <app-basic (state)="changeState($event)" (save)="save()"></app-basic>
+    <app-basic
+      [eduTextbook]="textBook"
+      (state)="changeState($event)"
+      (save)="save()"
+    ></app-basic>
     } @case (1) {
     <app-textbook-pertain
+      [eduTextbook]="textBook"
       (state)="changeState($event)"
       (save)="save()"
       (maxWidth)="(submitComp.style.width)"
     ></app-textbook-pertain>
     } @case (2) {
     <app-textbook-content
+      [eduTextbook]="textBook"
       (state)="changeState($event)"
       (save)="save()"
       (maxWidth)="(submitComp.style.width)"
     ></app-textbook-content>
     } @case (3) {
     <app-attachment
+      [eduTextbook]="textBook"
       (state)="changeState($event)"
       (save)="save()"
       (maxWidth)="(submitComp.style.width)"
     ></app-attachment>
     } }
   </div>
+  }
 </div>

+ 29 - 12
projects/textbook/src/modules/nav-author/apply/apply.component.ts

@@ -1,12 +1,14 @@
 import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
 import { CommonCompModule } from '../../../services/common.modules';
-import { Router } from '@angular/router';
+import { Router, ActivatedRoute } from '@angular/router';
 import { ReactiveFormsModule } from '@angular/forms';
 import { BasicInComponent } from '../components/basic-in/basic-in.component';
 import { TextbookPertainComponent } from '../components/textbook-pertain/textbook-pertain.component';
-import { TextbookContentComponent }  from '../components/textbook-content/textbook-content.component'
-import { AttachmentComponent } from '../components/attachment/attachment.component'
+import { TextbookContentComponent } from '../components/textbook-content/textbook-content.component';
+import { AttachmentComponent } from '../components/attachment/attachment.component';
 // import { NzLayoutModule } from 'ng-zorro-antd/layout';
+import Parse from 'parse';
+
 @Component({
   selector: 'app-apply',
   imports: [
@@ -15,13 +17,14 @@ import { AttachmentComponent } from '../components/attachment/attachment.compone
     BasicInComponent,
     TextbookPertainComponent,
     TextbookContentComponent,
-    AttachmentComponent
+    AttachmentComponent,
   ],
   standalone: true,
   templateUrl: './apply.component.html',
   styleUrls: ['./apply.component.scss'],
 })
 export class ApplyComponent implements OnInit {
+  textBook: Parse.Object | any;
   state: number = 0;
   get stateMap() {
     let map: any = {
@@ -32,22 +35,36 @@ export class ApplyComponent implements OnInit {
     };
     return map[this.state];
   }
-  editForm: any;
+  showFrom: boolean = false;
 
-  constructor(private router: Router) {}
+  constructor(private router: Router, private activeRoute: ActivatedRoute) {}
 
-  ngOnInit() {}
-  changeState(event:string|any){
+  async ngOnInit() {
+    this.activeRoute.paramMap.subscribe(async (params) => {
+      let id = params.get('id');
+      if (id) {
+        let query = new Parse.Query('EduTextbook');
+        query.equalTo('objectId', id);
+        this.textBook = await query.first();
+        console.log(this.textBook);
+      }
+      this.showFrom = true;
+    });
+  }
+  changeState(event: string | any) {
     console.log(event);
-    if(event == 'pre'){
+    if(event?.textBook){
+      this.textBook = event?.textBook
+    }
+    if (event?.type == 'pre') {
       this.state--;
-    }else{
+    } else {
       this.state++;
     }
   }
   save() {}
 
-  back(){
-    history.back()
+  back() {
+    history.back();
   }
 }

+ 1 - 1
projects/textbook/src/modules/nav-author/components/attachment/attachment.component.ts

@@ -40,7 +40,7 @@ interface opinionType {
   styleUrls: ['./attachment.component.scss'],
 })
 export class AttachmentComponent implements OnInit {
-  @Input('editFrom') editFrom: any;
+  @Input('eduTextbook') editFrom: any;
   @Input('maxWidth') maxWidth: number = 0;
   @Output() state: EventEmitter<any> = new EventEmitter<any>();
   @Output() save: EventEmitter<any> = new EventEmitter<any>();

+ 37 - 30
projects/textbook/src/modules/nav-author/components/basic-in/basic-in.component.html

@@ -6,7 +6,12 @@
     (ngSubmit)="submitForm()"
   >
     <nz-form-item style="margin-bottom: 16px">
-      <nz-form-label [nzSm]="8" [nzNoColon]="true" [nzXs]="8" nzRequired nzFor="user"
+      <nz-form-label
+        [nzSm]="8"
+        [nzNoColon]="true"
+        [nzXs]="8"
+        nzRequired
+        nzFor="user"
         >申报教材名称</nz-form-label
       >
       <nz-form-control nzErrorTip="请输入申报教材名称" [nzSm]="12" [nzXs]="12">
@@ -74,7 +79,9 @@
       </nz-form-control>
     </nz-form-item>
     <nz-form-item style="margin-bottom: 16px">
-      <nz-form-label [nzSm]="8" [nzNoColon]="true" [nzXs]="8" nzRequired>申报类型</nz-form-label>
+      <nz-form-label [nzSm]="8" [nzNoColon]="true" [nzXs]="8" nzRequired
+        >申报类型</nz-form-label
+      >
       <nz-form-control nzErrorTip="请输入申报类型" [nzSm]="12" [nzXs]="12">
         <nz-input-group>
           <!-- <input
@@ -88,12 +95,12 @@
             style="display: flex; flex-direction: column"
             formControlName="type"
           >
-            <label nz-radio nzValue="单册">单册</label>
+            <label nz-radio nzValue="单册" (click)="requiredTypeNumber(false)">单册</label>
             <div class="basic-row">
-              <label nz-radio nzValue="全册">全册</label>
+              <label nz-radio nzValue="全册" (click)="requiredTypeNumber(true)">全册</label>
               @if (validateForm.value.type =='全册') {
               <nz-input-group
-                style="flex:1; margin-left: 20px"
+                style="flex: 1; margin-left: 20px"
                 [nzSuffix]="suffixTemplateInfo"
               >
                 <input
@@ -125,8 +132,6 @@
             nzAllowClear
             nzPlaceHolder="请输入搜索内容"
             formControlName="majorPoniter"
-            (ngModelChange)="changeMajor()"
-            (nzOnSearch)="getMajor($event)"
           >
             @for(major of selectList; track major.code;let index = $index){
             <nz-option
@@ -155,8 +160,6 @@
             nzAllowClear
             nzPlaceHolder="请输入搜索内容"
             formControlName="lang"
-            (ngModelChange)="changeMajor()"
-            (nzOnSearch)="getMajor($event)"
           >
             @for(lang of selectLang; track lang.lang;let index = $index){
             <nz-option
@@ -211,23 +214,21 @@
             nzAllowClear
             nzPlaceHolder="请输入是否重点立项教材"
             formControlName="approval"
-            (ngModelChange)="changeMajor()"
-            (nzOnSearch)="getMajor($event)"
           >
-            @for(item of directorys; track item.code;let index = $index){
-            <nz-option
-              nzCustomContent
-              [nzValue]="item.code"
-              [nzLabel]="item.name"
-              >{{ item.name }}</nz-option
+            <nz-option nzCustomContent [nzValue]="true" [nzLabel]="'是'"
+              >是</nz-option
+            >
+            <nz-option nzCustomContent [nzValue]="false" [nzLabel]="'否'"
+              >否</nz-option
             >
-            }
           </nz-select>
         </nz-input-group>
       </nz-form-control>
     </nz-form-item>
     <nz-form-item style="margin-bottom: 16px">
-      <nz-form-label [nzSm]="8" [nzNoColon]="true" [nzXs]="8" nzRequired>出版单位</nz-form-label>
+      <nz-form-label [nzSm]="8" [nzNoColon]="true" [nzXs]="8" nzRequired
+        >出版单位</nz-form-label
+      >
       <nz-form-control nzErrorTip="请输入出版单位" [nzSm]="12" [nzXs]="12">
         <nz-input-group>
           <input
@@ -240,28 +241,29 @@
       </nz-form-control>
     </nz-form-item>
     <nz-form-item style="margin-bottom: 16px">
-      <nz-form-label [nzSm]="8" [nzNoColon]="true" [nzXs]="8" nzRequired>初版时间</nz-form-label>
+      <nz-form-label [nzSm]="8" [nzNoColon]="true" [nzXs]="8" nzRequired
+        >初版时间</nz-form-label
+      >
       <nz-form-control nzErrorTip="请输入初版时间" [nzSm]="12" [nzXs]="12">
         <nz-input-group>
           <nz-date-picker
             style="width: 100%"
             formControlName="editionFirst"
-            (ngModelChange)="onChange($event)"
           ></nz-date-picker>
         </nz-input-group>
       </nz-form-control>
     </nz-form-item>
     <nz-form-item style="margin-bottom: 16px">
-      <nz-form-label [nzSm]="8" [nzNoColon]="true" [nzXs]="8" nzRequired>载体形式</nz-form-label>
+      <nz-form-label [nzSm]="8" [nzNoColon]="true" [nzXs]="8" nzRequired
+        >载体形式</nz-form-label
+      >
       <nz-form-control nzErrorTip="请输入载体形式" [nzSm]="12" [nzXs]="12">
         <nz-input-group>
           <nz-select
             nzShowSearch
             nzAllowClear
             nzPlaceHolder="请输入是否重点立项教材"
-            formControlName="approval"
-            (ngModelChange)="changeMajor()"
-            (nzOnSearch)="getMajor($event)"
+            formControlName="carrierShape"
           >
             @for(item of carrierOptions; track item.code;let index = $index){
             <nz-option
@@ -285,7 +287,6 @@
             <nz-date-picker
               style="flex: 1"
               formControlName="editionDate"
-              (ngModelChange)="onChange($event)"
             ></nz-date-picker>
             <nz-input-group
               style="width: 100px; margin-left: 20px"
@@ -313,7 +314,6 @@
             <nz-date-picker
               style="flex: 1"
               formControlName="printDate"
-              (ngModelChange)="onChange($event)"
             ></nz-date-picker>
             <nz-input-group
               style="width: 100px; margin-left: 20px"
@@ -365,7 +365,7 @@
             formControlName="importantProject"
           >
             @for (item of importantProjectList; track item.value) {
-            <label nz-radio [nzValue]="item.value">{{ item.title }}</label>
+            <label (click)="onChangeRadio()" nz-radio [nzValue]="item.value">{{ item.title }}</label>
             } @if (validateForm.value.importantProject ==
             '其他省部级及以上项目') {
             <input
@@ -380,7 +380,9 @@
       </nz-form-control>
     </nz-form-item>
     <nz-form-item style="margin-bottom: 16px">
-      <nz-form-label [nzSm]="8" [nzNoColon]="true" [nzXs]="8" nzRequired>版权页截图</nz-form-label>
+      <nz-form-label [nzSm]="8" [nzNoColon]="true" [nzXs]="8" nzRequired
+        >版权页截图</nz-form-label
+      >
       <nz-form-control nzErrorTip="请输入版权页截图" [nzSm]="12" [nzXs]="12">
         <nz-input-group>
           <nz-upload
@@ -440,7 +442,12 @@
   >
     保存本页
   </button>
-  <button nz-button nzType="primary" style="background: #3e49b3; border: 1px #3e49b3" (click)="submitForm('next')">
+  <button
+    nz-button
+    nzType="primary"
+    style="background: #3e49b3; border: 1px #3e49b3"
+    (click)="submitForm('next')"
+  >
     下一页
   </button>
 </div>

+ 158 - 43
projects/textbook/src/modules/nav-author/components/basic-in/basic-in.component.ts

@@ -8,7 +8,8 @@ import { NzUploadModule } from 'ng-zorro-antd/upload';
 import { NzMessageService } from 'ng-zorro-antd/message';
 import { NzUploadChangeParam } from 'ng-zorro-antd/upload';
 import { NzModalService } from 'ng-zorro-antd/modal';
-
+import { textbookServer } from '../../../../services/textbook'
+import Parse from 'parse';
 import {
   FormControl,
   FormGroup,
@@ -29,6 +30,7 @@ import {
   styleUrls: ['./basic-in.component.scss'],
 })
 export class BasicInComponent implements OnInit {
+  @Input('eduTextbook') eduTextbook: Parse.Object|any;
   @Input('editFrom') editFrom: any;
   @Output() state: EventEmitter<any> = new EventEmitter<any>();
   @Output() save: EventEmitter<any> = new EventEmitter<any>();
@@ -46,7 +48,7 @@ export class BasicInComponent implements OnInit {
     authors: FormControl<string>; //其他主编姓名
     editor: FormControl<string>; //其他编者姓名
 
-    approval: FormControl<string>; //是否为重点立项教材
+    approval: FormControl<boolean>; //是否为重点立项教材
     editionUnit: FormControl<string>; //出版单位
     editionFirst: FormControl<Date>; //初版时间
     carrierShape: FormControl<string>; //载体形式
@@ -71,13 +73,12 @@ export class BasicInComponent implements OnInit {
     author: ['', [Validators.required]],
     unit: ['', [Validators.required]],
     type: ['', [Validators.required]],
-    typeNumber: ['', [Validators.required]],
-
+    typeNumber: [''],
     majorPoniter: ['', [Validators.required]],
     lang: ['', [Validators.required]],
     authors: ['', [Validators.required]],
     editor: ['', [Validators.required]],
-    approval: ['', [Validators.required]],
+    approval: [false],
     editionUnit: ['', [Validators.required]],
     editionFirst: [new Date(), [Validators.required]],
     carrierShape: ['', [Validators.required]],
@@ -87,9 +88,9 @@ export class BasicInComponent implements OnInit {
     printNumber: [0, [Validators.required]],
     printSum: [0, [Validators.required]],
     importantProject: ['', [Validators.required]],
-    importantProjectOther: ['', [Validators.required]],
-    copyrightImgUrl: ['', [Validators.required]],
-    CIPImgUrl: ['', [Validators.required]],
+    importantProjectOther: [''],
+    copyrightImgUrl: ['https://www.jyvtc.edu.cn/yssj/resource/cms/2022/01/2022010610314324023.pdf'],
+    CIPImgUrl: ['https://www.jyvtc.edu.cn/yssj/resource/cms/2022/01/2022010610314324023.pdf'],
     // remember: [true],
   });
   //教材应用对象及所诉学科专业类
@@ -109,21 +110,6 @@ export class BasicInComponent implements OnInit {
   ];
   //语言选择
   selectLang: Array<any> = languages.options;
-  //重点项目选择
-  directorys: Array<any> = [
-    {
-      name: '电气设计原理',
-      code: 'T001',
-    },
-    {
-      name: '微生物学',
-      code: 'T002',
-    },
-    {
-      name: '细胞学',
-      code: 'T003',
-    },
-  ];
   //载体形式
   carrierOptions: Array<any> = [
     {
@@ -161,21 +147,66 @@ export class BasicInComponent implements OnInit {
   //   return this.importantProjectList.some((item:any)=> item.value == this.validateForm.value.importantProject)
   // }
   constructor(
+    public tbookSer: textbookServer,
     private fb: NonNullableFormBuilder,
     private modal: NzModalService,
     private msg: NzMessageService
-  ) {}
-
-  ngOnInit() {}
+  ) {
+  }
 
-  changeMajor() {}
-  getMajor(e: any) {
-    console.log(e);
+  ngOnInit() {
+    console.log(this.eduTextbook);
+    this.validateForm = this.fb.group({
+      title: [this.eduTextbook?.get('title') || '', [Validators.required]],
+      ISBN: [this.eduTextbook?.get('ISBN') || '', [Validators.required]],
+      author: [this.eduTextbook?.get('author') || '', [Validators.required]],
+      unit: [this.eduTextbook?.get('unit') || '', [Validators.required]],
+      type: [this.eduTextbook?.get('type') || '', [Validators.required]],
+      typeNumber: [this.eduTextbook?.get('typeNumber') || '', [Validators.required]],
+  
+      majorPoniter: [this.eduTextbook?.get('majorPoniter') || '', [Validators.required]],
+      lang: [this.eduTextbook?.get('lang') || '', [Validators.required]],
+      authors: [this.eduTextbook?.get('authors') || '', [Validators.required]],
+      editor: [this.eduTextbook?.get('editor') || '', [Validators.required]],
+      approval: [this.eduTextbook?.get('approval') || false, [Validators.required]],
+      editionUnit: [this.eduTextbook?.get('editionUnit') || '', [Validators.required]],
+      editionFirst: [this.eduTextbook?.get('editionFirst') || new Date(), [Validators.required]],
+      carrierShape: [this.eduTextbook?.get('carrierShape') || '', [Validators.required]],
+      editionDate: [this.eduTextbook?.get('editionDate') || new Date(), [Validators.required]],
+      editionNumber: [this.eduTextbook?.get('editionNumber') || 0, [Validators.required]],
+      printDate: [this.eduTextbook?.get('printDate') || new Date(), [Validators.required]],
+      printNumber: [this.eduTextbook?.get('printNumber') || 0, [Validators.required]],
+      printSum: [this.eduTextbook?.get('printSum') || 0, [Validators.required]],
+      importantProject: [this.eduTextbook?.get('importantProject') || '', [Validators.required]],
+      importantProjectOther: [this.eduTextbook?.get('importantProjectOther') || '', [Validators.required]],
+      copyrightImgUrl: [this.eduTextbook?.get('copyrightImgUrl') || 'https://www.jyvtc.edu.cn/yssj/resource/cms/2022/01/2022010610314324023.pdf', [Validators.required]],
+      CIPImgUrl: [this.eduTextbook?.get('CIPImgUrl') || 'https://www.jyvtc.edu.cn/yssj/resource/cms/2022/01/2022010610314324023.pdf', [Validators.required]],
+      // remember: [true],
+    });
+  }
+  //校验其他省部级及以上项目是否需填
+  onChangeRadio(){
+    if (this.validateForm.value.importantProject != '其他省部级及以上项目') {
+      this.validateForm.controls.importantProjectOther.clearValidators();
+      this.validateForm.controls.importantProjectOther.markAsPristine();
+    } else {
+      this.validateForm.controls.importantProjectOther.setValidators(Validators.required);
+      this.validateForm.controls.importantProjectOther.markAsDirty();
+    }
+    this.validateForm.controls.importantProjectOther.updateValueAndValidity();
   }
-  onChange(e: any) {
-    console.log(e);
-    console.log(this.validateForm);
+  //校验全册册数是否需填
+  requiredTypeNumber(e:boolean){
+    if (e) {
+      this.validateForm.controls.typeNumber.clearValidators();
+      this.validateForm.controls.typeNumber.markAsPristine();
+    } else {
+      this.validateForm.controls.typeNumber.setValidators(Validators.required);
+      this.validateForm.controls.typeNumber.markAsDirty();
+    }
+    this.validateForm.controls.typeNumber.updateValueAndValidity();
   }
+
   handleChange(info: NzUploadChangeParam): void {
     if (info.file.status !== 'uploading') {
       console.log(info.file, info.fileList);
@@ -187,29 +218,113 @@ export class BasicInComponent implements OnInit {
     }
   }
 
-  submitForm(event?: string): void {
+  async submitForm(event?: string): Promise<void> {
     console.log(this.validateForm.value);
     if (this.validateForm.valid) {
-      console.log(this.validateForm.value);
+      // console.log(this.validateForm.value);
+      // let {
+      //   title,
+      //   ISBN,
+      //   author,
+      //   unit,
+      //   type,
+      //   typeNumber,
+      //   majorPoniter,
+      //   lang,
+      //   authors,
+      //   editor,
+      //   approval,
+      //   editionUnit,
+      //   editionFirst,
+      //   carrierShape,
+      //   editionDate,
+      //   editionNumber,
+      //   printDate,
+      //   printNumber,
+      //   printSum,
+      //   importantProject,
+      //   importantProjectOther,
+      //   copyrightImgUrl,
+      //   CIPImgUrl,
+      // } = this.validateForm.value
+      let params = this.validateForm.value
+      if(event == 'next'){
+        await this.saveEduTextbook(params, this.validateForm.valid)
+        this.state.emit({type:'next',textBook:this.eduTextbook});
+      }
     } else {
+      if(event == 'save'){
+        let params = this.validateForm.value
+        await this.saveEduTextbook(params, this.validateForm.valid)
+        this.modal.success({
+          nzTitle: '保存成功',
+          nzContent: '<p>已保存并且至空间</p>',
+          nzOnOk: () => console.log('Info OK')
+        });
+        return
+      }
       Object.values(this.validateForm.controls).forEach((control) => {
         if (control.invalid) {
           control.markAsDirty();
           control.updateValueAndValidity({ onlySelf: true });
         }
       });
+      this.msg.warning('请填写完整的内容')
+      return
+    }
+    if(event == 'save'){
+      let params = this.validateForm.value
+      await this.saveEduTextbook(params, this.validateForm.valid)
+      this.modal.success({
+        nzTitle: '保存成功',
+        nzContent: '<p>已保存并且至空间</p>',
+        nzOnOk: () => console.log('Info OK')
+      });
     }
-    if (event == 'pre') {
-      this.state.emit('pre');
+  }
+
+  async saveEduTextbook(params: any, isComplete:boolean){
+    console.log(params);
+    if(!this.eduTextbook){
+      let obj = Parse.Object.extend('EduTextbook');
+      this.eduTextbook = new obj();
     }
-    if (event == 'next') {
-      this.state.emit('next');
+    //如果填写未完整,仅保存,状态修改待完善101
+    if(this.eduTextbook.get('status') == '102' && !isComplete){
+      this.eduTextbook?.set('status', '101');
+    }else if(!this.eduTextbook.get('status')){
+      this.eduTextbook?.set('status', '101');
     }
-    if(event == 'save')
-    this.modal.success({
-      nzTitle: '保存成功',
-      nzContent: '<p>已保存并且至空间</p>',
-      nzOnOk: () => console.log('Info OK')
+    this.eduTextbook?.set('user', Parse.User.current()?.toPointer());
+    this.eduTextbook?.set('company', {
+      __type: 'Pointer',
+      className: 'Company',
+      objectId: this.tbookSer.company,
     });
+    this.eduTextbook?.set('title', params.title);
+    this.eduTextbook?.set('ISBN', params.ISBN);
+    this.eduTextbook?.set('author', params.author);
+    this.eduTextbook?.set('unit', params.unit);
+    this.eduTextbook?.set('type', params.type);
+    this.eduTextbook?.set('typeNumber', params.typeNumber);
+    this.eduTextbook?.set('majorPoniter', params.majorPoniter);
+    this.eduTextbook?.set('lang', params.lang);
+    this.eduTextbook?.set('authors', params.authors);
+    this.eduTextbook?.set('editor', params.editor);
+    this.eduTextbook?.set('approval', params.approval);
+    this.eduTextbook?.set('editionUnit', params.editionUnit);
+    this.eduTextbook?.set('editionFirst', params.editionFirst);
+    this.eduTextbook?.set('carrierShape', params.carrierShape);
+    this.eduTextbook?.set('editionDate', params.editionDate);
+    this.eduTextbook?.set('editionNumber', params.editionNumber);
+    this.eduTextbook?.set('printDate', params.printDate);
+    this.eduTextbook?.set('printNumber', params.printNumber);
+    this.eduTextbook?.set('printSum', params.printSum);
+    this.eduTextbook?.set('importantProject', params.importantProject);
+    this.eduTextbook?.set('importantProjectOther', params.importantProjectOther);
+    this.eduTextbook?.set('copyrightImgUrl', params.copyrightImgUrl);
+    this.eduTextbook?.set('CIPImgUrl', params.CIPImgUrl);
+    await this.eduTextbook?.save();
+    return
   }
 }

+ 2 - 3
projects/textbook/src/modules/nav-author/components/page-textbook/page-textbook.component.ts

@@ -1,11 +1,10 @@
 import { Component, OnInit, ViewChild } from '@angular/core';
-import { ActivatedRoute, RouterOutlet } from '@angular/router';
+import { ActivatedRoute, RouterOutlet, Router } from '@angular/router';
 import { CompTableListComponent } from '../../../../app/comp-table/comp-table-list/comp-table-list.component';
 import { EduTextbook } from '../../../../schemas/EduTextbook';
 // import { TranslateService } from '@ngx-translate/core';
 import * as Parse from "parse";
 import { CommonModule } from '@angular/common';
-
 @Component({
   selector: 'app-page-textbook',
   templateUrl: './page-textbook.component.html',
@@ -23,7 +22,7 @@ export class PageTextbookComponent implements OnInit {
   fieldsArray:Array<any>|undefined
 
   constructor(
-    private route: ActivatedRoute,
+    private router: Router,
     private activeRoute: ActivatedRoute,
     // private translate:TranslateService,
   ) {

+ 2 - 1
projects/textbook/src/modules/nav-author/components/textbook-content/textbook-content.component.ts

@@ -8,6 +8,7 @@ import { NzGridModule } from 'ng-zorro-antd/grid';
 import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
 import { NzTableModule } from 'ng-zorro-antd/table';
 import { NzModalService } from 'ng-zorro-antd/modal';
+import Parse from 'parse';
 import {
   FormControl,
   FormGroup,
@@ -38,7 +39,7 @@ interface course {
   styleUrls: ['./textbook-content.component.scss'],
 })
 export class TextbookContentComponent  implements OnInit {
-  @Input('editFrom') editFrom: any;
+  @Input('eduTextbook') editFrom: any;
   @Input('maxWidth') maxWidth: number = 0;
   @Output() state: EventEmitter<any> = new EventEmitter<any>();
   @Output() save: EventEmitter<any> = new EventEmitter<any>();

+ 82 - 17
projects/textbook/src/modules/nav-author/components/textbook-pertain/textbook-pertain.component.ts

@@ -8,12 +8,14 @@ import { NzGridModule } from 'ng-zorro-antd/grid';
 import { NzCheckboxModule } from 'ng-zorro-antd/checkbox';
 import { NzTableModule } from 'ng-zorro-antd/table';
 import { NzModalService } from 'ng-zorro-antd/modal';
+import Parse from 'parse';
 import {
   FormControl,
   FormGroup,
   NonNullableFormBuilder,
   Validators,
 } from '@angular/forms';
+import { textbookServer } from '../../../../services/textbook';
 interface author {
   name: string;
   unit: string;
@@ -46,7 +48,8 @@ interface achievementType {
   styleUrls: ['./textbook-pertain.component.scss'],
 })
 export class TextbookPertainComponent implements OnInit {
-  @Input('editFrom') editFrom: any;
+  @Input('eduTextbook') eduTextbook: Parse.Object|any;
+
   @Input('maxWidth') maxWidth: number = 0;
   @Output() state: EventEmitter<any> = new EventEmitter<any>();
   @Output() save: EventEmitter<any> = new EventEmitter<any>();
@@ -55,7 +58,7 @@ export class TextbookPertainComponent implements OnInit {
     period: FormControl<number | any>; //课程学时
     lessons: FormControl<string>; //适用课程
     characteristic: FormControl<Array<any> | any>; //适用课程性质
-    // authors: FormControl<Array<any> | any>; //作者信息
+    // authorList: FormControl<Array<any> | any>; //作者信息
     authorDetails: FormControl<string>; //第一主编(作者)相关教学经历
     // achievement: FormControl<Array<any> | any>; //相关科学研究项目、成果或论文专著(限5项)
   }> = this.fb.group({
@@ -63,7 +66,7 @@ export class TextbookPertainComponent implements OnInit {
     period: ['', [Validators.required]],
     lessons: ['', [Validators.required]],
     characteristic: ['', [Validators.required]],
-    // authors: ['', [Validators.required]],
+    // authorList: ['', [Validators.required]],
     authorDetails: ['', [Validators.required]],
     // achievement: ['', [Validators.required]],
   });
@@ -125,17 +128,50 @@ export class TextbookPertainComponent implements OnInit {
   ];
   workOptions = ['主编', '作者', '副主编', '写者'];
   constructor(
+    public tbookSer: textbookServer,
     private fb: NonNullableFormBuilder,
     private modal: NzModalService,
     private msg: NzMessageService
   ) {}
 
-  ngOnInit() {}
-  submitForm(event?: string): void {
+  ngOnInit() {
+    console.log(this.eduTextbook);
+    if(this.eduTextbook.id){
+      this.checkOptionsOne = this.eduTextbook.get('characteristic') || this.checkOptionsOne
+      let major = this.selectList.find(item=> item.code == this.eduTextbook.get('major')?.code)
+      let lessons = this.eduTextbook.get('lessons')?.join(';')
+      this.validateForm = this.fb.group({
+        major: [major?.code || '', [Validators.required]],
+        period: [this.eduTextbook.get('period') || '', [Validators.required]],
+        lessons: [lessons || '', [Validators.required]],
+        characteristic: [this.eduTextbook.get('characteristic') || '', [Validators.required]],
+        authorDetails: [this.eduTextbook.get('authorDetails') || '', [Validators.required]],
+      });
+    }
+
+  }
+  async submitForm(event?: string):Promise<void>{
     console.log(this.validateForm.value);
     if (this.validateForm.valid) {
-      console.log(this.validateForm.value);
+      let params = this.validateForm.value
+      if (event == 'next') {
+        await this.saveEduTextbook(params, this.validateForm.valid)
+        this.state.emit({type:'next',textBook:this.eduTextbook});
+      }
     } else {
+      if(event == 'pre'){
+        this.state.emit({type:'pre'});
+      }
+      else if(event == 'save'){
+        let params = this.validateForm.value
+        await this.saveEduTextbook(params, this.validateForm.valid)
+        this.modal.success({
+          nzTitle: '保存成功',
+          nzContent: '<p>已保存并且至空间</p>',
+          nzOnOk: () => console.log('Info OK')
+        });
+        return
+      }
       Object.values(this.validateForm.controls).forEach((control) => {
         if (control.invalid) {
           control.markAsDirty();
@@ -144,18 +180,15 @@ export class TextbookPertainComponent implements OnInit {
       });
       this.msg.warning('请填写完整信息');
     }
-    if (event == 'pre') {
-      this.state.emit('pre');
-    }
-    if (event == 'next') {
-      this.state.emit('next');
+    if(event == 'save'){
+      let params = this.validateForm.value
+      await this.saveEduTextbook(params, this.validateForm.valid)
+      this.modal.success({
+        nzTitle: '保存成功',
+        nzContent: '<p>已保存并且至空间</p>',
+        nzOnOk: () => console.log('Info OK')
+      });
     }
-    if(event == 'complete')
-    this.modal.success({
-      nzTitle: '您已填写完成',
-      nzContent: '<p>已保存并且至空间</p>',
-      nzOnOk: () => console.log('Info OK')
-    });
   }
   changeCode() {}
   getCode(e: any) {}
@@ -226,4 +259,36 @@ export class TextbookPertainComponent implements OnInit {
         break;
     }
   }
+
+  async saveEduTextbook(params: any, isComplete:boolean){
+    console.log(params);
+    if(!this.eduTextbook){
+      this.msg.error('请先创建教材')
+      return
+      // let obj = Parse.Object.extend('EduTextbook');
+      // this.eduTextbook = new obj();
+    }
+    //如果填写未完整,仅保存,状态修改待完善101
+    if(this.eduTextbook.get('status') == '102' && !isComplete){
+      this.eduTextbook?.set('status', '101');
+    }else if(!this.eduTextbook.get('status')){
+      this.eduTextbook?.set('status', '101');
+    }
+    this.eduTextbook?.set('user', Parse.User.current()?.toPointer());
+    this.eduTextbook?.set('company', {
+      __type: 'Pointer',
+      className: 'Company',
+      objectId: this.tbookSer.company,
+    });
+    let major = this.selectList.find(item=> item.code == params.major)
+    let lessons = params.lessons.split(';')
+    this.eduTextbook?.set('major', major);
+    params.period && this.eduTextbook?.set('period', params.period);
+    params.period && this.eduTextbook?.set('lessons', lessons);
+    this.eduTextbook?.set('characteristic', params.characteristic);
+    this.eduTextbook?.set('authorDetails', params.authorDetails);
+
+    await this.eduTextbook?.save();
+    return
+  }
 }

+ 10 - 5
projects/textbook/src/schemas/EduTextbook.ts

@@ -1,4 +1,5 @@
 import { MatDialog } from "@angular/material/dialog";
+import { Router } from "@angular/router";
 import Parse from "parse";
 import { ParseSchema } from "./func-parse";
 
@@ -8,20 +9,24 @@ export const EduTextbook:ParseSchema = {
     emptyImg:"/img/webhook-empty.png",
     include:["user"],
     buttons:[
+        // 仅在 /nav-author/manage/space 显示的编辑
         {
             name:"编辑",
             place:"item",
             show:(options:{object:Parse.Object})=>{
-                return true
+                if(location?.pathname=='/nav-author/manage/space'){
+                    return true
+                }
+                return false
             },
-            // handle:(options:{clusterServ:ClusterService,dialog:MatDialog,object:Parse.Object})=>{
-            //     options?.clusterServ?.openMinerDialog(options?.dialog,options?.object)
-            // }
+            handle:(options:{dialog:MatDialog,object:Parse.Object,router?:Router})=>{
+                options.router?.navigate(['/nav-author/manage/apply',{id:options.object.id}])
+            }
         },
     ],
     fieldsArray:[
         {key:"title",name:"教材名称",type:"String",isHeader:true},
         {key:"desc",name:"教材描述",type:"String"},
-        {key:"user",name:"创建人",type:"Pointer",targetClass:"_User",isHeader:true,showName:"${name}"},
+        {key:"user",name:"创建人",type:"Pointer",targetClass:"_User",isHeader:true,showName:"${mobile}"},
     ]
 }

+ 100 - 55
projects/textbook/src/services/auth.service.ts

@@ -1,9 +1,13 @@
-import { Injectable } from "@angular/core";
-import Parse from "parse";
-import { Router } from "@angular/router";
-import { textbookServer } from './textbook'
+import { Injectable } from '@angular/core';
+import Parse from 'parse';
+import { Router } from '@angular/router';
+import { textbookServer } from './textbook';
+import { catchError } from 'rxjs/operators';
+import { HttpClient } from '@angular/common/http';
+import { NzMessageService } from 'ng-zorro-antd/message';
+
 @Injectable({
-  providedIn: "root",
+  providedIn: 'root',
 })
 export class AuthServr {
   isLoggedIn = false;
@@ -11,80 +15,121 @@ export class AuthServr {
   regcountdown: number = 60; //注册验证码倒计时
   resetcountdown: number = 60; //重置密码验证码倒计时
   redirectUrl: string = '';
-  roterPath: Array<any> = [
-    {
-      name: '国家级管理员',
-      type: 1,
-      route: '/nav-admin',
-    },
-    {
-      name: '省级教育行政部门',
-      type: 2,
-      route: '/nav-province-submit',
-    },
-    {
-      name: '流程管理员登录',
-      type: 3,
-      route: '/nav-province-contact',
-    },
-    {
-      name: '省属高校联系人',
-      type: 4,
-      route: '/nav-province-school-contact',
-    },
-    {
-      name: '教材评审组成员',
-      type: 5,
-      route: '/nav-review',
-    },
-    {
-      name: '作者/教师/主编',
-      type: 6,
-      route:'/nav-author/manage/space'
-    },
-  ];
-  constructor(public router: Router,private textbook:textbookServer) {}
-  login(username:any, password:any, company:string) {
+  roterPath: any = {
+    "国家级管理员":'/nav-admin',
+    "省级教育行政部门":"/nav-province-submit",
+    "流程管理员登录":"/nav-province-contact",
+    "省属高校联系人":"/nav-province-school-contact",
+    "教材评审组成员":"/nav-review",
+    "作者/教师/主编":"/nav-author/manage/space",
+  }
+  constructor(
+    public router: Router,
+    private textbook: textbookServer,
+    private http: HttpClient,
+    private message: NzMessageService,
+  ) {}
+  login(username: any, password: any, company: string) {
     return new Promise(async (resolve, reject) => {
       let a = /^1[3456789]\d{9}$/;
       //如果是手机号登录,获取对应的mobile
       if (String(username).match(a)) {
-        let query = new Parse.Query("_User");
-        query.equalTo("company", company);
-        query.equalTo("mobile", username);
+        let query = new Parse.Query('_User');
+        query.equalTo('company', company);
+        query.equalTo('mobile', username);
         let res = await query.first();
         if (res?.id) {
-          username = res.get("username");
+          username = res.get('username');
         } else {
-          reject({ message: "用户不存在" });
+          reject({ message: '用户不存在' });
           return;
         }
       }
       Parse.User.logIn(username, password)
-        .then(async (data:any) => {
-          // await this.developerSer.authDevCompany(); //更新开发者信息
+        .then(async (data: any) => {
           console.log(data);
-          localStorage.setItem('profile',JSON.stringify({type:'navAuthor'}))
-          this.textbook.profile = {type:'navAuthor'}
-          this.router.navigate([this.roterPath[5].route]);
+          await this.profileVerify()
           resolve(data);
         })
-        .catch((err:any) => {
+        .catch((err: any) => {
           console.log(err.message);
-          if (err.message.indexOf("Invalid username/password.") != -1) {
-            reject({ message: "用户名或密码不正确" });
+          if (err.message.indexOf('Invalid username/password.') != -1) {
+            reject({ message: '用户名或密码不正确' });
           } else {
-            reject({ message: "用户名或密码不正确" });
+            reject({ message: '用户名或密码不正确' });
+          }
+        });
+    });
+  }
+  //手机号注册/登录
+  register(mobile: any, code: any, company: string) {
+    return new Promise((resolve, reject) => {
+      let host =
+        (Parse as any).serverURL?.split('parse')?.[0] ||
+        'https://server.fmode.cn/';
+      this.http
+        .post(host + 'api/textbook/register', {
+          company: company,
+          mobile: mobile,
+          code:code
+        })
+        .pipe(
+          catchError(async (e) => {
+            reject(e);
+          })
+        )
+        .subscribe((res: any) => {
+          console.log(res);
+          if (res?.code == 200) {
+            let token = res.data.token;
+            Parse.User.become(token)
+              .then(async (data: any) => {
+                console.log(data);
+                await this.profileVerify()
+                resolve(data)
+              })
+              .catch((err: any) => {
+                console.log(err.message);
+                if (err.message.indexOf('Invalid username/password.') != -1) {
+                  reject({ message: '用户名或密码不正确' });
+                } else {
+                  reject({ message: '用户名或密码不正确' });
+                }
+              });
           }
         });
     });
   }
+  //验证用户类型
+  async profileVerify(){
+    let query = new Parse.Query('Profile')
+    query.equalTo('user',Parse.User.current()?.id)
+    query.notEqualTo('isDeleted',true)
+    let r = await query.first()
+    if(r?.id){
+      if(r.get('identity')){
+        this.router.navigate([this.roterPath[r.get('identity')]])
+        let profile = r.toJSON()
+        localStorage.setItem(
+          'profile',
+          JSON.stringify(profile)
+        );
+        this.textbook.profile = profile;
+      }
+      else if(!r.get('verify')){
+        this.message.warning('请完善个人信息')
+        this.router.navigate(['/user/account_info'])
+      }
+    }else{
+      this.router.navigate(['/user/account_info'])
+    }
+  }
 
   logout(): void {
     Parse.User.logOut().then((user) => {
       // this.developerSer.companyId = ''
-      window.localStorage.clear()
-      this.router.navigate(["/user/login"]);
+      window.localStorage.clear();
+      this.router.navigate(['/user/login']);
     });
   }
 }

+ 153 - 4
server/api/textbook/routes.js

@@ -1,16 +1,77 @@
-let Parse = global.Parse;
+let Parse = global.Parse
 const router = require('express').Router();
 let bodyParser = require('body-parser')
+const pgp = require('pg-promise')();
+
 router.use(bodyParser.json({ limit: '10mb' }))
 router.use(bodyParser.urlencoded({ extended: false }))
 
-router.use("/test",async (req,res,next)=>{
+router.use("/test", async (req, res, next) => {
     res.json({
-        code:200,
-        data:"test"
+        code: 200,
+        data: "test"
     })
 })
 
+router.post("/register", async (req, res, next) => {
+    let company = req.body.company;
+    let mobile = req.body.mobile;
+    let code = req.body.code;
+    let password = req.body.password
+    if (!company || !mobile || !code) {
+        goWrong(req, "参数不完整,请检查");
+        return
+    }
+    // 检查用户信息
+    let user;
+    user = await getUserByMobile(mobile, company);
+    if (user) {
+        // goWrong(req, "用户已存在,请登录")
+        let token = await setMobileSessionToken(user)
+        res.json({
+            code: 200,
+            msg: '用户已存在,请登录',
+            data: {
+                userId: user.id,
+                username: user.get("username"),
+                mobile: user.get("mobile"),
+                token: token?.get("sessionToken")
+            }
+        })
+        return
+    }
+    try {
+        let _User = Parse.Object.extend("_User")
+        let User = new _User
+        User.set("username", company + mobile)
+        User.set("password", company + mobile)
+        User.set("company", {
+            __type: 'Pointer',
+            className: 'Company',
+            objectId: company
+        })
+        User.set("mobile", mobile)
+        User.set("status", 'normal')
+        let saveUser = await User.save({ useMasterKey: true })
+        let token = await setMobileSessionToken(saveUser)
+        res.json({
+            code: 200,
+            msg: '注册成功',
+            data: {
+                userId: saveUser.id,
+                username: saveUser.get("username"),
+                mobile: saveUser.get("mobile"),
+                token: token?.get("sessionToken")
+            }
+        })
+
+    } catch (error) {
+        console.log(error)
+        return
+    }
+
+})
+
 function goWrong(response, msg) {
     response.status(500)
     response.json({
@@ -19,5 +80,93 @@ function goWrong(response, msg) {
     })
     return
 }
+/* 检查用户是否存在 */
+function getUserByMobile(mobile, company) {
+    let query = new global.Parse.Query("_User");
+    query.equalTo("mobile", mobile);
+    query.equalTo("company", company);
+    return query.first({ useMasterKey: true });
+}
+/* 创建_Session并返回 */
+const db = pgp({
+    user: 'postgres',
+    password: 'postgres',
+    host: 'localhost',
+    port: 25432,
+    database: 'postgres'
+});
 
+async function setMobileSessionToken(user, reset) {
+    try {
+        // let Session = global.Parse.Object.extend('_Session');
+        if (user) {
+            //获取最后一次的token,如果过期时间大于未来2小时直接返回,若不是重置
+            let nowTime = new Date().getTime() + (1000 * 60 * 120)
+            let query = new Parse.Query('_Session')
+            query.equalTo('user', user.id)
+            query.descending('expiresAt')
+            query.greaterThan('expiresAt', new Date(nowTime))
+            query.select('expiresAt', 'sessionToken')
+            let reqSessionToken = await query.first({ useMasterKey: true })
+            if (reqSessionToken?.id && reqSessionToken.get('sessionToken')) {
+                return reqSessionToken
+            }
+            let username = user?.username || user?.get('username')
+            let sessionToken = "r:" + username + (new Date().getTime() / 1000).toFixed();
+            // 创建新Session登录系统
+            let Session = Parse.Object.extend('_Session');
+            let session = new Session()
+            let r = await session.save(null, {
+                useMasterKey: true
+            })
+            const objectId = r.id;
+            let now = new Date();
+            let expiresAt = (now.getFullYear() + 1) + '-' + (now.getMonth() + 1) + '-' + now.getDate() + ' 12:00:00'
+            console.log(expiresAt);
+            let sql = `INSERT INTO "_Session" ("objectId","user", "sessionToken", "expiresAt", "createdWith")
+            VALUES
+            ('${objectId}','${user.id}', '${sessionToken}','${expiresAt}','{"action": "login","authProvider": "appPassword"}')
+            ON CONFLICT("objectId") DO UPDATE
+            SET 
+            "user" = excluded."user",
+            "sessionToken" = excluded."sessionToken",
+            "expiresAt"=excluded."expiresAt",
+            "createdWith"=excluded."createdWith"
+            `
+            const data = await db.any(sql);
+            console.log('_Session', data)
+            let doneObj = {
+                get(field = 'sessionToken') {
+                    if (field = 'sessionToken') return sessionToken
+                }
+            }
+            // let session = new Session();
+            // session.set("user", {
+            //     __type: 'Pointer',
+            //     className: '_User',
+            //     objectId: user.id
+            // });
+            // session.set("sessionToken", sessionToken);
+            // let now = new Date();
+            // now.setFullYear(now.getFullYear() + 1);
+            // let expiresAt = now;
+            // session.set("expiresAt", expiresAt);
+            // session.set("createdWith", {
+            //     "action": "login",
+            //     "authProvider": "appPassword"
+            // })
+            // session.set("restricted", false)
+            // let doneObj = await session.save(null, {
+            //     useMasterKey: true
+            // })
+            // if (!doneObj) {
+            //     return false
+            // }
+            return doneObj
+        }
+    } catch (err) {
+        console.log(err)
+        return false
+    }
+}
 module.exports = router;

+ 1 - 0
server/db/index.js

@@ -2,6 +2,7 @@
 const EduSchemas = [
     require("./schemas/Company").Company,
     require("./schemas/EduTextbook").EduTextbook,
+    require("./schemas/Profile").Profile,
     require("./schemas/EduCollection").EduCollection,
     require("./schemas/_User")._User,
     require("./schemas/_Role")._Role,

+ 10 - 2
server/db/schemas/EduTextbook.js

@@ -2,6 +2,7 @@ const EduTextbook = {
     "className": "EduTextbook",
     "fields": {
         "status": {
+            //状态 101:待完善资料 102:可提交状态
             "type": "String",
             "required": false
         },
@@ -10,7 +11,14 @@ const EduTextbook = {
             "targetClass":"_User",
             "required": false
         },
-
+        "render": { //是否提交
+            "type": "Boolean",
+            "required": false
+        },
+        "isDeleted": {
+            "type": "Boolean",
+            "required": false
+        },
         "title": {
             "type": "String",
             "required": false
@@ -52,7 +60,7 @@ const EduTextbook = {
             "required": false
         },
         "approval": {
-            "type": "String",
+            "type": "Boolean",
             "required": false
         },
         "edition": {

+ 73 - 0
server/db/schemas/Profile.js

@@ -0,0 +1,73 @@
+const Profile = {
+  "className": "Profile",
+  "fields": {
+    "user": {
+      "type": "Pointer",
+      "targetClass": "_User",
+      "required": false
+    },
+    "name": {
+      "type": "String"
+    },
+    "mobile": {
+      "type": "String"
+    },
+    "email": {
+      "type": "String"
+    },
+    "workPhone": {
+      "type": "String"
+    },
+    "province": {
+      "type": "String"
+    },
+    "unitType": {
+      "type": "String"
+    },
+    "unit": {
+      "type": "String"
+    },
+    "branch": {
+      "type": "String"
+    },
+    "job": {
+      "type": "String"
+    },
+    "idcard": {
+      "type": "String"
+    },
+    "identity": {
+      "type": "String"
+    },
+    "file": {
+      "type": "String"
+    }
+  },
+  "classLevelPermissions": {
+    "find": {
+      "*": true
+    },
+    "get": {
+      "*": true
+    },
+    "count": {
+      "*": true
+    },
+    "create": {
+      "*": true
+    },
+    "update": {
+      "*": true
+    },
+    "delete": {
+      "*": true
+    },
+    "addField": {
+      "*": true
+    },
+    "protectedFields": {
+      "*": []
+    }
+  }
+}
+module.exports.Profile = Profile