Explorar el Código

项目管理完成邀请项目成员,文档管理初步搭建

Huanyi hace 2 días
padre
commit
38d0897153

+ 14 - 1
src/api/document/folder/index.ts

@@ -1,6 +1,6 @@
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
-import { FolderVO, FolderForm, FolderQuery } from '@/api/document/folder/types';
+import { FolderVO, FolderForm, FolderQuery, ProjectVO, ProjectQuery } from '@/api/document/folder/types';
 
 /**
  * 查询文件夹管理列表
@@ -61,3 +61,16 @@ export const delFolder = (id: string | number | Array<string | number>) => {
     method: 'delete'
   });
 };
+
+/**
+ * 查询文件夹项目列表
+ * @param query
+ * @returns {*}
+ */
+export const listProject = (query?: ProjectQuery): AxiosPromise<ProjectVO[]> => {
+  return request({
+    url: '/document/folder/listProject',
+    method: 'get',
+    params: query
+  });
+};

+ 90 - 0
src/api/document/folder/types.ts

@@ -116,3 +116,93 @@ export interface FolderQuery extends PageQuery {
    */
   params?: any;
 }
+
+/**
+ * 项目VO
+ */
+export interface ProjectVO {
+  /**
+   * 序号
+   */
+  id: string | number;
+
+  /**
+   * 项目编号
+   */
+  code: string;
+
+  /**
+   * 名称
+   */
+  name: string;
+
+  /**
+   * 项目语言
+   */
+  language: string;
+
+  /**
+   * 项目类型
+   */
+  type: number;
+
+  /**
+   * 状态
+   */
+  status: string;
+
+  /**
+   * 开始时间
+   */
+  startTime: string;
+
+  /**
+   * 结束时间
+   */
+  endTime: string;
+
+  /**
+   * 创建时间
+   */
+  createTime: string;
+
+  /**
+   * 更新时间
+   */
+  updateTime: string;
+}
+
+/**
+ * 项目查询参数
+ */
+export interface ProjectQuery extends PageQuery {
+  /**
+   * 项目名
+   */
+  name?: string;
+
+  /**
+   * 项目编号
+   */
+  code?: string;
+
+  /**
+   * 项目语言
+   */
+  language?: string;
+
+  /**
+   * 项目类型
+   */
+  type?: number;
+
+  /**
+   * 项目状态
+   */
+  status?: string;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}

+ 39 - 1
src/api/project/management/index.ts

