chore: join project

This commit is contained in:
xiaojunnuo
2026-02-28 00:49:02 +08:00
parent e17f381b1f
commit 6163c3f08e
14 changed files with 379 additions and 146 deletions

View File

@@ -19,30 +19,30 @@ export function parse(jsonString = "{}", defaultValue = {}) {
/** /**
* @description 接口请求返回 * @description 接口请求返回
* @param {Any} data 返回值 * @param {Any} data 返回值
* @param {String} msg 状态信息 * @param {String} message 状态信息
* @param {Number} code 状态码 * @param {Number} code 状态码
*/ */
export function response(data = {}, msg = "", code = 0) { export function response(data = {}, message = "", code = 0) {
return [200, { code, msg, data }]; return [200, { code, message, data }];
} }
/** /**
* @description 接口请求返回 正确返回 * @description 接口请求返回 正确返回
* @param {Any} data 返回值 * @param {Any} data 返回值
* @param {String} msg 状态信息 * @param {String} message 状态信息
*/ */
export function responseSuccess(data = {}, msg = "成功") { export function responseSuccess(data = {}, message = "成功") {
return response(data, msg); return response(data, message);
} }
/** /**
* @description 接口请求返回 错误返回 * @description 接口请求返回 错误返回
* @param {Any} data 返回值 * @param {Any} data 返回值
* @param {String} msg 状态信息 * @param {String} message 状态信息
* @param {Number} code 状态码 * @param {Number} code 状态码
*/ */
export function responseError(data = {}, msg = "请求失败", code = 500) { export function responseError(data = {}, message = "请求失败", code = 500) {
return response(data, msg, code); return response(data, message, code);
} }
/** /**

View File

@@ -218,6 +218,7 @@ export default {
projectUserManager: "Project User Management", projectUserManager: "Project User Management",
myProjectManager: "My Projects", myProjectManager: "My Projects",
myProjectDetail: "Project Detail", myProjectDetail: "Project Detail",
projectJoin: "Join Project",
}, },
certificateRepo: { certificateRepo: {
title: "Certificate Repository", title: "Certificate Repository",
@@ -830,6 +831,9 @@ export default {
fetchFailed: "Failed to fetch project list", fetchFailed: "Failed to fetch project list",
applySuccess: "Application successful, waiting for admin approval", applySuccess: "Application successful, waiting for admin approval",
applyFailed: "Application failed, please try again later", applyFailed: "Application failed, please try again later",
leave: "Leave Project",
leaveSuccess: "Leave project successful",
leaveFailed: "Leave project failed, please try again later",
}, },
addonSelector: { addonSelector: {
select: "Select", select: "Select",

View File

@@ -224,6 +224,7 @@ export default {
enterpriseSetting: "企业设置", enterpriseSetting: "企业设置",
myProjectManager: "我的项目", myProjectManager: "我的项目",
myProjectDetail: "项目详情", myProjectDetail: "项目详情",
projectJoin: "加入项目",
}, },
certificateRepo: { certificateRepo: {
title: "证书仓库", title: "证书仓库",
@@ -839,12 +840,15 @@ export default {
project: { project: {
noProjectJoined: "您还没有加入任何项目", noProjectJoined: "您还没有加入任何项目",
applyToJoin: "请申请加入项目以开始使用", applyToJoin: "请申请加入项目以开始使用",
systemProjects: "系统项目列表", projectList: "项目列表",
createdAt: "创建时间", createdAt: "创建时间",
applyJoin: "申请加入", applyJoin: "申请加入",
noSystemProjects: "暂无系统项目", noProjects: "暂无项目",
fetchFailed: "获取项目列表失败", fetchFailed: "获取项目列表失败",
applySuccess: "申请成功,等待管理员审核", applySuccess: "申请成功,等待管理员审核",
applyFailed: "申请失败,请稍后重试", applyFailed: "申请失败,请稍后重试",
leave: "退出项目",
leaveSuccess: "退出项目成功",
leaveFailed: "退出项目失败,请稍后重试",
}, },
}; };

View File

@@ -10,7 +10,7 @@ import { usePermissionStore } from "/@/plugin/permission/store.permission";
import util from "/@/plugin/permission/util.permission"; import util from "/@/plugin/permission/util.permission";
import { useUserStore } from "/@/store/user"; import { useUserStore } from "/@/store/user";
import { useProjectStore } from "../store/project"; import { useProjectStore } from "../store/project";
export const PROJECT_BLANK_PATH = "/certd/project/blank"; export const PROJECT_JOIN_PATH = "/certd/project/join";
function buildAccessedMenus(menus: any) { function buildAccessedMenus(menus: any) {
if (menus == null) { if (menus == null) {
return; return;
@@ -131,10 +131,10 @@ function setupAccessGuard(router: Router) {
if (projectStore.isEnterprise) { if (projectStore.isEnterprise) {
//加载我的项目 //加载我的项目
await projectStore.init(); await projectStore.init();
if (!projectStore.currentProject && to.path !== PROJECT_BLANK_PATH) { if (!projectStore.currentProject && to.path !== PROJECT_JOIN_PATH) {
//没有项目 //没有项目
return { return {
path: PROJECT_BLANK_PATH, path: PROJECT_JOIN_PATH,
replace: true, replace: true,
}; };
} }

View File

@@ -43,10 +43,10 @@ export const certdResources = [
}, },
}, },
{ {
title: "certd.sysResources.myProjectBlank", title: "certd.sysResources.projectJoin",
name: "MyProjectBlank", name: "ProjectJoin",
path: "/certd/project/blank", path: "/certd/project/join",
component: "/certd/project/blank.vue", component: "/certd/project/join.vue",
meta: { meta: {
isMenu: false, isMenu: false,
show: true, show: true,

View File

@@ -24,6 +24,29 @@ const projectPermissionDict = dict({
], ],
}); });
const projectMemberStatusDict = dict({
data: [
{
value: "pending",
label: "待审核",
color: "orange",
icon: "material-symbols:hourglass-top",
},
{
value: "approved",
label: "已加入",
color: "green",
icon: "material-symbols:done-all",
},
{
value: "rejected",
label: "已拒绝",
color: "red",
icon: "material-symbols:close",
},
],
});
const myProjectDict = dict({ const myProjectDict = dict({
url: "/enterprise/project/list", url: "/enterprise/project/list",
getData: async () => { getData: async () => {
@@ -56,5 +79,6 @@ export function useDicts() {
projectPermissionDict, projectPermissionDict,
myProjectDict, myProjectDict,
userDict, userDict,
projectMemberStatusDict,
}; };
} }

View File

@@ -1,122 +0,0 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">
{{ t("certd.sysResources.myProjectManager") }}
</div>
</template>
<div class="blank-container">
<div class="project-list-container">
<h3 class="text-lg font-medium mb-4">{{ t("certd.project.systemProjects") }}</h3>
<a-card v-for="project in projects" :key="project.id" :bordered="false" class="project-card">
<div class="project-card-content">
<div class="project-info">
<h4 class="text-md font-medium">{{ project.name }}</h4>
<p class="text-gray-500 text-sm">{{ t("certd.project.createdAt") }}: {{ formatDate(project.createTime) }}</p>
</div>
<a-button type="primary" @click="applyToJoin(project.id)">{{ t("certd.project.applyJoin") }}</a-button>
</div>
</a-card>
<div v-if="projects.length === 0" class="no-projects"></div>
</div>
</div>
</fs-page>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { useI18n } from "/src/locales";
import { message } from "ant-design-vue";
import { request } from "/src/api/service";
const { t } = useI18n();
const projects = ref<any[]>([]);
const getSystemProjects = async () => {
try {
// 假设这里调用获取系统项目列表的API
const response = await request({
url: "/enterprise/project/list",
method: "post",
data: { type: "system" }, // 假设type=system表示系统项目
});
projects.value = response || [];
} catch (error) {
message.error(t("certd.project.fetchFailed"));
console.error("获取项目列表失败:", error);
}
};
const applyToJoin = async (projectId: number) => {
try {
// 假设这里调用申请加入项目的API
await request({
url: "/enterprise/project/apply",
method: "post",
data: { projectId },
});
message.success(t("certd.project.applySuccess"));
// 申请成功后可以刷新页面或跳转到项目列表
} catch (error) {
message.error(t("certd.project.applyFailed"));
console.error("申请加入项目失败:", error);
}
};
const formatDate = (dateString: string) => {
if (!dateString) return "";
const date = new Date(dateString);
return date.toLocaleString();
};
onMounted(() => {
getSystemProjects();
});
</script>
<style lang="less">
.blank-container {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.empty-state {
margin-bottom: 48px;
text-align: center;
}
.empty-description {
margin-top: 16px;
}
.project-list-container {
margin-top: 32px;
}
.project-card {
margin-bottom: 16px;
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
.project-card-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.project-info {
flex: 1;
}
.no-projects {
margin-top: 24px;
padding: 48px 0;
text-align: center;
}
</style>

View File

@@ -0,0 +1,153 @@
<template>
<fs-page class="page-project-join">
<template #header>
<div class="title">
{{ t("certd.sysResources.projectJoin") }}
<span v-if="projectStore.projects.length === 0" class="sub">{{ t("certd.project.noProjectJoined") }}</span>
</div>
</template>
<div class="project-container">
<h3 class="text-lg font-medium mb-4">{{ t("certd.project.projectList") }}</h3>
<div class="flex flex-wrap gap-4">
<div v-for="project in projects" :key="project.id" class="w-full md:w-1/4">
<a-card :bordered="true" class="project-card">
<div class="project-card-content">
<div class="project-info">
<h3 class="text-md font-bold title">{{ project.name }}</h3>
<p class="text-gray-500 text-sm">{{ formatDate(project.createTime) }}</p>
</div>
<div class="flex justify-between items-center">
<div v-if="project.status">
<fs-values-format :model-value="project.status" :dict="projectMemberStatusDict"></fs-values-format>
</div>
<div v-if="project.permission"><fs-values-format :model-value="project.permission" :dict="projectPermissionDict"></fs-values-format></div>
</div>
</div>
<template #actions>
<span v-if="!project.status || project.status === 'rejected'" class="flex-inline items-center" :title="t('certd.project.applyJoin')" @click="applyToJoin(project.id)">
<fs-icon class="fs-18 mr-2" icon="mdi:checkbox-marked-circle-outline"></fs-icon>
{{ t("certd.project.applyJoin") }}
</span>
<span v-if="project.status === 'pending' || project.status === 'approved'" class="flex-inline items-center" :title="t('certd.project.leave')" @click="leaveProject(project.id)">
<fs-icon class="fs-18 mr-2" icon="mdi:arrow-right-thin-circle-outline"></fs-icon>
{{ t("certd.project.leave") }}
</span>
</template>
</a-card>
</div>
</div>
</div>
</fs-page>
</template>
<script lang="ts" setup>
import { ref, onMounted } from "vue";
import { useI18n } from "/src/locales";
import { message, Modal } from "ant-design-vue";
import { request } from "/src/api/service";
import { useProjectStore } from "/@/store/project";
import dayjs from "dayjs";
import { useDicts } from "../dicts";
import { modalProps } from "ant-design-vue/es/modal/Modal";
defineOptions({
name: "ProjectJoin",
});
const { t } = useI18n();
const { projectMemberStatusDict, projectPermissionDict } = useDicts();
const projects = ref<any[]>([]);
const projectStore = useProjectStore();
const getSystemProjects = async () => {
try {
// 假设这里调用获取系统项目列表的API
const response = await request({
url: "/enterprise/project/all",
method: "post",
});
projects.value = response || [];
} catch (error) {
message.error(t("certd.project.fetchFailed"));
console.error("获取项目列表失败:", error);
}
};
const applyToJoin = async (projectId: number) => {
// 假设这里调用申请加入项目的API
Modal.confirm({
title: t("certd.project.applyJoin"),
content: t("certd.project.applyJoinConfirm"),
onOk: async () => {
await request({
url: "/enterprise/project/applyJoin",
method: "post",
data: { projectId },
});
message.success(t("certd.project.applySuccess"));
await getSystemProjects();
// 申请成功后可以刷新页面或跳转到项目列表
},
});
};
const formatDate = (dateString: string) => {
if (!dateString) {
return "";
}
return dayjs(dateString).format("YYYY-MM-DD HH:mm:ss");
};
onMounted(() => {
getSystemProjects();
});
async function leaveProject(projectId: number) {
// 假设这里调用退出项目的API
Modal.confirm({
title: t("certd.project.leave"),
content: t("certd.project.leaveConfirm"),
onOk: async () => {
await request({
url: "/enterprise/project/leave",
method: "post",
data: { projectId },
});
message.success(t("certd.project.leaveSuccess"));
// 退出成功后可以刷新页面或跳转到项目列表
await getSystemProjects();
},
});
}
</script>
<style lang="less">
.page-project-join {
.project-container {
padding: 24px;
margin: 0 auto;
.project-card {
margin-bottom: 16px;
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.project-card-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.title {
font-size: 16px;
font-weight: bold;
margin-bottom: 8px;
}
}
}
}
</style>

View File

@@ -82,6 +82,7 @@ CREATE TABLE "cd_project_member"
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP) "update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
); );
ALTER TABLE cd_project_member ADD COLUMN status varchar(128);
CREATE INDEX "index_project_member_user_id" ON "cd_project_member" ("user_id"); CREATE INDEX "index_project_member_user_id" ON "cd_project_member" ("user_id");
CREATE INDEX "index_project_member_project_id" ON "cd_project_member" ("project_id"); CREATE INDEX "index_project_member_project_id" ON "cd_project_member" ("project_id");

View File

@@ -2,6 +2,7 @@ import { BaseController, Constants } from '@certd/lib-server';
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core'; import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { AuthService } from '../../../modules/sys/authority/service/auth-service.js'; import { AuthService } from '../../../modules/sys/authority/service/auth-service.js';
import { ProjectService } from '../../../modules/sys/enterprise/service/project-service.js'; import { ProjectService } from '../../../modules/sys/enterprise/service/project-service.js';
import { ProjectMemberService } from '../../../modules/sys/enterprise/service/project-member-service.js';
/** /**
*/ */
@@ -10,6 +11,10 @@ import { ProjectService } from '../../../modules/sys/enterprise/service/project-
export class UserProjectController extends BaseController { export class UserProjectController extends BaseController {
@Inject() @Inject()
service: ProjectService; service: ProjectService;
@Inject()
projectMemberService: ProjectMemberService;
@Inject() @Inject()
authService: AuthService; authService: AuthService;
@@ -17,6 +22,11 @@ export class UserProjectController extends BaseController {
return this.service; return this.service;
} }
/**
* 我的项目
* @param body
* @returns
*/
@Post('/list', { summary: Constants.per.authOnly }) @Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) { async list(@Body(ALL) body: any) {
const userId= this.getUserId(); const userId= this.getUserId();
@@ -24,4 +34,74 @@ export class UserProjectController extends BaseController {
return this.ok(res); return this.ok(res);
} }
/**
*
* @param body 所有项目
* @returns
*/
@Post('/all', { summary: Constants.per.authOnly })
async all(@Body(ALL) body: any) {
const userId= this.getUserId();
const res = await this.service.getAllWithStatus(userId);
return this.ok(res);
}
@Post('/applyJoin', { summary: Constants.per.authOnly })
async applyJoin(@Body(ALL) body: any) {
const userId= this.getUserId();
const res = await this.service.applyJoin({ userId, projectId: body.projectId });
return this.ok(res);
}
@Post('/updateMember', { summary: Constants.per.authOnly })
async updateMember(@Body(ALL) body: any) {
const {projectId} = await this.getProjectUserIdAdmin();
const {status,permission,userId} = body;
const member = await this.projectMemberService.findOne({
where: {
projectId,
userId,
},
});
if (!member) {
throw new Error('成员不存在');
}
const res = await this.projectMemberService.update({
id: member.id,
status,
permission,
});
return this.ok(res);
}
@Post('/approveJoin', { summary: Constants.per.authOnly })
async approveJoin(@Body(ALL) body: any) {
const {projectId} = await this.getProjectUserIdAdmin();
const {status,permission,userId} = body;
const res = await this.service.approveJoin({ userId, projectId: projectId,status,permission });
return this.ok(res);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Body(ALL) body: any) {
const {projectId} = await this.getProjectUserIdAdmin();
await this.projectMemberService.deleteWhere({
projectId,
userId: this.getUserId(),
});
return this.ok();
}
@Post('/leave', { summary: Constants.per.authOnly })
async leave(@Body(ALL) body: any) {
const {projectId} = body
const userId = this.getUserId();
await this.projectMemberService.deleteWhere({
projectId,
userId,
});
return this.ok();
}
} }

