瀏覽代碼

选择成员

dusenyao 1 月之前
父節點
當前提交
d111377781

+ 28 - 0
package-lock.json

@@ -16,6 +16,7 @@
         "dompurify": "^3.1.5",
         "element-ui": "^2.15.14",
         "hanzi-writer": "^3.7.0",
+        "jquery": "^3.7.1",
         "js-audio-recorder": "^1.0.7",
         "js-cookie": "^3.0.5",
         "mathjax": "^3.2.2",
@@ -23,6 +24,7 @@
         "nprogress": "^0.2.0",
         "tinymce": "^5.10.9",
         "vue": "^2.6.14",
+        "vue-esign": "^1.1.4",
         "vue-router": "^3.6.5",
         "vuedraggable": "^2.24.3",
         "vuex": "^3.6.2"
@@ -11766,6 +11768,11 @@
         "@sideway/pinpoint": "^2.0.0"
       }
     },
+    "node_modules/jquery": {
+      "version": "3.7.1",
+      "resolved": "https://registry.npmmirror.com/jquery/-/jquery-3.7.1.tgz",
+      "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
+    },
     "node_modules/js-audio-recorder": {
       "version": "1.0.7",
       "resolved": "https://registry.npmmirror.com/js-audio-recorder/-/js-audio-recorder-1.0.7.tgz",
@@ -19082,6 +19089,14 @@
         "csstype": "^3.1.0"
       }
     },
+    "node_modules/vue-esign": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/vue-esign/-/vue-esign-1.1.4.tgz",
+      "integrity": "sha512-7Ix5PdcyyhVfsvrT9a+yp5+36gbQ0/bpDO+QSLT58IgJ5t164PEptOy5Nslw8bZbk3n3Hc7SP5B8eXQ8X8W+OA==",
+      "dependencies": {
+        "vue": "^2.5.11"
+      }
+    },
     "node_modules/vue-eslint-parser": {
       "version": "9.4.3",
       "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",
@@ -28744,6 +28759,11 @@
         "@sideway/pinpoint": "^2.0.0"
       }
     },
+    "jquery": {
+      "version": "3.7.1",
+      "resolved": "https://registry.npmmirror.com/jquery/-/jquery-3.7.1.tgz",
+      "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg=="
+    },
     "js-audio-recorder": {
       "version": "1.0.7",
       "resolved": "https://registry.npmmirror.com/js-audio-recorder/-/js-audio-recorder-1.0.7.tgz",
@@ -34186,6 +34206,14 @@
         "csstype": "^3.1.0"
       }
     },
+    "vue-esign": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmmirror.com/vue-esign/-/vue-esign-1.1.4.tgz",
+      "integrity": "sha512-7Ix5PdcyyhVfsvrT9a+yp5+36gbQ0/bpDO+QSLT58IgJ5t164PEptOy5Nslw8bZbk3n3Hc7SP5B8eXQ8X8W+OA==",
+      "requires": {
+        "vue": "^2.5.11"
+      }
+    },
     "vue-eslint-parser": {
       "version": "9.4.3",
       "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",

+ 0 - 1
package.json

@@ -27,7 +27,6 @@
     "hanzi-writer": "^3.7.0",
     "jquery": "^3.7.1",
     "js-audio-recorder": "^1.0.7",
-    "js-cookie": "^3.0.5",
     "mathjax": "^3.2.2",
     "md5": "^2.3.0",
     "nprogress": "^0.2.0",

+ 2 - 2
src/api/list.js

@@ -27,7 +27,7 @@ export function queryOrgList(data) {
   });
 }
 
-// 分页查询内置管理员用户列表
+// 分页查询用户列表
 export function queryUserList(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=page_query-PageQueryUserList`, data, {
     baseURL: process.env.VUE_APP_EEP,
@@ -39,4 +39,4 @@ export function queryOrgUserList(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=page_query-PageQueryMyOrgUserList_OrgManager`, data, {
     baseURL: process.env.VUE_APP_EEP,
   });
-}
+}

+ 9 - 2
src/api/user.js

@@ -17,8 +17,8 @@ export function GetMyUserInfo(data) {
   return http.post(`${process.env.VUE_APP_FileServer}?MethodName=user_manager-GetMyUserInfo`, data);
 }
 
-// 注册页面机构列表
-export function orgIndexList (data) {
+// 得到机构列表
+export function orgIndexList(data) {
   return http.post(`${process.env.VUE_APP_EepServer}?MethodName=org_manager-GetOrgList`, data, {
     baseURL: process.env.VUE_APP_EEP,
   });
@@ -37,3 +37,10 @@ export function registerUser(data) {
     baseURL: process.env.VUE_APP_EEP,
   });
 }
+
+// 得到用户列表(指定ID)
+export function GetUserList_ID(data) {
+  return http.post(`${process.env.VUE_APP_EepServer}?MethodName=user_manager-GetUserList_ID`, data, {
+    baseURL: process.env.VUE_APP_EEP,
+  });
+}

+ 21 - 23
src/layouts/default/header/index.vue