@@ -1,6 +1,6 @@
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
-import { ManagementVO, ManagementForm, ManagementQuery, ProjectMemberVO, ProjectMemberQuery } from '@/api/project/management/types';
+import { ManagementVO, ManagementForm, ManagementQuery, ProjectMemberVO, ProjectMemberQuery, UserNotInProjectQuery, UserVO, InviteMemberForm } from '@/api/project/management/types';
 
 /**
  * 查询项目管理列表
@@ -87,3 +87,41 @@ export const queryProjectMember = (query: ProjectMemberQuery): AxiosPromise<Proj
     params: query
   });
 };
+
+/**
+ * 查询未加入项目的用户列表
+ * @param query
+ * @returns {*}
+ */
+export const listOnNameNotJoinProject = (query: UserNotInProjectQuery): AxiosPromise<UserVO[]> => {
+  return request({
+    url: '/system/user/listOnNameNotJoinProject',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 邀请成员加入项目
+ * @param data
+ */
+export const inviteProjectMember = (data: InviteMemberForm) => {
+  return request({
+    url: '/project/management/queryProjectMemberInviteMember',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 移除项目成员
+ * @param projectId 项目ID
+ * @param userId 用户ID
+ */
+export const removeProjectMember = (projectId: string | number, userId: string | number) => {
+  return request({
+    url: '/project/management/queryProjectMemberRemove',
+    method: 'delete',
+    params: { projectId, userId }
+  });
+};

+ 70 - 0
src/api/project/management/types.ts

@@ -276,3 +276,73 @@ export interface ProjectMemberQuery extends PageQuery {
    */
   id: string | number;
 }
+
+/**
+ * 未加入项目的用户查询参数
+ */
+export interface UserNotInProjectQuery extends PageQuery {
+  /**
+   * 项目ID
+   */
+  id: string | number;
+
+  /**
+   * 用户姓名(模糊查询)
+   */
+  name?: string;
+}
+
+/**
+ * 用户信息VO
+ */
+export interface UserVO {
+  /**
+   * 用户ID
+   */
+  id: string | number;
+
+  /**
+   * 用户姓名
+   */
+  name: string;
+
+  /**
+   * 部门名称
+   */
+  deptName: string;
+
+  /**
+   * 手机号
+   */
+  phoneNumber: string;
+}
+
+/**
+ * 邀请用户项
+ */
+export interface InviteUserItem {
+  /**
+   * 用户ID
+   */
+  id: string | number;
+
+  /**
+   * 备注
+   */
+  note?: string;
+}
+
+/**
+ * 邀请成员表单
+ */
+export interface InviteMemberForm {
+  /**
+   * 项目ID
+   */
+  projectId: string | number;
+
+  /**
+   * 用户列表
+   */
+  users: InviteUserItem[];
+}

+ 42 - 1
src/lang/modules/project/management/en_US.ts

@@ -67,7 +67,9 @@ export default {
     createBy: 'Creator',
     createTime: 'Create Time',
     updateTime: 'Update Time',
-    operation: 'Operation'
+    operation: 'Operation',
+    actions: 'Actions',
+    enterProject: 'Enter Project'
   },
   // Project Status Enum
   status: {
@@ -165,5 +167,44 @@ export default {
       projectMemberTip: 'Project member list will be displayed here...',
       centerMemberTip: 'Center member list will be displayed here...'
     }
+  },
+  // Project Member Management
+  member: {
+    // Buttons
+    inviteMember: 'Invite Member',
+    remove: 'Remove',
+    invite: 'Invite',
+    confirm: 'Confirm',
+    cancel: 'Cancel',
+    // Table Columns
+    name: 'Name',
+    phoneNumber: 'Phone Number',
+    dept: 'Department',
+    note: 'Note',
+    time: 'Time',
+    operation: 'Operation',
+    // Dialogs
+    inviteDialogTitle: 'Invite Member',
+    confirmInviteTitle: 'Confirm Invitation',
+    removeMemberTitle: 'Remove Member',
+    // Form
+    userNickname: 'User Nickname',
+    userNicknamePlaceholder: 'Please enter user nickname to search',
+    selectedUsers: 'Selected Users',
+    notePlaceholder: 'Please enter note',
+    // Pagination
+    previousPage: 'Previous',
+    nextPage: 'Next',
+    // Messages
+    confirmInviteMessage: 'Confirm to invite the above members to join the project?',
+    confirmRemoveMessage: 'Confirm to remove {name} from {dept}?',
+    inviteSuccess: 'Members invited successfully',
+    inviteFailed: 'Failed to invite members',
+    removeSuccess: 'Member removed successfully',
+    removeFailed: 'Failed to remove member',
+    searchUserFailed: 'Failed to search users',
+    userAlreadySelected: 'This user has already been selected',
+    selectAtLeastOneUser: 'Please select at least one user',
+    noUserFound: 'No user found'
   }
 };

+ 42 - 1
src/lang/modules/project/management/zh_CN.ts

@@ -67,7 +67,9 @@ export default {
     createBy: '创建者',
     createTime: '创建时间',
     updateTime: '更新时间',
-    operation: '操作'
+    operation: '操作',
+    actions: '操作',
+    enterProject: '进入项目'
   },
   // 项目状态枚举
   status: {
@@ -165,5 +167,44 @@ export default {
       projectMemberTip: '这里将展示项目成员列表...',
       centerMemberTip: '这里将展示中心成员列表...'
     }
+  },
+  // 项目成员管理
+  member: {
+    // 按钮
+    inviteMember: '邀请成员',
+    remove: '移除',
+    invite: '邀请',
+    confirm: '确认',
+    cancel: '取消',
+    // 表格列
+    name: '姓名',
+    phoneNumber: '手机号',
+    dept: '部门',
+    note: '备注',
+    time: '时间',
+    operation: '操作',
+    // 对话框
+    inviteDialogTitle: '邀请成员',
+    confirmInviteTitle: '确认邀请',
+    removeMemberTitle: '移除成员',
+    // 表单
+    userNickname: '用户昵称',
+    userNicknamePlaceholder: '请输入用户昵称搜索',
+    selectedUsers: '已选择用户',
+    notePlaceholder: '请输入备注信息',
+    // 分页
+    previousPage: '上一页',
+    nextPage: '下一页',
+    // 提示消息
+    confirmInviteMessage: '确认邀请以上成员加入项目吗?',
+    confirmRemoveMessage: '确认移除{dept}的{name}吗?',
+    inviteSuccess: '邀请成员成功',
+    inviteFailed: '邀请成员失败',
+    removeSuccess: '移除成员成功',
+    removeFailed: '移除成员失败',
+    searchUserFailed: '搜索用户失败',
+    userAlreadySelected: '该用户已被选择',
+    selectAtLeastOneUser: '请至少选择一个用户',
+    noUserFound: '未找到用户'
   }
 };

+ 1 - 1
src/layout/components/Settings/index.vue

@@ -48,7 +48,7 @@
     <div class="drawer-item">
       <span>开启 TopNav</span>
       <span class="comp-style">
-        <el-switch v-model="settingsStore.topNav" class="drawer-switch" @change="topNavChange" />
+        <el-switch v-model="settingsStore.topNav" class="drawer-switch" disabled @change="topNavChange" />
       </span>
     </div>
 

+ 1 - 1
src/store/modules/settings.ts

@@ -19,7 +19,7 @@ export const useSettingsStore = defineStore('setting', () => {
   const theme = ref<string>(storageSetting.value.theme);
   const sideTheme = ref<string>(storageSetting.value.sideTheme);
   const showSettings = ref<boolean>(defaultSettings.showSettings);
-  const topNav = ref<boolean>(storageSetting.value.topNav);
+  const topNav = ref<boolean>(true); // 固定使用 topNav,不检测缓存
   const tagsView = ref<boolean>(storageSetting.value.tagsView);
   const tagsIcon = ref<boolean>(storageSetting.value.tagsIcon);
   const fixedHeader = ref<boolean>(storageSetting.value.fixedHeader);

+ 62 - 0
src/views/document/folder/document.vue

@@ -0,0 +1,62 @@
+<template>
+  <div>
+    <el-card shadow="never">
+      <template #header>
+        <div class="flex justify-between items-center">
+          <span class="text-lg font-bold">文档管理 - 测试页面</span>
+          <el-button type="primary" @click="handleBack">返回项目列表</el-button>
+        </div>
+      </template>
+
+      <el-empty description="这是文档管理测试页面">
+        <template #extra>
+          <el-descriptions :column="1" border>
+            <el-descriptions-item label="项目ID">{{ projectId }}</el-descriptions-item>
+            <el-descriptions-item label="页面状态">测试中</el-descriptions-item>
+            <el-descriptions-item label="说明">
+              这里将展示项目的文档管理功能
+            </el-descriptions-item>
+          </el-descriptions>
+        </template>
+      </el-empty>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+interface Props {
+  projectId?: number | string;
+}
+
+const props = defineProps<Props>();
+
+const emit = defineEmits<{
+  back: [];
+}>();
+
+const handleBack = () => {
+  emit('back');
+};
+</script>
+
+<style scoped lang="scss">
+.flex {
+  display: flex;
+}
+
+.justify-between {
+  justify-content: space-between;
+}
+
+.items-center {
+  align-items: center;
+}
+
+.text-lg {
+  font-size: 1.125rem;
+}
+
+.font-bold {
+  font-weight: 700;
+}
+</style>

+ 23 - 2
src/views/document/folder/index.vue

@@ -1,7 +1,28 @@
 <template>
-  <div>
+  <div class="folder-container">
+    <component :is="currentComponent" v-bind="componentProps" />
   </div>
 </template>
 
-<script setup>
+<script setup lang="ts">
+import { ref, shallowRef, provide } from 'vue';
+import ProjectComponent from './project.vue';
+
+const currentComponent = shallowRef(ProjectComponent);
+const componentProps = ref<any>({});
+
+// 提供组件切换方法
+const switchComponent = (component: any, props: any = {}) => {
+  currentComponent.value = component;
+  componentProps.value = props;
+};
+
+// 向子组件提供切换方法
+provide('switchComponent', switchComponent);
 </script>
+
+<style scoped>
+.folder-container {
+  height: 100%;
+}
+</style>

+ 214 - 0
src/views/document/folder/project.vue

@@ -0,0 +1,214 @@
+<template>
+  <div>
+    <!-- 项目列表视图 -->
+    <div v-if="!showDocumentView">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="100px">
+            <el-form-item :label="t('project.management.search.code')" prop="code">
+              <el-input v-model="queryParams.code" :placeholder="t('project.management.search.codePlaceholder')" clearable @keyup.enter="handleQuery" style="width: 240px;" />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.name')" prop="name">
+              <el-input v-model="queryParams.name" :placeholder="t('project.management.search.namePlaceholder')" clearable @keyup.enter="handleQuery" style="width: 240px;" />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.language')" prop="language">
+              <el-select v-model="queryParams.language" :placeholder="t('project.management.search.languagePlaceholder')" clearable style="width: 240px;">
+                <el-option v-for="dict in project_language" :key="dict.value" :label="parseI18nName(dict.label)" :value="dict.value"/>
+              </el-select>
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.type')" prop="type">
+              <el-select v-model="queryParams.type" :placeholder="t('project.management.search.typePlaceholder')" clearable style="width: 240px;">
+                <el-option v-for="dict in project_type" :key="dict.value" :label="parseI18nName(dict.label)" :value="dict.value"/>
+              </el-select>
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.startTime')">
+              <el-date-picker
+                v-model="dateRangeStartTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="daterange"
+                range-separator="-"
+                :start-placeholder="t('project.management.search.startDate')"
+                :end-placeholder="t('project.management.search.endDate')"
+                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+                style="width: 240px;"
+              />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.endTime')">
+              <el-date-picker
+                v-model="dateRangeEndTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="daterange"
+                range-separator="-"
+                :start-placeholder="t('project.management.search.startDate')"
+                :end-placeholder="t('project.management.search.endDate')"
+                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+                style="width: 240px;"
+              />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.createTime')">
+              <el-date-picker
+                v-model="dateRangeCreateTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="daterange"
+                range-separator="-"
+                :start-placeholder="t('project.management.search.startDate')"
+                :end-placeholder="t('project.management.search.endDate')"
+                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+                style="width: 240px;"
+              />
+            </el-form-item>
+            <el-form-item :label="t('project.management.search.updateTime')">
+              <el-date-picker
+                v-model="dateRangeUpdateTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="daterange"
+                range-separator="-"
+                :start-placeholder="t('project.management.search.startDate')"
+                :end-placeholder="t('project.management.search.endDate')"
+                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+                style="width: 240px;"
+              />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">{{ t('project.management.search.search') }}</el-button>
+              <el-button icon="Refresh" @click="resetQuery">{{ t('project.management.search.reset') }}</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="projectList">
+        <el-table-column :label="t('project.management.table.id')" align="center" prop="id" width="100" />
+        <el-table-column :label="t('project.management.table.code')" align="center" prop="code" width="200" />
+        <el-table-column :label="t('project.management.table.name')" align="center" prop="name" width="200" />
+        <el-table-column :label="t('project.management.table.language')" align="center" prop="language" width="150">
+          <template #default="scope">
+            <dict-tag :options="project_language" :value="scope.row.language" />
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('project.management.table.type')" align="center" prop="type" width="200">
+          <template #default="scope">
+            <dict-tag :options="project_type" :value="scope.row.type" />
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('project.management.table.status')" align="center" prop="status" width="100" />
+        <el-table-column :label="t('project.management.table.startTime')" align="center" prop="startTime" width="120">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('project.management.table.endTime')" align="center" prop="endTime" width="120">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column :label="t('project.management.table.createTime')" align="center" prop="createTime" min-width="180" />
+        <el-table-column :label="t('project.management.table.updateTime')" align="center" prop="updateTime" min-width="180" />
+        <el-table-column :label="t('project.management.table.actions')" align="center" width="150" fixed="right">
+          <template #default="scope">
+            <el-button type="primary" link @click="handleEnterProject(scope.row)">
+              {{ t('project.management.table.enterProject') }}
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+    </div>
+
+    <!-- 文档管理视图 -->
+    <document-view v-else :project-id="currentProjectId" @back="handleBackToList" />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { listProject } from '@/api/document/folder';
+import { ProjectVO, ProjectQuery } from '@/api/document/folder/types';
+import { useI18n } from 'vue-i18n';
+import { parseI18nName } from '@/utils/i18n';
+import DocumentView from './document.vue';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+const { t } = useI18n();
+const { project_type, project_language } = toRefs<any>(proxy?.useDict('project_type', 'project_language'));
+
+const projectList = ref<ProjectVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const total = ref(0);
+const showDocumentView = ref(false);
+const currentProjectId = ref<number | string>();
+const dateRangeStartTime = ref<[DateModelType, DateModelType]>(['', '']);
+const dateRangeEndTime = ref<[DateModelType, DateModelType]>(['', '']);
+const dateRangeCreateTime = ref<[DateModelType, DateModelType]>(['', '']);
+const dateRangeUpdateTime = ref<[DateModelType, DateModelType]>(['', '']);
+
+const queryFormRef = ref<ElFormInstance>();
+
+const queryParams = ref<ProjectQuery>({
+  pageNum: 1,
+  pageSize: 10,
+  code: undefined,
+  name: undefined,
+  language: undefined,
+  type: undefined,
+  status: undefined,
+  params: {}
+});
+
+/** 查询项目列表 */
+const getList = async () => {
+  loading.value = true;
+  queryParams.value.params = {};
+  proxy?.addDateRange(queryParams.value, dateRangeStartTime.value, 'StartTime');
+  proxy?.addDateRange(queryParams.value, dateRangeEndTime.value, 'EndTime');
+  proxy?.addDateRange(queryParams.value, dateRangeCreateTime.value, 'CreateTime');
+  proxy?.addDateRange(queryParams.value, dateRangeUpdateTime.value, 'UpdateTime');
+  const res = await listProject(queryParams.value);
+  projectList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  dateRangeStartTime.value = ['', ''];
+  dateRangeEndTime.value = ['', ''];
+  dateRangeCreateTime.value = ['', ''];
+  dateRangeUpdateTime.value = ['', ''];
+  queryFormRef.value?.resetFields();
+  handleQuery();
+}
+
+/** 进入项目 */
+const handleEnterProject = (row: ProjectVO) => {
+  currentProjectId.value = row.id;
+  showDocumentView.value = true;
+};
+
+/** 返回项目列表 */
+const handleBackToList = () => {
+  showDocumentView.value = false;
+  currentProjectId.value = undefined;
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 492 - 18
src/views/project/management/detail/pages/projectMember.vue

@@ -1,39 +1,218 @@
 <template>
   <div class="project-member-page">
-    <h3>{{ t('project.management.detail.menu.projectMember') }}</h3>
+    <div class="header-section">
+      <h3>{{ t('project.management.detail.menu.projectMember') }}</h3>
+      <el-button
+        v-hasPermi="['project:management:queryProjectMemberInviteMember']"
+        type="primary"
+        @click="openInviteDialog"
+      >
+        {{ t('project.management.member.inviteMember') }}
+      </el-button>
+    </div>
     <el-divider />
 
     <el-card shadow="never">
-      <el-table 
-        v-loading="loading" 
-        border 
+      <el-table
+        v-loading="loading"
+        border
         :data="memberList"
         style="width: 100%"
       >
-        <el-table-column type="index" label="序号" width="60" align="center" />
-        <el-table-column label="姓名" align="center" prop="name" />
-        <el-table-column label="手机号" align="center" prop="phoneNumber" />
-        <el-table-column label="部门" align="center" prop="dept" />
-        <el-table-column label="角色" align="center" prop="role" />
-        <el-table-column label="时间" align="center" prop="time" width="180" />
+        <el-table-column type="index" :label="t('project.management.table.id')" width="60" align="center" />
+        <el-table-column :label="t('project.management.member.name')" align="center" prop="name" width="150" />
+        <el-table-column :label="t('project.management.member.phoneNumber')" align="center" prop="phoneNumber" width="150" />
+        <el-table-column :label="t('project.management.member.dept')" align="center" prop="dept" width="200" />
+        <el-table-column :label="t('project.management.member.note')" align="center" prop="note" show-overflow-tooltip />
+        <el-table-column :label="t('project.management.member.time')" align="center" prop="time" width="180" />
+        <el-table-column :label="t('project.management.member.operation')" align="center" width="120" fixed="right">
+          <template #default="scope">
+            <el-button
+              v-hasPermi="['project:management:queryProjectMemberRemove']"
+              type="danger"
+              size="small"
+              link
+              @click="handleRemoveMember(scope.row)"
+            >
+              {{ t('project.management.member.remove') }}
+            </el-button>
+          </template>
+        </el-table-column>
       </el-table>
 
-      <pagination 
-        v-show="total > 0" 
-        :total="total" 
-        v-model:page="queryParams.pageNum" 
-        v-model:limit="queryParams.pageSize" 
-        @pagination="getList" 
+      <pagination
+        v-show="total > 0"
+        :total="total"
+        v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
       />
     </el-card>
+
+    <!-- 邀请成员对话框 -->
+    <el-dialog
+      v-model="inviteDialogVisible"
+      :title="t('project.management.member.inviteDialogTitle')"
+      width="700px"
+      @close="handleInviteDialogClose"
+    >
+      <el-form :model="inviteForm" label-width="130px">
+        <el-form-item :label="t('project.management.member.userNickname')">
+          <el-select
+            v-model="searchKeyword"
+            filterable
+            remote
+            reserve-keyword
+            :placeholder="t('project.management.member.userNicknamePlaceholder')"
+            :remote-method="searchUsers"
+            :loading="searchLoading"
+            @change="handleSelectUser"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="user in userOptions"
+              :key="user.id"
+              :label="`${user.name} - ${user.deptName} - ${user.phoneNumber}`"
+              :value="user.id"
+            />
+            <template #footer>
+              <div v-if="userTotal > userQueryParams.pageSize" class="select-pagination">
+                <el-button
+                  text
+                  :disabled="userQueryParams.pageNum === 1"
+                  @click="loadPrevPage"
+                >
+                  {{ t('project.management.member.previousPage') }}
+                </el-button>
+                <span>{{ userQueryParams.pageNum }} / {{ Math.ceil(userTotal / userQueryParams.pageSize) }}</span>
+                <el-button
+                  text
+                  :disabled="userQueryParams.pageNum * userQueryParams.pageSize >= userTotal"
+                  @click="loadNextPage"
+                >
+                  {{ t('project.management.member.nextPage') }}
+                </el-button>
+              </div>
+            </template>
+          </el-select>
+        </el-form-item>
+
+        <!-- 已选择的用户列表 -->
+        <el-form-item :label="t('project.management.member.selectedUsers')" v-if="selectedUsers.length > 0">
+          <div class="selected-users-list">
+            <div
+              v-for="(user, index) in selectedUsers"
+              :key="user.id"
+              class="user-card"
+            >
+              <div class="user-info-left">
+                <div class="info-value">{{ user.name }} / {{ user.deptName }}</div>
+                <div class="info-value">{{ user.phoneNumber }}</div>
+              </div>
+              <div class="user-note-right">
+                <el-input
+                  v-model="user.note"
+                  type="textarea"
+                  :rows="2"
+                  :placeholder="t('project.management.member.notePlaceholder')"
+                  maxlength="50"
+                  show-word-limit
+                />
+              </div>
+              <el-button
+                type="danger"
+                size="small"
+                text
+                @click="removeSelectedUser(index)"
+                class="remove-btn"
+              >
+                {{ t('project.management.member.remove') }}
+              </el-button>
+            </div>
+          </div>
+        </el-form-item>
+      </el-form>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="inviteDialogVisible = false">{{ t('project.management.member.cancel') }}</el-button>
+          <el-button
+            type="primary"
+            @click="handleInvite"
+            :disabled="selectedUsers.length === 0"
+          >
+            {{ t('project.management.member.invite') }}
+          </el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 确认邀请对话框 -->
+    <el-dialog
+      v-model="confirmDialogVisible"
+      :title="t('project.management.member.confirmInviteTitle')"
+      width="500px"
+    >
+      <div class="confirm-content">
+        <p style="margin-bottom: 15px; font-weight: bold;">{{ t('project.management.member.confirmInviteMessage') }}</p>
+        <div class="confirm-user-list">
+          <div
+            v-for="user in selectedUsers"
+            :key="user.id"
+            class="confirm-user-item"
+          >
+            <span>{{ user.name }} - {{ user.deptName }}</span>
+            <span v-if="user.note" class="user-note">({{ t('project.management.member.note') }}: {{ user.note }})</span>
+          </div>
+        </div>
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="confirmDialogVisible = false">{{ t('project.management.member.cancel') }}</el-button>
+          <el-button
+            type="primary"
+            @click="confirmInvite"
+            :loading="inviteLoading"
+          >
+            {{ t('project.management.member.confirm') }}
+          </el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- 移除成员确认对话框 -->
+    <el-dialog
+      v-model="removeDialogVisible"
+      :title="t('project.management.member.removeMemberTitle')"
+      width="400px"
+    >
+      <div style="padding: 20px 0; text-align: center; font-size: 14px;">
+        {{ t('project.management.member.confirmRemoveMessage', { dept: removingMember?.dept, name: removingMember?.name }) }}
+      </div>
+
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="removeDialogVisible = false">{{ t('project.management.member.cancel') }}</el-button>
+          <el-button
+            type="danger"
+            @click="confirmRemoveMember"
+            :loading="removeLoading"
+          >
+            {{ t('project.management.member.confirm') }}
+          </el-button>
+        </span>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
 import { inject, ref, onMounted } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { queryProjectMember } from '@/api/project/management';
-import { ProjectMemberVO, ProjectMemberQuery } from '@/api/project/management/types';
+import { ElMessage } from 'element-plus';
+import { queryProjectMember, listOnNameNotJoinProject, inviteProjectMember, removeProjectMember } from '@/api/project/management';
+import { ProjectMemberVO, ProjectMemberQuery, UserVO, UserNotInProjectQuery, InviteMemberForm } from '@/api/project/management/types';
 
 const { t } = useI18n();
 
@@ -67,6 +246,204 @@ const getList = async () => {
   }
 };
 
+// ========== 邀请成员相关 ==========
+// 对话框状态
+const inviteDialogVisible = ref(false);
+const confirmDialogVisible = ref(false);
+
+// 搜索相关
+const searchKeyword = ref('');
+const searchLoading = ref(false);
+const userOptions = ref<UserVO[]>([]);
+const userTotal = ref(0);
+
+// 用户查询参数
+const userQueryParams = ref<UserNotInProjectQuery>({
+  pageNum: 1,
+  pageSize: 5,
+  id: projectId?.value || 0,
+  name: ''
+});
+
+// 已选择的用户列表(包含用户信息和备注)
+interface SelectedUser extends UserVO {
+  note?: string;
+}
+const selectedUsers = ref<SelectedUser[]>([]);
+
+// 邀请表单
+const inviteForm = ref({});
+
+// 邀请加载状态
+const inviteLoading = ref(false);
+
+// ========== 移除成员相关 ==========
+// 移除对话框状态
+const removeDialogVisible = ref(false);
+const removeLoading = ref(false);
+const removingMember = ref<ProjectMemberVO | null>(null);
+
+/** 打开邀请对话框 */
+const openInviteDialog = () => {
+  inviteDialogVisible.value = true;
+};
+
+/** 搜索用户 */
+const searchUsers = async (query: string) => {
+  if (!query || query.trim() === '') {
+    userOptions.value = [];
+    userTotal.value = 0;
+    return;
+  }
+
+  searchLoading.value = true;
+  userQueryParams.value.name = query;
+  userQueryParams.value.pageNum = 1;
+  userQueryParams.value.id = projectId?.value || 0;
+
+  try {
+    const res = await listOnNameNotJoinProject(userQueryParams.value);
+    userOptions.value = res.rows || [];
+    userTotal.value = res.total || 0;
+  } catch (error) {
+    console.error('Failed to search users:', error);
+    ElMessage.error(t('project.management.member.searchUserFailed'));
+  } finally {
+    searchLoading.value = false;
+  }
+};
+
+/** 加载上一页 */
+const loadPrevPage = async () => {
+  if (userQueryParams.value.pageNum > 1) {
+    userQueryParams.value.pageNum--;
+    await searchUsers(userQueryParams.value.name || '');
+  }
+};
+
+/** 加载下一页 */
+const loadNextPage = async () => {
+  const maxPage = Math.ceil(userTotal.value / userQueryParams.value.pageSize);
+  if (userQueryParams.value.pageNum < maxPage) {
+    userQueryParams.value.pageNum++;
+    await searchUsers(userQueryParams.value.name || '');
+  }
+};
+
+/** 选择用户 */
+const handleSelectUser = (userId: string | number) => {
+  const user = userOptions.value.find(u => u.id === userId);
+  if (user) {
+    // 检查是否已经选择过该用户
+    const isAlreadySelected = selectedUsers.value.some(u => u.id === userId);
+    if (!isAlreadySelected) {
+      selectedUsers.value.push({
+        ...user,
+        note: ''
+      });
+    } else {
+      ElMessage.warning(t('project.management.member.userAlreadySelected'));
+    }
+  }
+  // 清空搜索框
+  searchKeyword.value = '';
+};
+
+/** 移除已选择的用户 */
+const removeSelectedUser = (index: number) => {
+  selectedUsers.value.splice(index, 1);
+};
+
+/** 点击邀请按钮 */
+const handleInvite = () => {
+  if (selectedUsers.value.length === 0) {
+    ElMessage.warning(t('project.management.member.selectAtLeastOneUser'));
+    return;
+  }
+  // 打开确认对话框
+  confirmDialogVisible.value = true;
+};
+
+/** 确认邀请 */
+const confirmInvite = async () => {
+  inviteLoading.value = true;
+  try {
+    const inviteData: InviteMemberForm = {
+      projectId: projectId?.value || 0,
+      users: selectedUsers.value.map(user => ({
+        id: user.id,
+        note: user.note || ''
+      }))
+    };
+
+    await inviteProjectMember(inviteData);
+    ElMessage.success(t('project.management.member.inviteSuccess'));
+
+    // 关闭所有对话框
+    confirmDialogVisible.value = false;
+    inviteDialogVisible.value = false;
+
+    // 重新初始化
+    resetInviteForm();
+
+    // 刷新成员列表
+    await getList();
+  } catch (error) {
+    console.error('Failed to invite members:', error);
+    ElMessage.error(t('project.management.member.inviteFailed'));
+  } finally {
+    inviteLoading.value = false;
+  }
+};
+
+/** 关闭邀请对话框 */
+const handleInviteDialogClose = () => {
+  resetInviteForm();
+};
+
+/** 重置邀请表单 */
+const resetInviteForm = () => {
+  searchKeyword.value = '';
+  selectedUsers.value = [];
+  userOptions.value = [];
+  userTotal.value = 0;
+  userQueryParams.value = {
+    pageNum: 1,
+    pageSize: 5,
+    id: projectId?.value || 0,
+    name: ''
+  };
+};
+
+/** 点击移除成员 */
+const handleRemoveMember = (member: ProjectMemberVO) => {
+  removingMember.value = member;
+  removeDialogVisible.value = true;
+};
+
+/** 确认移除成员 */
+const confirmRemoveMember = async () => {
+  if (!removingMember.value) return;
+
+  removeLoading.value = true;
+  try {
+    await removeProjectMember(projectId?.value || 0, removingMember.value.id);
+    ElMessage.success(t('project.management.member.removeSuccess'));
+    
+    // 关闭对话框
+    removeDialogVisible.value = false;
+    removingMember.value = null;
+    
+    // 刷新成员列表
+    await getList();
+  } catch (error) {
+    console.error('Failed to remove member:', error);
+    ElMessage.error(t('project.management.member.removeFailed'));
+  } finally {
+    removeLoading.value = false;
+  }
+};
+
 // 组件挂载时加载数据
 onMounted(() => {
   getList();
@@ -78,6 +455,13 @@ onMounted(() => {
   height: 100%;
 }
 
+.header-section {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
 .info-section {
   padding: 16px 0;
 }
@@ -106,4 +490,94 @@ h3 {
   font-style: italic;
   margin-top: 16px;
 }
+
+/* 邀请成员对话框样式 */
+.selected-users-list {
+  width: 100%;
+  max-height: 400px;
+  overflow-y: auto;
+}
+
+.user-card {
+  display: flex;
+  align-items: flex-start;
+  gap: 8px;
+  margin-bottom: 8px;
+  padding: 8px 12px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  background-color: #fff;
+}
+
+.user-info-left {
+  flex-shrink: 0;
+  width: 120px;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  padding-top: 6px;
+}
+
+.info-value {
+  font-size: 13px;
+  color: #303133;
+  line-height: 1.3;
+}
+
+.user-note-right {
+  flex: 3;
+  min-width: 0;
+}
+
+.remove-btn {
+  flex-shrink: 0;
+  margin-left: 4px;
+  align-self: center;
+}
+
+/* 确认对话框样式 */
+.confirm-content {
+  padding: 10px 0;
+}
+
+.confirm-user-list {
+  max-height: 300px;
+  overflow-y: auto;
+  border: 1px solid #e4e7ed;
+  border-radius: 4px;
+  padding: 12px;
+  background-color: #f5f7fa;
+}
+
+.confirm-user-item {
+  padding: 8px 0;
+  border-bottom: 1px solid #e4e7ed;
+  font-size: 14px;
+  color: #303133;
+}
+
+.confirm-user-item:last-child {
+  border-bottom: none;
+}
+
+.user-note {
+  color: #909399;
+  font-size: 12px;
+  margin-left: 8px;
+}
+
+/* 下拉选择分页样式 */
+.select-pagination {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 12px;
+  border-top: 1px solid #e4e7ed;
+  background-color: #f5f7fa;
+}
+
+.select-pagination span {
+  font-size: 12px;
+  color: #606266;
+}
 </style>