View File

@@ -16,6 +16,9 @@ export class ProjectMemberEntity {
@Column({ name: 'permission', comment: '权限' }) @Column({ name: 'permission', comment: '权限' })
permission: string; // read / write / admin permission: string; // read / write / admin
@Column({ name: 'status', comment: '申请状态' })
status: string; // pending / approved / rejected
@Column({ @Column({
name: 'create_time', name: 'create_time',
comment: '创建时间', comment: '创建时间',

View File

@@ -34,4 +34,10 @@ export class ProjectEntity {
// user permission read write admin // user permission read write admin
permission:string permission:string
} }
export type ProjectMemberItem = {
memberId: number;
status: string;
} & ProjectEntity

View File

@@ -18,7 +18,7 @@ export class ProjectMemberService extends BaseService<ProjectMemberEntity> {
return this.repository; return this.repository;
} }
async add(bean: ProjectMemberEntity) { async add(bean: Partial<ProjectMemberEntity>) {
const {projectId, userId} = bean; const {projectId, userId} = bean;
if (!projectId) { if (!projectId) {
throw new Error('项目ID不能为空'); throw new Error('项目ID不能为空');
@@ -38,10 +38,11 @@ export class ProjectMemberService extends BaseService<ProjectMemberEntity> {
return await super.add(bean) return await super.add(bean)
} }
async getByUserId(userId: number) { async getByUserId(userId: number,status?:string) {
return await this.repository.find({ return await this.repository.find({
where: { where: {
userId, userId,
status,
}, },
}); });
} }

View File

@@ -3,7 +3,7 @@ import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm'; import { InjectEntityModel } from '@midwayjs/typeorm';
import { LRUCache } from 'lru-cache'; import { LRUCache } from 'lru-cache';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { ProjectEntity } from '../entity/project.js'; import { ProjectEntity, ProjectMemberItem } from '../entity/project.js';
import { ProjectMemberService } from './project-member-service.js'; import { ProjectMemberService } from './project-member-service.js';
const projectCache = new LRUCache<string, any>({ const projectCache = new LRUCache<string, any>({
@@ -65,7 +65,7 @@ export class ProjectService extends BaseService<ProjectEntity> {
async getUserProjects(userId: number) { async getUserProjects(userId: number) {
const memberList = await this.projectMemberService.getByUserId(userId); const memberList = await this.projectMemberService.getByUserId(userId,'approved');
const projectIds = memberList.map(item => item.projectId); const projectIds = memberList.map(item => item.projectId);
const projectList = await this.repository.createQueryBuilder('project') const projectList = await this.repository.createQueryBuilder('project')
.where(' project.disabled = false') .where(' project.disabled = false')
@@ -89,6 +89,39 @@ export class ProjectService extends BaseService<ProjectEntity> {
return projectList return projectList
} }
async getAllWithStatus(userId: number) : Promise<ProjectMemberItem[]> {
let projectList:any = await this.find({
where: {
disabled: false,
userId: 0,
},
})
const projectMemberItemList:ProjectMemberItem[] = projectList
const memberList = await this.projectMemberService.getByUserId(userId);
const memberMap = memberList.reduce((prev, cur) => {
prev[cur.projectId] = cur as any;
return prev;
}, {} as Record<number, ProjectMemberItem>);
projectMemberItemList.forEach(item => {
if (item.adminId === userId) {
item.permission = 'admin';
item.status = 'approved';
item.memberId = userId
} else {
const memberItem :any = memberMap[item.id]
if (memberItem) {
item.permission = memberItem.permission;
item.status = memberItem.status;
item.memberId = memberItem.userId
}
}
})
return projectMemberItemList
}
async checkAdminPermission({ userId, projectId }: { userId: number, projectId: number }) { async checkAdminPermission({ userId, projectId }: { userId: number, projectId: number }) {
return await this.checkPermission({ return await this.checkPermission({
userId, userId,
@@ -143,8 +176,8 @@ export class ProjectService extends BaseService<ProjectEntity> {
throw new Error('项目已禁用'); throw new Error('项目已禁用');
} }
const member = await this.projectMemberService.getMember(projectId, userId); const member = await this.projectMemberService.getMember(projectId, userId);
if (!member) { if (!member || member.status !== 'approved') {
throw new Error(`用户${userId}不是项目${projectId}成员`); throw new Error(`用户${userId}不是项目${projectId}成员`);
} }
savedPermission = member.permission; savedPermission = member.permission;
} }
@@ -169,4 +202,50 @@ export class ProjectService extends BaseService<ProjectEntity> {
} }
return true return true
} }
async applyJoin({ userId, projectId }: { userId: number, projectId: number }) {
const project = await this.info(projectId);
if (!project) {
throw new Error('项目不存在');
}
if (project.disabled) {
throw new Error('项目已禁用');
}
if (project.adminId === userId) {
throw new Error('申请用户已经是该项目的管理员');
}
const member = await this.projectMemberService.getMember(projectId, userId);
if (member && member.status === 'approved') {
throw new Error('用户已加入项目');
}
if (member){
this.projectMemberService.update({
id: member.id,
status: 'pending',
})
}else{
// 加入项目
await this.projectMemberService.add({
userId,
projectId,
permission: 'read',
status: 'pending',
})
}
}
async approveJoin({ userId, projectId,status,permission }: { userId: number, projectId: number,status:string,permission:string }) {
const member = await this.projectMemberService.getMember(projectId, userId);
if (!member) {
throw new Error('找不到用户的申请记录');
}
await this.projectMemberService.update({
id: member.id,
status: status,
permission,
})
}
} }