@@ -44,18 +44,32 @@ export default {
   name: 'LayoutHeader',
   data() {
     const token = getToken();
-
-    return {
-      token: this.$store.state.user || token,
-      activePro: '', // 当前选中的项目
-      LoginNavIndex: 0, // 当前选中的项目索引
-      // 项目列表
-      projectList: [
+    // 用户类型与菜单映射
+    const projectListMap = {
+      USER: [
         { key: '', name: '个人工作台' },
         { key: 'project_manage', name: '项目管理' },
         { key: 'create_project', name: '创建项目' },
         { key: 'personal_center', name: '个人中心' },
       ],
+      ORG_MANAGER: [
+        { key: 'project_manage', name: '项目管理' },
+        { key: 'user_manage_org', name: '用户管理' },
+        { key: 'personal_center', name: '个人中心' },
+      ],
+      ADMIN: [
+        { key: 'org_manage', name: '机构管理' },
+        { key: 'user_manage', name: '用户管理' },
+        { key: 'system_config', name: '系统配置' },
+      ],
+    };
+    // 获取当前用户类型
+    const userType = (token?.user_type ?? 'USER').toUpperCase();
+    return {
+      token: this.$store.state.user || token,
+      activePro: '',
+      LoginNavIndex: 0,
+      projectList: projectListMap[userType] || projectListMap.USER,
     };
   },
   methods: {
@@ -73,22 +87,6 @@ export default {
       this.$router.push({ path: `/${key}` });
     },
   },
-  created() {
-    const userType = this.token?.user_type ?? '';
-    if (userType === 'ORG_MANAGER') {
-      this.projectList = [
-        { key: 'project_manage', name: '项目管理' },
-        { key: 'user_manage_org', name: '用户管理' },
-        { key: 'personal_center', name: '个人中心' },
-      ];
-    } else if (userType === 'ADMIN') {
-      this.projectList = [
-        { key: 'org_manage', name: '机构管理' },
-        { key: 'user_manage', name: '用户管理' },
-        { key: 'system_config', name: '系统配置' },
-      ];
-    }
-  },
 };
 </script>
 

+ 26 - 13
src/router/guard/index.js

@@ -1,4 +1,4 @@
-import { getConfig } from '@/utils/auth';
+import { getToken } from '@/utils/auth';
 
 import NProgress from 'nprogress';
 import 'nprogress/nprogress.css';
@@ -6,30 +6,43 @@ NProgress.configure({ showSpinner: false });
 
 const whiteList = ['/login', '/image_change']; // 重定向白名单
 
+// 用户类型对应的跳转路径
+export const userTypeToJump = {
+  USER: '/',
+  ORG_MANAGER: '/project_manage',
+  ADMIN: '/org_manage',
+};
+
+export function getUserTypeToJump(type) {
+  return userTypeToJump[type] || '/';
+}
+
 export function setupRouterGuard(router) {
   // 全局前置守卫
   router.beforeEach(async (to, from, next) => {
     NProgress.start();
 
-    if (getConfig()) {
+    const token = getToken();
+
+    const userType = token?.user_type ?? '';
+
+    if (token) {
       if (to.path === '/login') {
-        next({ path: '/' });
+        next({ path: getUserTypeToJump(userType) });
         NProgress.done();
-      } else {
-        next();
+        return;
       }
+      next();
+      return;
     } else if (whiteList.includes(to.path)) {
       // 在登录白名单中,直接进入
       next();
-    } else {
-      // 其他无权访问的页面将重定向到登录页面
-      if (process.env.NODE_ENV === 'development') {
-        next('/login');
-      } else {
-        next('/login');
-      }
-      NProgress.done();
+      return;
     }
+    // 其他无权访问的页面将重定向到登录页面
+    next('/login');
+    NProgress.done();
+    return;
   });
 
   // 全局后置钩子

+ 10 - 1
src/router/modules/project.js

@@ -56,7 +56,7 @@ const personalWorkPage = {
       },
       component: () => import('@/views/personal_workbench/project/index.vue'),
     },
-    // 制作与审校管理
+    // 我的项目 -> 制作与审校管理
     {
       path: `/personal_workbench/production_editorial/:id`,
       name: 'ProductionEditorial',
@@ -65,6 +65,15 @@ const personalWorkPage = {
       },
       component: () => import('@/views/personal_workbench/project/ProductionEditorialManage.vue'),
     },
+    // 我的项目 -> 项目信息管理
+    {
+      path: `/personal_workbench/project_info/:id`,
+      name: 'ProjectInfo',
+      meta: {
+        title: '项目信息管理',
+      },
+      component: () => import('@/views/personal_workbench/project/ProjectInfoManage.vue'),
+    },
   ],
 };
 

+ 1 - 1
src/store/modules/user.js

@@ -49,7 +49,7 @@ const actions = {
         .then((response) => {
           setToken(response);
           commit(user.SET_USER_INFO, response);
-          reslove();
+          reslove(response.user_type);
         })
         .catch((error) => {
           reject(error);

+ 1 - 0
src/styles/common.scss

@@ -12,6 +12,7 @@
   align-items: center;
   font-size: 14px;
   color: $main-color;
+  white-space: nowrap;
   cursor: pointer;
 
   & + & {

+ 59 - 14
src/utils/auth.js

@@ -1,38 +1,83 @@
-import Cookies from 'js-cookie'
+/**
+ * 设置带过期时间的数据
+ * @param {string} key - 存储的键
+ * @param {array} value - 存储的值
+ * @param {number} expiryDays - 过期天数
+ * @returns {void}
+ */
+function setItemWithExpiry(key, value, expiryDays) {
+  const _val = typeof value === 'object' ? JSON.stringify(value) : value;
+  const now = new Date();
+  // 计算过期时间的时间戳(毫秒)
+  const expiryTime = now.getTime() + expiryDays * 24 * 60 * 60 * 1000;
+  const item = {
+    value: _val,
+    expiry: expiryTime,
+  };
+  localStorage.setItem(key, JSON.stringify(item));
+}
+
+/**
+ * 获取数据时判断是否过期
+ * @param {string} key - 存储的键
+ * @returns {Object|null}
+ */
+function getItemWithExpiry(key) {
+  const itemStr = localStorage.getItem(key);
+
+  if (!itemStr) {
+    return null;
+  }
+  try {
+    const item = JSON.parse(itemStr);
+    const now = new Date();
+
+    if (now.getTime() > item.expiry) {
+      console.log('ss');
+
+      localStorage.removeItem(key);
+      return null;
+    }
+
+    return JSON.parse(item.value);
+  } catch (e) {
+    // 解析失败,直接返回null
+    return null;
+  }
+}
+
 const TokenKey = 'GCLS_Token';
 
 export function getSessionID() {
-  const token = Cookies.get(TokenKey);
-  const _token = token ? JSON.parse(token) : null;
-  return _token ? _token.session_id ?? '' : '';
+  const token = getItemWithExpiry(TokenKey);
+  return token ? token.session_id ?? '' : '';
 }
 
 export function getToken() {
-  const token = Cookies.get(TokenKey);
-  return token ? JSON.parse(token) : null;
+  const token = getItemWithExpiry(TokenKey);
+
+  return token;
 }
 
 export function setToken(token) {
-  const _token = typeof token === 'object' ? JSON.stringify(token) : '';
-  Cookies.set(TokenKey, _token, { expires: 10, });
+  setItemWithExpiry(TokenKey, token, 10);
 }
 
 export function removeToken() {
-  Cookies.remove(TokenKey);
+  localStorage.removeItem(TokenKey);
 }
 
 const ConfigKey = 'GCLS_Config';
 
 export function getConfig() {
-  const config = Cookies.get(ConfigKey);
-  return config ? JSON.parse(config) : null;
+  const config = getItemWithExpiry(ConfigKey);
+  return config;
 }
 
 export function setConfig(value) {
-  let _val = typeof value === 'object' ? JSON.stringify(value) : '';
-  Cookies.set(ConfigKey, _val, { expires: 10, });
+  setItemWithExpiry(ConfigKey, value, 10);
 }
 
 export function removeConfig() {
-  Cookies.remove(ConfigKey);
+  localStorage.removeItem(ConfigKey);
 }

+ 41 - 41
src/views/book/courseware/preview/components/article/components/FreewriteLettle.vue

@@ -2,7 +2,6 @@
 <template>
   <div class="practice practiceSingleNPC">
     <img
-      @click="changePraShow()"
       class="close-icon"
       :src="
         themeColor
@@ -13,6 +12,7 @@
               : require('@/assets/icon/Cross-16-normal-red.png')
           : require('@/assets/icon/Cross-16-normal-red.png')
       "
+      @click="changePraShow()"
     />
     <div class="right-content">
       <div class="right-strockred">
@@ -20,21 +20,21 @@
           <img class="img" :src="data.strokes_image_url" alt="" />
         </template>
         <FreeWriteQP
-          :bgColor.sync="bgColor"
+          id="esign"
+          ref="esign"
+          :bg-color.sync="bgColor"
           :height="height"
-          :isCrop="isCrop"
-          :lineColor="hanzicolor"
-          :lineWidth="hanziweight"
+          :is-crop="isCrop"
+          :line-color="hanzicolor"
+          :line-width="hanziweight"
           :width="width"
           class="vueEsign"
-          ref="esign"
-          id="esign"
         />
-        <a @click="resetHuahua" class="clean-btn" v-if="TaskModel != 'ANSWER'"></a>
+        <a v-if="TaskModel != 'ANSWER'" class="clean-btn" @click="resetHuahua"></a>
       </div>
       <ul class="nav-list">
-        <li @click="play()" :class="currenHzData && currenHzData.history ? '' : 'disabled'">播放</li>
-        <li @click="handleWriteImg" :class="TaskModel == 'ANSWER' ? 'disabled' : ''">保存</li>
+        <li :class="currenHzData && currenHzData.history ? '' : 'disabled'" @click="play()">播放</li>
+        <li :class="TaskModel == 'ANSWER' ? 'disabled' : ''" @click="handleWriteImg">保存</li>
       </ul>
     </div>
   </div>
@@ -48,7 +48,7 @@ import FreeWriteQP from './FreeWriteQP.vue';
 // import ChinaDict from "./ChinaDict";
 // import Audio from "./AudioRed.vue";
 import { LearnWebSI } from '@/api/app';
-import { set } from 'js-cookie';
+
 export default {
   components: {
     Strockplayredline,
@@ -86,7 +86,7 @@ export default {
       penIndex: 0,
       hanzicolor: '',
       hanziweight: '',
-      thinpen: require('../../../../assets/common/thin-pen.png'), //细笔
+      thinpen: require('../../../../assets/common/thin-pen.png'), // 细笔
       thinpenActive: require('../../../../assets/common/thin-pen-active.png'),
       thickpen: require('../../../../assets/common/thick-pen.png'),
       thickpenActive: require('../../../../assets/common/thick-pen-active.png'),
@@ -99,7 +99,27 @@ export default {
   },
   computed: {},
   watch: {},
-  //方法集合
+  // 生命周期 - 创建完成(可以访问当前this实例)
+  created() {
+    let _this = this;
+    let color = _this.colorsList[_this.colorIndex];
+    _this.hanzicolor = color;
+    _this.hanziweight = 6;
+    if (_this.currenHzData && _this.currenHzData.strokes_image_url) {
+      _this.imgOrCans = true;
+    }
+    _this.data = JSON.parse(JSON.stringify(_this.currenHzData));
+  },
+  // 生命周期 - 挂载完成(可以访问DOM元素)
+  mounted() {},
+  beforeCreate() {}, // 生命周期 - 创建之前
+  beforeMount() {}, // 生命周期 - 挂载之前
+  beforeUpdate() {}, // 生命周期 - 更新之前
+  updated() {}, // 生命周期 - 更新之后
+  beforeDestroy() {}, // 生命周期 - 销毁之前
+  destroyed() {}, // 生命周期 - 销毁完成
+  activated() {},
+  // 方法集合
   methods: {
     play(index) {
       let _this = this;
@@ -160,7 +180,7 @@ export default {
         _this.data.strokes_image_url = '';
       }
       _this.$emit('deleteWriteRecord', _this.rowIndex, _this.colIndex, _this.currentHz);
-      //this.removeImage();
+      // this.removeImage();
     },
     removeImage() {
       let _this = this;
@@ -177,18 +197,18 @@ export default {
           _this.$message.success('删除成功');
           _this.data = {};
           _this.$emit('deleteWriteRecord', _this.rowIndex, _this.colIndex);
-          //this.resetHuahua();
+          // this.resetHuahua();
         });
       }
     },
-    //不保存到记录列表
+    // 不保存到记录列表
     handleWriteImg() {
       if (this.TaskModel == 'ANSWER') {
         return;
       }
       this.$refs.esign.generate().then((res) => {
         let Book_img = res.replace('data:image/png;base64,', '');
-        let write_img = 'data:image/png;base64,' + Book_img;
+        let write_img = `data:image/png;base64,${Book_img}`;
         let answer = {};
         answer = {
           hz: this.currentHz,
@@ -204,7 +224,7 @@ export default {
         // this.$message.warning("请先书写在保存");
       });
     },
-    //保存到记录列表
+    // 保存到记录列表
     handleWriteImg_save() {
       if (this.TaskModel == 'ANSWER') {
         return;
@@ -213,7 +233,7 @@ export default {
         .generate()
         .then((res) => {
           let Book_img = res.replace('data:image/png;base64,', '');
-          let write_img = 'data:image/png;base64,' + Book_img;
+          let write_img = `data:image/png;base64,${Book_img}`;
           let answer = {};
           answer = {
             hz: this.currentHz,
@@ -242,34 +262,14 @@ export default {
             }
             this.closeifFreeShow(obj, this.rowIndex, this.colIndex);
           });
-          //console.log(Book_img);
+          // console.log(Book_img);
           // this.textOcr(res.replace("data:image/png;base64,", ""));
         })
         .catch((err) => {
           this.$message.warning('请先书写在保存');
         });
     },
-  },
-  //生命周期 - 创建完成(可以访问当前this实例)
-  created() {
-    let _this = this;
-    let color = _this.colorsList[_this.colorIndex];
-    _this.hanzicolor = color;
-    _this.hanziweight = 6;
-    if (_this.currenHzData && _this.currenHzData.strokes_image_url) {
-      _this.imgOrCans = true;
-    }
-    _this.data = JSON.parse(JSON.stringify(_this.currenHzData));
-  },
-  //生命周期 - 挂载完成(可以访问DOM元素)
-  mounted() {},
-  beforeCreate() {}, //生命周期 - 创建之前
-  beforeMount() {}, //生命周期 - 挂载之前
-  beforeUpdate() {}, //生命周期 - 更新之前
-  updated() {}, //生命周期 - 更新之后
-  beforeDestroy() {}, //生命周期 - 销毁之前
-  destroyed() {}, //生命周期 - 销毁完成
-  activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
+  }, // 如果页面有keep-alive缓存功能,这个函数会触发
 };
 </script>
 <style lang="scss" scoped>

+ 55 - 14
src/views/create_project/createProject.vue

@@ -68,8 +68,8 @@
           placeholder="请输入作者简介"
         />
       </el-form-item>
-      <el-form-item prop="content_count_YG">
-        <span slot="label" style="line-height: 16px">预计容量 <br />(课数)</span>
+      <el-form-item prop="content_count_YG" class="label-tworow">
+        <span slot="label">预计容量 <br />(课数)</span>
         <el-input v-model="project.content_count_YG" type="number" />
       </el-form-item>
       <el-form-item label="预计字数" prop="word_count_YG">
@@ -81,36 +81,44 @@
       <el-form-item label="读者对象" prop="reader">
         <el-input v-model="project.reader" type="text" placeholder="请输入读者对象" maxlength="20" />
       </el-form-item>
-      <el-form-item label="组长列表" prop="leader_id_list">
-        <el-select v-model="project.leader_id_list" multiple placeholder="请选择组长列表">
-          <el-option label="组长1" value="leader1" />
-          <el-option label="组长2" value="leader2" />
-          <el-option label="组长3" value="leader3" />
-        </el-select>
+      <el-form-item prop="leader_id_list" class="label-tworow">
+        <span slot="label">邀请其他<br />项目组长</span>
+        <el-input v-model="leaderNames" type="text">
+          <el-button slot="append" @click="selectLeader">选择</el-button>
+        </el-input>
       </el-form-item>
-      <el-form-item label="组员列表" prop="member_id_list">
-        <el-select v-model="project.member_id_list" multiple placeholder="请选择组员列表">
-          <el-option label="组员1" value="member1" />
-          <el-option label="组员2" value="member2" />
-          <el-option label="组员3" value="member3" />
-        </el-select>
+      <el-form-item label="项目成员" prop="member_id_list">
+        <el-input v-model="memberNames" type="text">
+          <el-button slot="append" @click="selectMembers">选择</el-button>
+        </el-input>
       </el-form-item>
       <el-form-item class="submit-button">
         <el-button @click="$router.push('/create_project/start')">取消</el-button>
         <el-button type="primary" @click="createProject">确定</el-button>
       </el-form-item>
     </el-form>
+
+    <selectMembers :visible.sync="visibleMembers" :title="selectMembersTitle" @confirm="handleSelectedMembers" />
   </div>
 </template>
 
 <script>
 import { CreateProject } from '@/api/project';
 
+import selectMembers from './selectProjectMembers.vue';
+
 export default {
   name: 'CreateProject',
+  components: {
+    selectMembers,
+  },
   data() {
     return {
       labelInput: '', // 用于输入标签
+      leaderNames: '', // 组长姓名
+      memberNames: '', // 组员姓名
+      selectMembersTitle: '', // 选择组长弹窗标题
+      type: 'leader',
       project: {
         name: '',
         category: '',
@@ -131,6 +139,7 @@ export default {
       formRules: {
         name: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
       },
+      visibleMembers: false, // 控制选择成员弹窗的显示与隐藏
     };
   },
   methods: {
@@ -158,6 +167,29 @@ export default {
         }
       });
     },
+    selectLeader() {
+      this.selectMembersTitle = '选择组长';
+      this.type = 'leader';
+      this.visibleMembers = true;
+    },
+    selectMembers() {
+      this.selectMembersTitle = '选择项目成员';
+      this.type = 'member';
+      this.visibleMembers = true;
+    },
+    /**
+     * 处理选择的成员
+     * @param {Array} selectedUsers - 选中的用户列表
+     */
+    handleSelectedMembers(selectedUsers) {
+      if (this.type === 'leader') {
+        this.leaderNames = selectedUsers.map((user) => user.real_name).join(';');
+        this.project.leader_id_list = selectedUsers.map((user) => user.id);
+      } else if (this.type === 'member') {
+        this.memberNames = selectedUsers.map((user) => user.real_name).join(';');
+        this.project.member_id_list = selectedUsers.map((user) => user.id);
+      }
+    },
   },
 };
 </script>
@@ -213,6 +245,15 @@ export default {
       }
     }
 
+    .label-tworow {
+      :deep > label {
+        display: inline-flex;
+        justify-content: flex-end;
+        width: 100%;
+        line-height: 16px;
+      }
+    }
+
     .submit-button {
       width: 100%;
       padding-right: 10px;

+ 280 - 0
src/views/create_project/selectProjectMembers.vue

@@ -0,0 +1,280 @@
+<template>
+  <el-dialog :visible="visible" :title="title" class="select-members" width="900px" @close="dialogClose">
+    <div class="query-criteria">
+      <span class="criteria-label">真实姓名</span>
+      <el-input v-model="real_name" placeholder="请输入姓名" @change="getUserList" />
+      <span>所属机构</span>
+      <el-select v-model="org_id_list" placeholder="请选择机构" multiple @change="getUserList">
+        <el-option v-for="item in org_list" :key="item.id" :label="item.name" :value="item.id" />
+      </el-select>
+      <span class="query-button">
+        <el-button type="primary" @click="getUserList">查询</el-button>
+      </span>
+    </div>
+
+    <el-table ref="user" :data="user_list">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column prop="real_name" label="真实姓名" width="140" />
+      <el-table-column prop="user_name" label="用户名" width="140" />
+      <el-table-column prop="email" label="邮箱" width="200" />
+      <el-table-column prop="phone" label="电话" width="120" />
+      <el-table-column prop="org_name" label="所属机构" />
+    </el-table>
+    <PaginationPage :total="total" @getList="getUserList" />
+
+    <div class="member-container">
+      <div class="top">
+        <span class="title">已选成员</span>
+        <span class="link" @click="addSelectedUsers">添加</span>
+      </div>
+      <div class="member-list">
+        <div class="member-header">
+          <span class="header-item">序号</span>
+          <span class="header-item">真实姓名</span>
+          <span class="header-item">用户名</span>
+          <span class="header-item">邮箱</span>
+          <span class="header-item">电话</span>
+          <span class="header-item">操作</span>
+        </div>
+        <div v-for="(user, index) in selectedUsers" :key="user.id" class="member-row">
+          <span class="item">{{ index + 1 }}</span>
+          <span class="item">{{ user.real_name }}</span>
+          <span class="item">{{ user.user_name }}</span>
+          <span class="item">{{ user.email }}</span>
+          <span class="item">{{ user.phone }}</span>
+          <span class="item button">
+            <el-button type="text" @click="removeUser(index)">删除</el-button>
+          </span>
+        </div>
+      </div>
+    </div>
+
+    <div slot="footer">
+      <el-button @click="dialogClose">取消</el-button>
+      <el-button type="primary" @click="confirm">确定</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import { queryUserList } from '@/api/list';
+import { orgIndexList } from '@/api/user';
+
+import PaginationPage from '@/components/PaginationPage.vue';
+
+export default {
+  name: 'SelectProjectMembers',
+  components: {
+    PaginationPage,
+  },
+  props: {
+    visible: {
+      type: Boolean,
+      required: true,
+    },
+    title: {
+      type: String,
+      default: '选择项目成员',
+    },
+  },
+  data() {
+    return {
+      real_name: '', // 真实姓名
+      org_list: [], // 机构列表
+      org_id_list: [], // 机构id列表
+      user_list: [], // 用户列表
+      total: 0,
+      page_capacity: 10,
+      cur_page: 1,
+      selectedUsers: [], // 选中的用户
+    };
+  },
+  created() {
+    this.getOrgIndexList();
+    this.getUserList();
+  },
+  methods: {
+    getUserList(data) {
+      const params = {
+        real_name: this.real_name,
+        org_id_list: this.org_id_list,
+        page_capacity: this.page_capacity,
+        cur_page: this.cur_page,
+      };
+      queryUserList({ ...params, ...data }).then(({ user_list, total_count, cur_page }) => {
+        this.user_list = user_list;
+        this.total = total_count;
+        this.page_capacity = data?.page_capacity || this.page_capacity;
+        this.cur_page = cur_page;
+      });
+    },
+    getOrgIndexList() {
+      orgIndexList().then(({ org_list }) => {
+        this.org_list = org_list;
+      });
+    },
+    // 添加选中的用户
+    addSelectedUsers() {
+      // 获取选中的用户
+      const selected = this.$refs.user.selection;
+      if (selected.length === 0) {
+        this.$message.warning('请至少选择一名成员');
+        return;
+      }
+      // 通过 id 去重
+      const uniqueSelectedUsers = selected.filter(({ id }) => {
+        return this.selectedUsers.findIndex((user) => user.id === id) === -1;
+      });
+      // 合并选中的用户
+      this.selectedUsers = [...this.selectedUsers, ...uniqueSelectedUsers];
+      // 清空选中的用户
+      this.$refs.user.clearSelection();
+    },
+    // 删除用户
+    removeUser(index) {
+      this.selectedUsers.splice(index, 1);
+    },
+    dialogClose() {
+      this.$emit('update:visible', false);
+      this.real_name = '';
+      this.org_id_list = [];
+      this.selectedUsers = [];
+      this.$refs.user.clearSelection();
+    },
+    confirm() {
+      this.$emit('confirm', this.selectedUsers);
+      this.dialogClose();
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.select-members {
+  .query-criteria {
+    display: flex;
+    column-gap: 8px;
+    align-items: center;
+    margin-bottom: 12px;
+
+    .criteria-label {
+      white-space: nowrap;
+    }
+
+    .el-input {
+      width: 200px;
+    }
+
+    .el-select {
+      width: 360px;
+    }
+
+    :deep .el-input__inner {
+      background-color: #fff;
+      border-color: #dcdcdc;
+    }
+
+    .query-button {
+      display: flex;
+      flex: 1;
+      justify-content: flex-end;
+    }
+  }
+
+  .member-container {
+    padding: 8px 0;
+    margin-top: 12px;
+    border-top: 1px solid #dcdcdc;
+
+    .top {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 0 12px;
+    }
+
+    .member-list {
+      margin-top: 10px;
+
+      .member-header {
+        display: flex;
+        font-size: 15px;
+        font-weight: bold;
+        text-align: center;
+        background-color: $main-background-color;
+        border: $border;
+
+        .header-item {
+          padding: 8px 12px;
+
+          &:first-child {
+            width: 55px;
+          }
+
+          &:nth-child(2) {
+            width: 140px;
+          }
+
+          &:nth-child(3) {
+            width: 140px;
+          }
+
+          &:nth-child(4) {
+            width: 200px;
+          }
+
+          &:nth-child(5) {
+            width: 200px;
+          }
+
+          &:last-child {
+            flex: 1;
+          }
+        }
+      }
+
+      .member-row {
+        display: flex;
+        align-items: center;
+        text-align: center;
+
+        .item {
+          height: 40px;
+          padding: 8px 12px;
+          border-bottom: 1px solid #dcdcdc;
+
+          &:first-child {
+            width: 55px;
+          }
+
+          &:nth-child(2) {
+            width: 140px;
+          }
+
+          &:nth-child(3) {
+            width: 140px;
+          }
+
+          &:nth-child(4) {
+            width: 200px;
+          }
+
+          &:nth-child(5) {
+            width: 200px;
+          }
+
+          &:last-child {
+            flex: 1;
+          }
+
+          &.button {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 6 - 3
src/views/login/index.vue

@@ -53,7 +53,7 @@
       :close-on-click-modal="false"
       class="login-userAgree"
     >
-      <userAgreement class="userAgree-login" :changeAgreement="changeAgreement" />
+      <userAgreement class="userAgree-login" :change-agreement="changeAgreement" />
     </el-dialog>
   </div>
 </template>
@@ -62,7 +62,10 @@
 import md5 from 'md5';
 import { GetVerificationCodeImage, GetLogo } from '@/api/app';
 import { setConfig } from '@/utils/auth';
+import { getUserTypeToJump } from '@/router/guard';
+
 import UserAgreement from './userAgreement.vue';
+
 export default {
   name: 'LoginPage',
   components: { UserAgreement },
@@ -123,8 +126,8 @@ export default {
         let _form = { ...this.form, password: md5(this.form.password).toUpperCase() };
         this.$store
           .dispatch('user/login', _form)
-          .then(() => {
-            this.$router.push({ path: '/' });
+          .then((user_type) => {
+            this.$router.push({ path: getUserTypeToJump(user_type) });
           })
           .catch(() => {
             this.updateVerificationCode();

+ 264 - 0
src/views/personal_workbench/project/ProjectInfoManage.vue

@@ -0,0 +1,264 @@
+<template>
+  <div class="project-info-manage">
+    <el-form
+      ref="projectForm"
+      :inline="true"
+      :model="project"
+      :rules="formRules"
+      label-width="120px"
+      class="project-form"
+    >
+      <el-form-item label="项目名称" prop="name" class="link-item">
+        <el-input v-model="project.name" placeholder="请输入项目名称" maxlength="20" />
+        <span class="link">更改</span>
+      </el-form-item>
+      <el-form-item label="项目分类" prop="category" class="link-item">
+        <el-input v-model="project.category" maxlength="30" />
+        <span class="link">更改</span>
+      </el-form-item>
+      <el-form-item label="作品标签" prop="label_list" class="label-input link-item">
+        <div class="label-input-content">
+          <div class="label-list">
+            <el-tag
+              v-for="(tag, index) in project.label_list"
+              :key="index"
+              closable
+              @close="project.label_list.splice(index, 1)"
+            >
+              {{ tag }}
+            </el-tag>
+          </div>
+          <el-input v-model="labelInput" placeholder="请输入标签" @keyup.enter.native="labelChange" />
+          <span class="link">更改</span>
+        </div>
+      </el-form-item>
+      <el-form-item label="语种" prop="language" class="link-item">
+        <el-input v-model="project.language" type="text" placeholder="请输入语种" maxlength="20" />
+        <span class="link">更改</span>
+      </el-form-item>
+      <el-form-item label="所属课题" prop="topic" class="link-item">
+        <el-input v-model="project.topic" type="text" placeholder="请输入所属课题" />
+        <span class="link">更改</span>
+      </el-form-item>
+      <el-form-item label="出版单位" prop="publisher" class="link-item">
+        <el-input v-model="project.publisher" type="text" placeholder="请输入出版单位" maxlength="20" />
+        <span class="link">更改</span>
+      </el-form-item>
+      <el-form-item label="内容简介" prop="content_intro" class="link-item">
+        <el-input
+          v-model="project.content_intro"
+          type="textarea"
+          :autosize="{ minRows: 4 }"
+          maxlength="1500"
+          show-word-limit
+          placeholder="请输入内容简介"
+        />
+        <span class="link">更改</span>
+      </el-form-item>
+      <el-form-item label="选题背景" prop="background" class="link-item">
+        <el-input
+          v-model="project.background"
+          type="textarea"
+          :autosize="{ minRows: 4 }"
+          maxlength="1500"
+          show-word-limit
+          placeholder="请输入选题背景"
+        />
+        <span class="link">更改</span>
+      </el-form-item>
+      <el-form-item label="作者简介" prop="author_intro" class="link-item">
+        <el-input
+          v-model="project.author_intro"
+          type="textarea"
+          :autosize="{ minRows: 4 }"
+          maxlength="1500"
+          show-word-limit
+          placeholder="请输入作者简介"
+        />
+        <span class="link">更改</span>
+      </el-form-item>
+      <el-form-item prop="content_count_YG" class="content-count link-item">
+        <span slot="label">预计容量<br />(课数)</span>
+        <el-input v-model="project.content_count_YG" type="number" />
+        <span class="link">更改</span>
+      </el-form-item>
+      <el-form-item label="预计字数" prop="word_count_YG" class="link-item">
+        <el-input v-model="project.word_count_YG" type="number" />
+        <span class="link">更改</span>
+      </el-form-item>
+      <el-form-item label="计划出版时间" prop="plan_publish_date" class="link-item">
+        <el-date-picker v-model="project.plan_publish_date" type="date" placeholder="选择日期" />
+        <span class="link">更改</span>
+      </el-form-item>
+      <el-form-item label="读者对象" prop="reader" class="link-item">
+        <el-input v-model="project.reader" type="text" placeholder="请输入读者对象" maxlength="20" />
+        <span class="link">更改</span>
+      </el-form-item>
+      <el-form-item label="组长列表" prop="leader_id_list" class="link-item">
+        <el-select v-model="project.leader_id_list" multiple placeholder="请选择组长列表">
+          <el-option label="组长1" value="leader1" />
+          <el-option label="组长2" value="leader2" />
+          <el-option label="组长3" value="leader3" />
+        </el-select>
+        <span class="link">更改</span>
+      </el-form-item>
+      <el-form-item label="组员列表" prop="member_id_list" class="link-item">
+        <el-select v-model="project.member_id_list" multiple placeholder="请选择组员列表">
+          <el-option label="组员1" value="member1" />
+          <el-option label="组员2" value="member2" />
+          <el-option label="组员3" value="member3" />
+        </el-select>
+        <span class="link">更改</span>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { GetProjectInfo } from '@/api/project';
+import { GetUserList_ID } from '@/api/user';
+
+export default {
+  name: 'ProjectInfoManage',
+  data() {
+    return {
+      id: this.$route.params.id,
+      isManage: this.$route.query.isManage,
+      labelInput: '',
+      leaderNames: '', // 组长姓名
+      memberNames: '', // 组员姓名
+      project: {
+        name: '',
+        category: '',
+        label_list: [],
+        language: '',
+        topic: '',
+        publisher: '',
+        content_intro: '',
+        background: '',
+        author_intro: '',
+        content_count_YG: 100,
+        word_count_YG: 100000,
+        plan_publish_date: '', // 计划出版时间
+        reader: '', // 读者对象
+        leader_id_list: [], // 组长列表
+        member_id_list: [], // 组员列表
+      },
+      formRules: {
+        name: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
+      },
+    };
+  },
+  created() {
+    this.getProjectInfo();
+  },
+  methods: {
+    getProjectInfo() {
+      GetProjectInfo({ id: this.id }).then(({ project_info }) => {
+        this.project = project_info;
+        this.getUserList_ID(project_info.leader_id_list, 'leader');
+        this.getUserList_ID(project_info.member_id_list, 'member');
+      });
+    },
+    // 处理标签输入
+    labelChange() {
+      if (this.labelInput.trim() !== '') {
+        this.project.label_list.push(this.labelInput.trim());
+        this.labelInput = ''; // 清空输入框
+      }
+    },
+    /**
+     * 得到用户列表(指定ID)
+     * @param {Array} id_list - 用户ID列表
+     * @param {string} type - 用户类型(组长或组员)
+     */
+    getUserList_ID(id_list, type) {
+      GetUserList_ID({ id_list }).then(({ user_list }) => {
+        if (type === 'leader') {
+          this.leaderNames = user_list.map((user) => user.real_name).join(', ');
+        } else if (type === 'member') {
+          this.memberNames = user_list.map((user) => user.real_name).join(', ');
+        }
+      });
+    },
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+@use '@/styles/mixin.scss' as *;
+
+.project-info-manage {
+  @include page-base;
+
+  max-width: 1148px;
+  margin: 0 auto;
+
+  .project-form {
+    .link-item {
+      .el-input,
+      .el-select {
+        width: 380px;
+      }
+
+      .el-textarea {
+        width: 930px;
+      }
+
+      .link {
+        margin-left: 12px;
+      }
+    }
+
+    .el-input,
+    .el-select {
+      width: 420px;
+
+      :deep .el-input__inner {
+        background-color: #fff;
+        border-color: #dcdcdc;
+      }
+    }
+
+    .el-textarea {
+      width: 970px;
+
+      :deep .el-textarea__inner {
+        background-color: #fff;
+        border-color: #dcdcdc;
+      }
+    }
+
+    .label-input {
+      max-width: 540px;
+
+      :deep .label-input-content {
+        display: flex;
+        align-items: center;
+        width: 420px;
+
+        .label-list {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 4px;
+        }
+
+        .el-input {
+          flex: 1;
+          min-width: 90px;
+          margin-left: 6px;
+        }
+      }
+    }
+
+    .content-count {
+      :deep > label {
+        display: inline-flex;
+        justify-content: flex-end;
+        width: 100%;
+        line-height: 16px;
+      }
+    }
+  }
+}
+</style>

+ 6 - 3
src/views/personal_workbench/project/index.vue

@@ -12,7 +12,7 @@
 
         <el-table-column prop="operation" label="操作" fixed="right" width="300">
           <template slot-scope="{ row }">
-            <span class="link" @click="updateProject(row.id)">修改信息</span>
+            <span class="link" @click="projectInfoManage(row.id, true)">项目信息管理</span>
             <span class="link" @click="productionEditorialManage(row.id)">制作与审校管理</span>
             <span class="link danger">删除</span>
           </template>
@@ -50,10 +50,13 @@ export default {
       });
     },
     /**
-     * 修改项目
+     * 项目信息管理或查看
      * @param {string} id - 项目ID
+     * @param {boolean} isManage - 是否为管理模式
      */
-    updateProject(id) {},
+    projectInfoManage(id, isManage = false) {
+      this.$router.push({ path: `/personal_workbench/project_info/${id}`, query: { isManage } });
+    },
     /**
      * 制作与审校管理
      * @param {string} id - 项目ID