refactor(projects): 1、优化新增 产品和新增项目;2、调整角色提示信息
This commit is contained in:
@@ -33,8 +33,6 @@ interface ProductMemberResponse {
|
||||
roleId: string | number;
|
||||
roleName: string;
|
||||
roleCode: string;
|
||||
/** 多角色合并展示的非主角色名列表 */
|
||||
additionalRoleNames?: string[] | null;
|
||||
managerFlag: boolean;
|
||||
status: 0 | 1;
|
||||
joinedTime: string;
|
||||
@@ -76,7 +74,6 @@ export function normalizeProductMember(response: ProductMemberResponse): Api.Pro
|
||||
roleId: normalizeStringId(response.roleId),
|
||||
roleName: response.roleName || '',
|
||||
roleCode: response.roleCode || '',
|
||||
additionalRoleNames: response.additionalRoleNames ?? [],
|
||||
managerFlag: Boolean(response.managerFlag),
|
||||
status: response.status,
|
||||
joinedTime: response.joinedTime,
|
||||
|
||||
@@ -574,6 +574,19 @@ export async function fetchCreateProductMember(id: string, data: Api.Product.Cre
|
||||
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
|
||||
}
|
||||
|
||||
export async function fetchBatchCreateProductMembers(id: string, data: Api.Product.BatchCreateProductMembersParams) {
|
||||
const result = await request<Array<string | number>>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PRODUCT_PREFIX}/${id}/members/batch`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<Array<string | number>>, list =>
|
||||
Array.isArray(list) ? list.map(normalizeStringId) : []
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchUpdateProductMember(id: string, memberId: string, data: Api.Product.UpdateProductMemberParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
@@ -583,6 +596,15 @@ export function fetchUpdateProductMember(id: string, memberId: string, data: Api
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchBatchInactiveProductMembers(id: string, data: Api.Product.BatchInactiveProductMembersParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PRODUCT_PREFIX}/${id}/members/batch/inactive`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchInactiveProductMember(
|
||||
id: string,
|
||||
memberId: string,
|
||||
|
||||
@@ -147,8 +147,6 @@ export interface ProjectMemberResponse {
|
||||
roleId: string | number;
|
||||
roleName: string;
|
||||
roleCode: string;
|
||||
/** 多角色合并展示的非主角色名列表 */
|
||||
additionalRoleNames?: string[] | null;
|
||||
managerFlag: boolean;
|
||||
status: 0 | 1;
|
||||
joinedTime: string;
|
||||
@@ -227,7 +225,6 @@ export function normalizeProjectMember(response: ProjectMemberResponse): Api.Pro
|
||||
roleId: normalizeStringId(response.roleId),
|
||||
roleName: response.roleName || '',
|
||||
roleCode: response.roleCode || '',
|
||||
additionalRoleNames: response.additionalRoleNames ?? [],
|
||||
managerFlag: Boolean(response.managerFlag),
|
||||
status: response.status,
|
||||
joinedTime: response.joinedTime,
|
||||
|
||||
@@ -284,6 +284,28 @@ export function fetchInactiveProjectMember(
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchBatchCreateProjectMembers(id: string, data: Api.Project.BatchCreateProjectMembersParams) {
|
||||
const result = await request<Array<string | number>>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PROJECT_PREFIX}/${id}/members/batch`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
|
||||
return mapServiceResult(result as ServiceRequestResult<Array<string | number>>, list =>
|
||||
Array.isArray(list) ? list.map(normalizeStringId) : []
|
||||
);
|
||||
}
|
||||
|
||||
export function fetchBatchInactiveProjectMembers(id: string, data: Api.Project.BatchInactiveProjectMembersParams) {
|
||||
return request<boolean>({
|
||||
...safeJsonRequestConfig,
|
||||
url: `${PROJECT_PREFIX}/${id}/members/batch/inactive`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取项目设置 */
|
||||
export async function fetchGetProjectSettings(id: string) {
|
||||
const result = await fetchGetProject(id);
|
||||
|
||||
30
src/typings/api/product.d.ts
vendored
30
src/typings/api/product.d.ts
vendored
@@ -99,15 +99,10 @@ declare namespace Api {
|
||||
userNickname: string;
|
||||
/** 角色 ID */
|
||||
roleId: string;
|
||||
/** 角色名称(主角色) */
|
||||
/** 角色名称 */
|
||||
roleName: string;
|
||||
/** 角色编码(主角色) */
|
||||
/** 角色编码 */
|
||||
roleCode: string;
|
||||
/**
|
||||
* 非主角色的中文名列表(多角色合并展示用,按字典序升序)
|
||||
* 单角色时为空数组 [];典型场景:创建者 + 经理重合时,主行 manager,creator 名进此列表
|
||||
*/
|
||||
additionalRoleNames: string[];
|
||||
/** 是否当前产品经理 */
|
||||
managerFlag: boolean;
|
||||
/** 成员状态 */
|
||||
@@ -215,6 +210,20 @@ declare namespace Api {
|
||||
previousManagerRoleId?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量新增产品成员参数
|
||||
*
|
||||
* 刻意不复用 CreateProductMemberParams:批量接口不承担「产品经理交接」语义,
|
||||
* 后端兜底拒绝 roleId 为产品经理角色的项。
|
||||
*/
|
||||
interface BatchCreateProductMembersParams {
|
||||
members: Array<{
|
||||
userId: string;
|
||||
roleId: string;
|
||||
remark?: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 产品创建(含初始团队)原子接口参数
|
||||
*
|
||||
@@ -223,7 +232,7 @@ declare namespace Api {
|
||||
interface CreateProductWithTeamParams {
|
||||
product: SaveProductParams;
|
||||
members: CreateProductMemberParams[];
|
||||
/** 关心人 user_id 数组(选填);后端按 (user, object, role) 三元组幂等写入 product_watcher 角色 */
|
||||
/** 关注人 user_id 数组(选填);后端按 (user, object, role) 三元组幂等写入 product_watcher 角色 */
|
||||
watcherUserIds?: string[];
|
||||
}
|
||||
|
||||
@@ -239,6 +248,11 @@ declare namespace Api {
|
||||
reason?: string | null;
|
||||
}
|
||||
|
||||
interface BatchInactiveProductMembersParams {
|
||||
memberIds: string[];
|
||||
reason?: string | null;
|
||||
}
|
||||
|
||||
// ========== 产品需求相关类型定义 ==========
|
||||
/** 需求状态编码 */
|
||||
type RequirementStatusCode =
|
||||
|
||||
31
src/typings/api/project.d.ts
vendored
31
src/typings/api/project.d.ts
vendored
@@ -519,15 +519,10 @@ declare namespace Api {
|
||||
userNickname: string;
|
||||
/** 角色 ID */
|
||||
roleId: string;
|
||||
/** 角色名称(主角色) */
|
||||
/** 角色名称 */
|
||||
roleName: string;
|
||||
/** 角色编码(主角色) */
|
||||
/** 角色编码 */
|
||||
roleCode: string;
|
||||
/**
|
||||
* 非主角色的中文名列表(多角色合并展示用,按字典序升序)
|
||||
* 单角色时为空数组 [];典型场景:创建者 + 负责人重合时,主行 manager,creator 名进此列表
|
||||
*/
|
||||
additionalRoleNames: string[];
|
||||
/** 是否项目负责人 */
|
||||
managerFlag: boolean;
|
||||
/** 成员状态 */
|
||||
@@ -625,6 +620,26 @@ declare namespace Api {
|
||||
reason: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量新增项目成员参数
|
||||
*
|
||||
* 刻意不复用 CreateProjectMemberParams:批量接口不承担"项目负责人交接"语义,
|
||||
* 后端兜底拒绝 roleId 为项目负责人角色的项。
|
||||
*/
|
||||
interface BatchCreateProjectMembersParams {
|
||||
members: Array<{
|
||||
userId: string;
|
||||
roleId: string;
|
||||
remark?: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
/** 批量移出项目成员参数 */
|
||||
interface BatchInactiveProjectMembersParams {
|
||||
memberIds: string[];
|
||||
reason?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目创建(含初始团队)原子接口参数
|
||||
*
|
||||
@@ -633,7 +648,7 @@ declare namespace Api {
|
||||
interface CreateProjectWithTeamParams {
|
||||
project: SaveProjectParams;
|
||||
members: CreateProjectMemberParams[];
|
||||
/** 关心人 user_id 数组(选填);后端按 (user, object, role) 三元组幂等写入 project_watcher 角色 */
|
||||
/** 关注人 user_id 数组(选填);后端按 (user, object, role) 三元组幂等写入 project_watcher 角色 */
|
||||
watcherUserIds?: string[];
|
||||
}
|
||||
|
||||
|
||||
4
src/typings/api/system-manage.d.ts
vendored
4
src/typings/api/system-manage.d.ts
vendored
@@ -47,6 +47,8 @@ declare namespace Api {
|
||||
type: RoleType;
|
||||
/** remark */
|
||||
remark?: string | null;
|
||||
/** 是否在前端选择面板可见:0 不可见 / 1 可见,缺省视作可见 */
|
||||
visible?: 0 | 1 | null;
|
||||
/** create time */
|
||||
createTime: number;
|
||||
}
|
||||
@@ -226,7 +228,7 @@ declare namespace Api {
|
||||
|
||||
type PostList = PageResult<Post>;
|
||||
|
||||
type RoleSimple = Pick<Role, 'id' | 'name' | 'code' | 'status' | 'sort'>;
|
||||
type RoleSimple = Pick<Role, 'id' | 'name' | 'code' | 'status' | 'sort' | 'remark' | 'visible'>;
|
||||
|
||||
type RoleSimpleList = RoleSimple[];
|
||||
|
||||
|
||||
4
src/typings/components.d.ts
vendored
4
src/typings/components.d.ts
vendored
@@ -100,6 +100,10 @@ declare module 'vue' {
|
||||
IconCarbonStop: typeof import('~icons/carbon/stop')['default']
|
||||
'IconCharm:download': typeof import('~icons/charm/download')['default']
|
||||
'IconEp:arrowDown': typeof import('~icons/ep/arrow-down')['default']
|
||||
'IconEp:box': typeof import('~icons/ep/box')['default']
|
||||
'IconEp:files': typeof import('~icons/ep/files')['default']
|
||||
'IconEp:infoFilled': typeof import('~icons/ep/info-filled')['default']
|
||||
'IconEp:plus': typeof import('~icons/ep/plus')['default']
|
||||
IconEpRemoveFilled: typeof import('~icons/ep/remove-filled')['default']
|
||||
IconEpSuccessFilled: typeof import('~icons/ep/success-filled')['default']
|
||||
'IconF7:circleFill': typeof import('~icons/f7/circle-fill')['default']
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, reactive, watch } from 'vue';
|
||||
import { PRODUCT_MANAGER_ROLE_CODE } from '@/constants/business';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessUserSelect from '@/components/custom/business-user-select.vue';
|
||||
|
||||
defineOptions({ name: 'ProductCreateTeamMemberDialog' });
|
||||
|
||||
type OperateMode = 'create' | 'edit';
|
||||
|
||||
interface DraftMemberInput {
|
||||
userId: string;
|
||||
roleId: string;
|
||||
@@ -16,22 +12,16 @@ interface DraftMemberInput {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
mode: OperateMode;
|
||||
initial: DraftMemberInput | null;
|
||||
userOptions: Api.SystemManage.UserSimple[];
|
||||
roleOptions: Api.SystemManage.RoleSimple[];
|
||||
/** 已使用且不可选的 userId(编辑模式应当排除当前行自身) */
|
||||
disabledUserIds?: readonly string[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', payload: DraftMemberInput): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disabledUserIds: () => []
|
||||
});
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
@@ -39,31 +29,21 @@ const visible = defineModel<boolean>('visible', { default: false });
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const model = reactive<DraftMemberInput>({
|
||||
userId: '',
|
||||
roleId: '',
|
||||
remark: ''
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => (props.mode === 'create' ? '新增团队成员' : '调整成员角色'));
|
||||
const model = reactive<DraftMemberInput>({ userId: '', roleId: '', remark: '' });
|
||||
|
||||
const userLabelMap = computed(() => new Map(props.userOptions.map(item => [String(item.id), item.nickname])));
|
||||
|
||||
const selectableRoles = computed(() => props.roleOptions.filter(role => role.visible !== 0));
|
||||
|
||||
const rules = computed(
|
||||
() =>
|
||||
({
|
||||
userId: props.mode === 'create' ? [createRequiredRule('请选择成员用户')] : [],
|
||||
roleId: [createRequiredRule('请选择角色')]
|
||||
}) satisfies Record<string, App.Global.FormRule[]>
|
||||
);
|
||||
|
||||
function isManagerRole(role: Api.SystemManage.RoleSimple) {
|
||||
return role.code === PRODUCT_MANAGER_ROLE_CODE;
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
await validate();
|
||||
|
||||
emit('submit', {
|
||||
userId: model.userId,
|
||||
roleId: model.roleId,
|
||||
@@ -72,34 +52,21 @@ async function handleConfirm() {
|
||||
}
|
||||
|
||||
watch(visible, async value => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) return;
|
||||
model.userId = props.initial?.userId || '';
|
||||
model.roleId = props.initial?.roleId || '';
|
||||
model.remark = props.initial?.remark || '';
|
||||
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog v-model="visible" :title="dialogTitle" preset="sm" @confirm="handleConfirm">
|
||||
<BusinessFormDialog v-model="visible" title="调整成员角色" preset="sm" @confirm="handleConfirm">
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" :validate-on-rule-change="false">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem v-if="mode === 'create'" label="成员用户" prop="userId">
|
||||
<BusinessUserSelect
|
||||
v-model="model.userId"
|
||||
:options="userOptions"
|
||||
:disabled-user-ids="disabledUserIds"
|
||||
disabled-label="已添加"
|
||||
placeholder="请选择成员用户"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-else label="成员用户">
|
||||
<ElFormItem label="成员用户">
|
||||
<ElInput
|
||||
:model-value="userLabelMap.get(String(model.userId)) || ''"
|
||||
readonly
|
||||
@@ -111,17 +78,13 @@ watch(visible, async value => {
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="目标角色" prop="roleId">
|
||||
<ElSelect v-model="model.roleId" class="w-full" placeholder="请选择角色">
|
||||
<ElOption
|
||||
v-for="role in roleOptions"
|
||||
:key="role.id"
|
||||
:label="role.name"
|
||||
:value="role.id"
|
||||
:disabled="isManagerRole(role)"
|
||||
>
|
||||
<span>{{ role.name }}</span>
|
||||
<span v-if="isManagerRole(role)" class="product-create-team-member-dialog__role-hint">
|
||||
(已由第 1 步指定)
|
||||
</span>
|
||||
<ElOption v-for="role in selectableRoles" :key="role.id" :label="role.name" :value="role.id">
|
||||
<div class="product-create-team-member-dialog__role-option">
|
||||
<span class="product-create-team-member-dialog__role-option-name">{{ role.name }}</span>
|
||||
<ElTooltip v-if="role.remark" :content="role.remark" placement="right" :show-after="120">
|
||||
<icon-ep:info-filled class="product-create-team-member-dialog__role-option-info" @click.stop />
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
@@ -144,12 +107,6 @@ watch(visible, async value => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.product-create-team-member-dialog__role-hint {
|
||||
margin-left: 8px;
|
||||
color: rgb(148 163 184 / 96%);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.product-create-team-member-dialog__readonly-input .el-input__wrapper) {
|
||||
background: linear-gradient(180deg, rgb(241 245 249 / 98%), rgb(226 232 240 / 94%)), rgb(241 245 249);
|
||||
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
|
||||
@@ -166,4 +123,31 @@ watch(visible, async value => {
|
||||
-webkit-text-fill-color: rgb(51 65 85 / 96%);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.product-create-team-member-dialog__role-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.product-create-team-member-dialog__role-option-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-create-team-member-dialog__role-option-info {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.product-create-team-member-dialog__role-option-info:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { PRODUCT_MANAGER_ROLE_CODE } from '@/constants/business';
|
||||
import { fetchGetRoleSimpleList } from '@/service/api';
|
||||
import { getProductTeamTableHeight } from '../../setting/shared';
|
||||
import ProductTeamBatchDialog, {
|
||||
type BatchMemberPayload
|
||||
} from '@/views/product/shared/components/product-team-batch-dialog.vue';
|
||||
import ProductCreateTeamMemberDialog from './product-create-team-member-dialog.vue';
|
||||
import type { ProductCreateBaseForm } from './product-create-base-form.vue';
|
||||
|
||||
@@ -21,57 +22,34 @@ interface DraftMember {
|
||||
interface Props {
|
||||
baseInfo: ProductCreateBaseForm;
|
||||
userOptions: Api.SystemManage.UserSimple[];
|
||||
roleOptions: Api.SystemManage.RoleSimple[];
|
||||
roleLoading: boolean;
|
||||
managerRoleError: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:members', members: Api.Product.CreateProductMemberParams[]): void;
|
||||
(e: 'update:watcherUserIds', watcherUserIds: string[]): void;
|
||||
}>();
|
||||
|
||||
const roleOptions = ref<Api.SystemManage.RoleSimple[]>([]);
|
||||
const roleLoading = ref(false);
|
||||
const managerRoleError = ref('');
|
||||
const members = ref<DraftMember[]>([]);
|
||||
|
||||
const memberDialogVisible = ref(false);
|
||||
const memberDialogMode = ref<'create' | 'edit'>('create');
|
||||
const editingKey = ref<string | null>(null);
|
||||
|
||||
const watcherUserIds = ref<string[]>([]);
|
||||
const batchDialogVisible = ref(false);
|
||||
|
||||
// 关心人候选用户:排除已在团队成员列表中的用户(包含产品经理本人)
|
||||
const watcherUserOptions = computed(() => {
|
||||
const memberUserIds = new Set(members.value.map(item => item.userId).filter(Boolean));
|
||||
return props.userOptions.filter(user => !memberUserIds.has(user.id));
|
||||
});
|
||||
|
||||
const teamTableHeight = getProductTeamTableHeight(4);
|
||||
const batchDisabledUserIds = computed(() => members.value.map(item => item.userId).filter(Boolean));
|
||||
|
||||
const userLabelMap = computed(() => new Map(props.userOptions.map(item => [String(item.id), item.nickname])));
|
||||
|
||||
const managerRole = computed(() => roleOptions.value.find(item => item.code === PRODUCT_MANAGER_ROLE_CODE) ?? null);
|
||||
|
||||
// 弹框传入的禁选用户列表:新增时排除所有已选;编辑时排除自身以外的已选
|
||||
const dialogDisabledUserIds = computed(() => {
|
||||
return members.value
|
||||
.filter(item => !editingKey.value || item.key !== editingKey.value)
|
||||
.map(item => item.userId)
|
||||
.filter(Boolean);
|
||||
});
|
||||
const managerRole = computed(() => props.roleOptions.find(item => item.code === PRODUCT_MANAGER_ROLE_CODE) ?? null);
|
||||
|
||||
const dialogInitial = computed(() => {
|
||||
if (memberDialogMode.value === 'create' || !editingKey.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!editingKey.value) return null;
|
||||
const target = members.value.find(item => item.key === editingKey.value);
|
||||
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!target) return null;
|
||||
return { userId: target.userId, roleId: target.roleId, remark: target.remark };
|
||||
});
|
||||
|
||||
@@ -80,31 +58,13 @@ function getUserNickname(userId: string) {
|
||||
}
|
||||
|
||||
function getRoleName(roleId: string) {
|
||||
return roleOptions.value.find(item => item.id === roleId)?.name || '--';
|
||||
return props.roleOptions.find(item => item.id === roleId)?.name || '--';
|
||||
}
|
||||
|
||||
function generateKey() {
|
||||
return `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
async function loadRoles() {
|
||||
roleLoading.value = true;
|
||||
managerRoleError.value = '';
|
||||
|
||||
const { data } = await fetchGetRoleSimpleList({ scopeType: 'object', objectType: 'product' });
|
||||
|
||||
roleLoading.value = false;
|
||||
|
||||
roleOptions.value = data ?? [];
|
||||
|
||||
if (!managerRole.value) {
|
||||
managerRoleError.value = '未找到产品经理角色,请联系管理员';
|
||||
return;
|
||||
}
|
||||
|
||||
refreshManagerRow();
|
||||
}
|
||||
|
||||
function refreshManagerRow() {
|
||||
const managerUserId = props.baseInfo.managerUserId;
|
||||
|
||||
@@ -132,14 +92,25 @@ function refreshManagerRow() {
|
||||
emitMembers();
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
memberDialogMode.value = 'create';
|
||||
editingKey.value = null;
|
||||
memberDialogVisible.value = true;
|
||||
function openBatch() {
|
||||
batchDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleBatchSubmit(payloads: BatchMemberPayload[]) {
|
||||
for (const p of payloads) {
|
||||
members.value.push({
|
||||
key: generateKey(),
|
||||
userId: p.userId,
|
||||
roleId: p.roleId,
|
||||
remark: p.remark,
|
||||
locked: false
|
||||
});
|
||||
}
|
||||
batchDialogVisible.value = false;
|
||||
emitMembers();
|
||||
}
|
||||
|
||||
function openEdit(row: DraftMember) {
|
||||
memberDialogMode.value = 'edit';
|
||||
editingKey.value = row.key;
|
||||
memberDialogVisible.value = true;
|
||||
}
|
||||
@@ -149,27 +120,16 @@ function removeMember(key: string) {
|
||||
emitMembers();
|
||||
}
|
||||
|
||||
function handleMemberSubmit(payload: { userId: string; roleId: string; remark: string }) {
|
||||
if (memberDialogMode.value === 'create') {
|
||||
members.value.push({
|
||||
key: generateKey(),
|
||||
userId: payload.userId,
|
||||
function handleMemberEditSubmit(payload: { userId: string; roleId: string; remark: string }) {
|
||||
if (!editingKey.value) return;
|
||||
const idx = members.value.findIndex(item => item.key === editingKey.value);
|
||||
if (idx >= 0) {
|
||||
members.value[idx] = {
|
||||
...members.value[idx],
|
||||
roleId: payload.roleId,
|
||||
remark: payload.remark,
|
||||
locked: false
|
||||
});
|
||||
} else if (editingKey.value) {
|
||||
const idx = members.value.findIndex(item => item.key === editingKey.value);
|
||||
|
||||
if (idx >= 0) {
|
||||
members.value[idx] = {
|
||||
...members.value[idx],
|
||||
roleId: payload.roleId,
|
||||
remark: payload.remark
|
||||
};
|
||||
}
|
||||
remark: payload.remark
|
||||
};
|
||||
}
|
||||
|
||||
memberDialogVisible.value = false;
|
||||
emitMembers();
|
||||
}
|
||||
@@ -188,8 +148,8 @@ function emitMembers() {
|
||||
}
|
||||
|
||||
async function runValidate(): Promise<boolean> {
|
||||
if (managerRoleError.value) {
|
||||
window.$message?.error(managerRoleError.value);
|
||||
if (props.managerRoleError) {
|
||||
window.$message?.error(props.managerRoleError);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -214,43 +174,32 @@ async function runValidate(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleWatcherChange(ids: string[]) {
|
||||
watcherUserIds.value = ids;
|
||||
emit('update:watcherUserIds', ids);
|
||||
}
|
||||
|
||||
// 团队成员变化时,剔除已被加入团队的关心人,避免重叠
|
||||
watch(
|
||||
() => members.value.map(item => item.userId).join(','),
|
||||
() => {
|
||||
const memberUserIds = new Set(members.value.map(item => item.userId).filter(Boolean));
|
||||
const filtered = watcherUserIds.value.filter(id => !memberUserIds.has(id));
|
||||
|
||||
if (filtered.length !== watcherUserIds.value.length) {
|
||||
handleWatcherChange(filtered);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(loadRoles);
|
||||
|
||||
watch(
|
||||
() => props.baseInfo.managerUserId,
|
||||
() => {
|
||||
if (!managerRoleError.value && managerRole.value) {
|
||||
if (!props.managerRoleError && managerRole.value) {
|
||||
refreshManagerRow();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// roleOptions 异步加载到位后,补一次 locked 行刷新
|
||||
watch(managerRole, () => {
|
||||
if (!props.managerRoleError && managerRole.value && props.baseInfo.managerUserId) {
|
||||
refreshManagerRow();
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({ validate: runValidate });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="roleLoading" class="team-step">
|
||||
<div class="team-step__toolbar">
|
||||
<ElButton type="primary" plain :disabled="Boolean(managerRoleError)" @click="openCreate">新增成员</ElButton>
|
||||
</div>
|
||||
<button type="button" class="team-step__add" :disabled="Boolean(managerRoleError)" @click="openBatch">
|
||||
<icon-ep:plus class="team-step__add-icon" />
|
||||
<span>新增成员</span>
|
||||
<span class="team-step__add-hint">从部门 / 管理链路 / 全公司 批量选人</span>
|
||||
</button>
|
||||
|
||||
<ElAlert
|
||||
v-if="managerRoleError"
|
||||
@@ -261,62 +210,49 @@ defineExpose({ validate: runValidate });
|
||||
class="team-step__alert"
|
||||
/>
|
||||
|
||||
<ElTable :data="members" :height="teamTableHeight" border row-key="key" empty-text="点击右上角“新增成员”添加">
|
||||
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
||||
<ElTableColumn label="成员姓名" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getUserNickname(row.userId) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="当前角色" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ getRoleName(row.roleId) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="备注" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.remark || '--' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="team-step__actions">
|
||||
<ElButton link type="primary" :disabled="row.locked" @click="openEdit(row)">编辑</ElButton>
|
||||
<ElButton link type="danger" :disabled="row.locked" @click="removeMember(row.key)">移除</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<div class="watcher-row">
|
||||
<span class="watcher-row__label">
|
||||
关心人
|
||||
<span class="watcher-row__optional">(选填)</span>
|
||||
</span>
|
||||
<ElSelect
|
||||
:model-value="watcherUserIds"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:max-collapse-tags="3"
|
||||
placeholder="可在列表 / 概览看到此产品的关注人"
|
||||
class="watcher-row__select"
|
||||
@update:model-value="handleWatcherChange"
|
||||
>
|
||||
<ElOption v-for="item in watcherUserOptions" :key="item.id" :value="item.id" :label="item.nickname" />
|
||||
</ElSelect>
|
||||
<div class="team-step__table-wrap">
|
||||
<ElTable :data="members" height="100%" border row-key="key" empty-text="点击右上角“新增成员”添加">
|
||||
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
||||
<ElTableColumn label="成员姓名" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getUserNickname(row.userId) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="当前角色" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ getRoleName(row.roleId) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="备注" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.remark || '--' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="team-step__actions">
|
||||
<ElButton link type="primary" :disabled="row.locked" @click="openEdit(row)">编辑</ElButton>
|
||||
<ElButton link type="danger" :disabled="row.locked" @click="removeMember(row.key)">移除</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<ProductCreateTeamMemberDialog
|
||||
v-model:visible="memberDialogVisible"
|
||||
:mode="memberDialogMode"
|
||||
:initial="dialogInitial"
|
||||
:user-options="userOptions"
|
||||
:role-options="roleOptions"
|
||||
:disabled-user-ids="dialogDisabledUserIds"
|
||||
@submit="handleMemberSubmit"
|
||||
@submit="handleMemberEditSubmit"
|
||||
/>
|
||||
|
||||
<ProductTeamBatchDialog
|
||||
v-model:visible="batchDialogVisible"
|
||||
:user-options="userOptions"
|
||||
:role-options="roleOptions"
|
||||
:disabled-user-ids="batchDisabledUserIds"
|
||||
@submit="handleBatchSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -326,16 +262,65 @@ defineExpose({ validate: runValidate });
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.team-step__toolbar {
|
||||
.team-step__add {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 0 16px;
|
||||
border: 1px dashed var(--el-border-color-darker);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
color 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.team-step__add:hover:not(:disabled) {
|
||||
border-color: var(--el-color-primary);
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.team-step__add:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.team-step__add-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.team-step__add-hint {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.team-step__add:hover:not(:disabled) .team-step__add-hint {
|
||||
color: var(--el-color-primary);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.team-step__alert {
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.team-step__table-wrap {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.team-step__actions {
|
||||
@@ -343,27 +328,4 @@ defineExpose({ validate: runValidate });
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.watcher-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.watcher-row__label {
|
||||
flex: 0 0 auto;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: rgb(60 70 95 / 96%);
|
||||
}
|
||||
|
||||
.watcher-row__optional {
|
||||
color: rgb(140 150 170 / 96%);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.watcher-row__select {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { PRODUCT_MANAGER_ROLE_CODE } from '@/constants/business';
|
||||
import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict';
|
||||
import { fetchCreateProductWithTeam, fetchGetProduct, fetchUpdateProduct } from '@/service/api';
|
||||
import { fetchCreateProductWithTeam, fetchGetProduct, fetchGetRoleSimpleList, fetchUpdateProduct } from '@/service/api';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import DictSelect from '@/components/custom/dict-select.vue';
|
||||
@@ -112,9 +113,29 @@ const baseFormRef = ref<InstanceType<typeof ProductCreateBaseForm> | null>(null)
|
||||
const teamStepRef = ref<InstanceType<typeof ProductCreateTeamStep> | null>(null);
|
||||
const currentStep = ref<1 | 2>(1);
|
||||
|
||||
// === 新增模式:角色列表(父级加载,下发给 team-step 与批量弹层) ===
|
||||
const roleOptions = ref<Api.SystemManage.RoleSimple[]>([]);
|
||||
const roleLoading = ref(false);
|
||||
const managerRoleError = ref('');
|
||||
|
||||
const managerRole = computed(() => roleOptions.value.find(item => item.code === PRODUCT_MANAGER_ROLE_CODE) ?? null);
|
||||
|
||||
async function loadRoles() {
|
||||
roleLoading.value = true;
|
||||
managerRoleError.value = '';
|
||||
|
||||
const { data } = await fetchGetRoleSimpleList({ scopeType: 'object', objectType: 'product' });
|
||||
|
||||
roleLoading.value = false;
|
||||
roleOptions.value = data ?? [];
|
||||
|
||||
if (!managerRole.value) {
|
||||
managerRoleError.value = '未找到产品经理角色,请联系管理员';
|
||||
}
|
||||
}
|
||||
|
||||
const createBaseModel = ref<ProductCreateBaseFormModel>(createBaseInfo());
|
||||
const draftMembers = ref<Api.Product.CreateProductMemberParams[]>([]);
|
||||
const draftWatcherUserIds = ref<string[]>([]);
|
||||
|
||||
function createBaseInfo(): ProductCreateBaseFormModel {
|
||||
return { code: '', name: '', directionCode: '', managerUserId: null, description: '' };
|
||||
@@ -158,8 +179,7 @@ async function handleCreateSubmit() {
|
||||
managerUserId: createBaseModel.value.managerUserId as string,
|
||||
description: getNullableText(createBaseModel.value.description)
|
||||
},
|
||||
members: draftMembers.value,
|
||||
watcherUserIds: draftWatcherUserIds.value.length > 0 ? draftWatcherUserIds.value : undefined
|
||||
members: draftMembers.value
|
||||
};
|
||||
|
||||
const { error, data } = await fetchCreateProductWithTeam(payload);
|
||||
@@ -188,8 +208,8 @@ watch(visible, async value => {
|
||||
editModel.value = createEditModel();
|
||||
createBaseModel.value = createBaseInfo();
|
||||
draftMembers.value = [];
|
||||
draftWatcherUserIds.value = [];
|
||||
await nextTick();
|
||||
await loadRoles();
|
||||
editFormRef.value?.clearValidate();
|
||||
return;
|
||||
}
|
||||
@@ -295,7 +315,7 @@ watch(visible, async value => {
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
|
||||
<!-- 新增模式:两步向导(复合内容特例,自定义 ElDialog 880px) -->
|
||||
<!-- 新增模式:两步向导(复合内容特例,1080px,左侧概念区 + 右侧步骤面板) -->
|
||||
<ElDialog
|
||||
v-else
|
||||
v-model="visible"
|
||||
@@ -304,43 +324,85 @@ watch(visible, async value => {
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
align-center
|
||||
width="760px"
|
||||
width="1080px"
|
||||
>
|
||||
<div class="product-create-dialog__stepbar">
|
||||
<div class="product-create-dialog__step" :class="{ 'is-active': currentStep === 1, 'is-done': currentStep > 1 }">
|
||||
<span class="product-create-dialog__step-index">1</span>
|
||||
<span class="product-create-dialog__step-text">
|
||||
<strong>基础资料</strong>
|
||||
<small>定义产品身份和负责人</small>
|
||||
</span>
|
||||
</div>
|
||||
<div class="product-create-dialog__step" :class="{ 'is-active': currentStep === 2 }">
|
||||
<span class="product-create-dialog__step-index">2</span>
|
||||
<span class="product-create-dialog__step-text">
|
||||
<strong>初始化团队</strong>
|
||||
<small>配置对象域成员角色</small>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="product-create-dialog__split">
|
||||
<aside class="product-create-dialog__guide">
|
||||
<div class="product-create-dialog__guide-hero">
|
||||
<div class="product-create-dialog__guide-hero-icon">
|
||||
<icon-ep:box />
|
||||
</div>
|
||||
<div class="product-create-dialog__guide-hero-text">
|
||||
<div class="product-create-dialog__guide-hero-title">产品</div>
|
||||
<div class="product-create-dialog__guide-hero-sub">需求 · 项目 · 迭代 的承载单元</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-create-dialog__body">
|
||||
<div v-show="currentStep === 1" class="product-create-dialog__panel">
|
||||
<ProductCreateBaseForm ref="baseFormRef" v-model="createBaseModel" :manager-user-options="managerUserOptions" />
|
||||
</div>
|
||||
<div v-show="currentStep === 2" class="product-create-dialog__panel">
|
||||
<ProductCreateTeamStep
|
||||
ref="teamStepRef"
|
||||
:base-info="createBaseModel"
|
||||
:user-options="managerUserOptions"
|
||||
@update:members="draftMembers = $event"
|
||||
@update:watcher-user-ids="draftWatcherUserIds = $event"
|
||||
/>
|
||||
<p class="product-create-dialog__guide-lead">
|
||||
产品与需求池管理,提供多维度的需求规划工具,打通客户、业务团队和产研团队之间的协作。
|
||||
</p>
|
||||
|
||||
<section class="product-create-dialog__guide-section">
|
||||
<h4>包含</h4>
|
||||
<p>需求、变更、迭代、模块、文档、状态、统计报表。</p>
|
||||
</section>
|
||||
|
||||
<section class="product-create-dialog__guide-section">
|
||||
<h4>参与人</h4>
|
||||
<p>产品经理(必填,创建后锁定) · 团队角色(业务专员 / 游客 / 关注人)。</p>
|
||||
</section>
|
||||
|
||||
<section class="product-create-dialog__guide-section">
|
||||
<h4>命名建议</h4>
|
||||
<p>建议使用业务团队约定俗成的简短名称,产品创建后不再轻易调整,会影响下游引用。</p>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<div class="product-create-dialog__main">
|
||||
<div class="product-create-dialog__stepbar">
|
||||
<div
|
||||
class="product-create-dialog__step"
|
||||
:class="{ 'is-active': currentStep === 1, 'is-done': currentStep > 1 }"
|
||||
>
|
||||
<span class="product-create-dialog__step-index">1</span>
|
||||
<span class="product-create-dialog__step-text">
|
||||
<strong>基础资料</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="product-create-dialog__step" :class="{ 'is-active': currentStep === 2 }">
|
||||
<span class="product-create-dialog__step-index">2</span>
|
||||
<span class="product-create-dialog__step-text">
|
||||
<strong>初始化团队</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-create-dialog__body">
|
||||
<div v-show="currentStep === 1" class="product-create-dialog__panel">
|
||||
<ProductCreateBaseForm
|
||||
ref="baseFormRef"
|
||||
v-model="createBaseModel"
|
||||
:manager-user-options="managerUserOptions"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="currentStep === 2" class="product-create-dialog__panel">
|
||||
<ProductCreateTeamStep
|
||||
ref="teamStepRef"
|
||||
:base-info="createBaseModel"
|
||||
:user-options="managerUserOptions"
|
||||
:role-options="roleOptions"
|
||||
:role-loading="roleLoading"
|
||||
:manager-role-error="managerRoleError"
|
||||
@update:members="draftMembers = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="product-create-dialog__footer">
|
||||
<span class="product-create-dialog__footer-meta">第 {{ currentStep }} 步,共 2 步</span>
|
||||
<span class="product-create-dialog__footer-meta">第 {{ currentStep }} 步,共 2 步</span>
|
||||
<ElSpace :size="10">
|
||||
<ElButton @click="closeDialog">取消</ElButton>
|
||||
<ElButton v-if="currentStep === 2" @click="goPrev">上一步</ElButton>
|
||||
@@ -378,6 +440,92 @@ watch(visible, async value => {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.product-create-dialog__split {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.product-create-dialog__guide {
|
||||
padding: 28px 24px;
|
||||
background: linear-gradient(180deg, #f7f9fc 0%, #fafbfc 100%);
|
||||
border-right: 1px solid rgb(229 233 242 / 96%);
|
||||
overflow-y: auto;
|
||||
max-height: min(720px, calc(100vh - 160px));
|
||||
}
|
||||
|
||||
.product-create-dialog__guide-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid rgb(229 233 242 / 96%);
|
||||
}
|
||||
|
||||
.product-create-dialog__guide-hero-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
font-size: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.product-create-dialog__guide-hero-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.product-create-dialog__guide-hero-title {
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.product-create-dialog__guide-hero-sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.product-create-dialog__guide-lead {
|
||||
margin: 0 0 24px;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.8;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.product-create-dialog__guide-section + .product-create-dialog__guide-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.product-create-dialog__guide-section h4 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: var(--el-text-color-primary);
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.product-create-dialog__guide-section p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.75;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.product-create-dialog__main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.product-create-dialog__stepbar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -433,13 +581,15 @@ watch(visible, async value => {
|
||||
}
|
||||
|
||||
.product-create-dialog__body {
|
||||
min-height: 0;
|
||||
max-height: min(560px, calc(100vh - 240px));
|
||||
overflow: auto;
|
||||
height: min(520px, calc(100vh - 240px));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.product-create-dialog__panel {
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.product-create-dialog__footer {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useMediaQuery } from '@vueuse/core';
|
||||
import { LAYOUT_SCROLL_EL_ID } from '@sa/materials';
|
||||
import { objectContextDomainConfigs } from '@/constants/object-context';
|
||||
import {
|
||||
fetchBatchCreateProductMembers,
|
||||
fetchBatchInactiveProductMembers,
|
||||
fetchChangeProductStatus,
|
||||
fetchCreateProductMember,
|
||||
fetchDeleteProduct,
|
||||
@@ -19,8 +21,12 @@ import { useAuthStore } from '@/store/modules/auth';
|
||||
import { useObjectContextStore } from '@/store/modules/object-context';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import ProductTeamBatchDialog, {
|
||||
type BatchMemberPayload
|
||||
} from '@/views/product/shared/components/product-team-batch-dialog.vue';
|
||||
import { useCurrentProduct } from '../shared/use-current-product';
|
||||
import BaseInfoDialog from './modules/base-info-dialog.vue';
|
||||
import MemberBatchRemoveDialog from './modules/member-batch-remove-dialog.vue';
|
||||
import MemberOperateDialog from './modules/member-operate-dialog.vue';
|
||||
import MemberRemoveDialog from './modules/member-remove-dialog.vue';
|
||||
import ProductDeleteDialog from './modules/product-delete-dialog.vue';
|
||||
@@ -70,7 +76,11 @@ const pageLoading = ref(false);
|
||||
const memberLoading = ref(false);
|
||||
const baseInfoVisible = ref(false);
|
||||
const memberOperateVisible = ref(false);
|
||||
const memberBatchVisible = ref(false);
|
||||
const memberRemoveVisible = ref(false);
|
||||
const memberBatchRemoveVisible = ref(false);
|
||||
const teamPanelRef = ref<InstanceType<typeof SettingTeamPanel> | null>(null);
|
||||
const selectedBatchRemoveMembers = ref<Api.Product.ProductMember[]>([]);
|
||||
const statusActionVisible = ref(false);
|
||||
const deleteVisible = ref(false);
|
||||
const memberOperateMode = ref<'create' | 'edit'>('create');
|
||||
@@ -217,9 +227,7 @@ function scrollToSection(key: string) {
|
||||
}
|
||||
|
||||
function openCreateMember() {
|
||||
memberOperateMode.value = 'create';
|
||||
selectedMember.value = null;
|
||||
memberOperateVisible.value = true;
|
||||
memberBatchVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditMember(member: Api.Product.ProductMember) {
|
||||
@@ -233,6 +241,12 @@ function openRemoveMember(member: Api.Product.ProductMember) {
|
||||
memberRemoveVisible.value = true;
|
||||
}
|
||||
|
||||
function openBatchRemoveMember(targetMembers: Api.Product.ProductMember[]) {
|
||||
if (!targetMembers.length) return;
|
||||
selectedBatchRemoveMembers.value = targetMembers;
|
||||
memberBatchRemoveVisible.value = true;
|
||||
}
|
||||
|
||||
function openLifecycleAction(action: Api.Product.ProductLifecycleAction) {
|
||||
selectedAction.value = action;
|
||||
statusActionVisible.value = true;
|
||||
@@ -288,6 +302,29 @@ async function handleSubmitMemberOperate(event: {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmitMemberBatch(payloads: BatchMemberPayload[]) {
|
||||
if (!currentObjectId.value || !payloads.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await fetchBatchCreateProductMembers(currentObjectId.value, {
|
||||
members: payloads.map(item => ({
|
||||
userId: item.userId,
|
||||
roleId: item.roleId,
|
||||
remark: item.remark.trim() || null
|
||||
}))
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success(`已新增 ${payloads.length} 名成员`);
|
||||
memberBatchVisible.value = false;
|
||||
|
||||
await Promise.all([loadMembers(), loadSettings()]);
|
||||
}
|
||||
|
||||
async function handleSubmitRemoveMember(payload: Api.Product.InactiveProductMemberParams) {
|
||||
if (!currentObjectId.value || !selectedMember.value?.id) {
|
||||
return;
|
||||
@@ -305,6 +342,30 @@ async function handleSubmitRemoveMember(payload: Api.Product.InactiveProductMemb
|
||||
await Promise.all([loadMembers(), loadSettings()]);
|
||||
}
|
||||
|
||||
async function handleSubmitBatchRemoveMember(payload: { reason: string | null }) {
|
||||
if (!currentObjectId.value || !selectedBatchRemoveMembers.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const memberIds = selectedBatchRemoveMembers.value.map(item => item.id).filter((id): id is string => Boolean(id));
|
||||
|
||||
if (!memberIds.length) return;
|
||||
|
||||
const { error } = await fetchBatchInactiveProductMembers(currentObjectId.value, {
|
||||
memberIds,
|
||||
reason: payload.reason
|
||||
});
|
||||
|
||||
if (error) return;
|
||||
|
||||
window.$message?.success(`已移出 ${memberIds.length} 名成员`);
|
||||
memberBatchRemoveVisible.value = false;
|
||||
selectedBatchRemoveMembers.value = [];
|
||||
teamPanelRef.value?.clearSelection();
|
||||
|
||||
await Promise.all([loadMembers(), loadSettings()]);
|
||||
}
|
||||
|
||||
async function handleSubmitLifecycleAction(payload: Api.Product.ChangeProductStatusParams) {
|
||||
if (!currentObjectId.value || !selectedAction.value) {
|
||||
return;
|
||||
@@ -393,6 +454,7 @@ watch(
|
||||
|
||||
<section :id="sectionIdMap.team" class="product-setting-page__section">
|
||||
<SettingTeamPanel
|
||||
ref="teamPanelRef"
|
||||
:members="members"
|
||||
:role-options="roleOptions"
|
||||
:loading="memberLoading"
|
||||
@@ -400,6 +462,7 @@ watch(
|
||||
@create="openCreateMember"
|
||||
@edit="openEditMember"
|
||||
@remove="openRemoveMember"
|
||||
@batch-remove="openBatchRemoveMember"
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -427,11 +490,23 @@ watch(
|
||||
:disabled-user-ids="members.filter(member => member.status === 0).map(member => member.userId)"
|
||||
@submit="handleSubmitMemberOperate"
|
||||
/>
|
||||
<ProductTeamBatchDialog
|
||||
v-model:visible="memberBatchVisible"
|
||||
:user-options="userOptions"
|
||||
:role-options="roleOptions"
|
||||
:disabled-user-ids="members.filter(member => member.status === 0).map(member => member.userId)"
|
||||
@submit="handleSubmitMemberBatch"
|
||||
/>
|
||||
<MemberRemoveDialog
|
||||
v-model:visible="memberRemoveVisible"
|
||||
:member="selectedMember"
|
||||
@submit="handleSubmitRemoveMember"
|
||||
/>
|
||||
<MemberBatchRemoveDialog
|
||||
v-model:visible="memberBatchRemoveVisible"
|
||||
:members="selectedBatchRemoveMembers"
|
||||
@submit="handleSubmitBatchRemoveMember"
|
||||
/>
|
||||
<StatusActionDialog
|
||||
v-model:visible="statusActionVisible"
|
||||
:action="selectedAction"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
|
||||
defineOptions({ name: 'MemberBatchRemoveDialog' });
|
||||
|
||||
interface Props {
|
||||
members: Api.Product.ProductMember[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', payload: { reason: string | null }): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const model = reactive({
|
||||
reason: ''
|
||||
});
|
||||
|
||||
const previewNames = computed(() => {
|
||||
const names = props.members.map(item => item.userNickname || item.userId || '').filter(Boolean);
|
||||
|
||||
if (names.length <= 5) {
|
||||
return names.join('、');
|
||||
}
|
||||
|
||||
return `${names.slice(0, 5).join('、')} 等 ${names.length} 人`;
|
||||
});
|
||||
|
||||
function handleConfirm() {
|
||||
emit('submit', {
|
||||
reason: model.reason.trim() || null
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
value => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.reason = '';
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog v-model="visible" title="批量移出成员" preset="sm" @confirm="handleConfirm">
|
||||
<ElAlert
|
||||
:title="`确认将选中的 ${props.members.length} 名成员(${previewNames})从当前产品团队中移出吗?`"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
class="mb-16px"
|
||||
/>
|
||||
<ElForm label-position="top">
|
||||
<ElFormItem label="移出原因">
|
||||
<ElInput
|
||||
v-model="model.reason"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入移出原因(统一应用到所有选中成员)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { TableInstance } from 'element-plus';
|
||||
import { filterProductMembers, formatProductMemberDate, getProductTeamTableHeight } from '../shared';
|
||||
|
||||
defineOptions({ name: 'SettingTeamPanel' });
|
||||
@@ -15,6 +16,7 @@ interface Emits {
|
||||
(e: 'create'): void;
|
||||
(e: 'edit', member: Api.Product.ProductMember): void;
|
||||
(e: 'remove', member: Api.Product.ProductMember): void;
|
||||
(e: 'batch-remove', members: Api.Product.ProductMember[]): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -26,19 +28,41 @@ const emit = defineEmits<Emits>();
|
||||
const searchKeyword = ref('');
|
||||
const selectedRoleId = ref('');
|
||||
const teamTableHeight = getProductTeamTableHeight(5);
|
||||
const tableRef = ref<TableInstance | null>(null);
|
||||
const selectedRows = ref<Api.Product.ProductMember[]>([]);
|
||||
const selectedCount = computed(() => selectedRows.value.length);
|
||||
|
||||
function isRowSelectable(row: Api.Product.ProductMember) {
|
||||
return row.status === 0 && !row.managerFlag;
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: Api.Product.ProductMember[]) {
|
||||
selectedRows.value = rows;
|
||||
}
|
||||
|
||||
function handleBatchRemove() {
|
||||
if (!selectedRows.value.length) return;
|
||||
emit('batch-remove', [...selectedRows.value]);
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
tableRef.value?.clearSelection();
|
||||
selectedRows.value = [];
|
||||
}
|
||||
|
||||
defineExpose({ clearSelection });
|
||||
const roleFilterOptions = computed(() => {
|
||||
const roleMap = new Map<string, string>();
|
||||
const seen = new Set<string>();
|
||||
const result: Api.SystemManage.RoleSimple[] = [];
|
||||
|
||||
props.roleOptions.forEach(role => {
|
||||
if (!roleMap.has(role.id)) {
|
||||
roleMap.set(role.id, role.name);
|
||||
}
|
||||
if (role.visible === 0) return;
|
||||
if (seen.has(role.id)) return;
|
||||
seen.add(role.id);
|
||||
result.push(role);
|
||||
});
|
||||
|
||||
return [...roleMap.entries()].map(([value, label]) => ({
|
||||
value,
|
||||
label
|
||||
}));
|
||||
return result;
|
||||
});
|
||||
const filteredMembers = computed(() =>
|
||||
filterProductMembers(props.members, {
|
||||
@@ -49,7 +73,7 @@ const filteredMembers = computed(() =>
|
||||
const hasFilter = computed(() => Boolean(searchKeyword.value.trim() || selectedRoleId.value));
|
||||
|
||||
watch(roleFilterOptions, options => {
|
||||
if (selectedRoleId.value && !options.some(item => item.value === selectedRoleId.value)) {
|
||||
if (selectedRoleId.value && !options.some(item => item.id === selectedRoleId.value)) {
|
||||
selectedRoleId.value = '';
|
||||
}
|
||||
});
|
||||
@@ -72,12 +96,14 @@ function getMemberStatusTagType(status: Api.Product.ProductMemberStatus) {
|
||||
</div>
|
||||
<div class="setting-team-panel__toolbar">
|
||||
<ElSelect v-model="selectedRoleId" clearable placeholder="筛选角色" class="setting-team-panel__role-filter">
|
||||
<ElOption
|
||||
v-for="option in roleFilterOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
<ElOption v-for="role in roleFilterOptions" :key="role.id" :label="role.name" :value="role.id">
|
||||
<div class="setting-team-panel__role-option">
|
||||
<span class="setting-team-panel__role-option-name">{{ role.name }}</span>
|
||||
<ElTooltip v-if="role.remark" :content="role.remark" placement="right" :show-after="120">
|
||||
<icon-ep:info-filled class="setting-team-panel__role-option-info" @click.stop />
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<ElInput v-model="searchKeyword" clearable placeholder="搜索成员姓名" class="setting-team-panel__search" />
|
||||
<ElButton
|
||||
@@ -89,35 +115,42 @@ function getMemberStatusTagType(status: Api.Product.ProductMemberStatus) {
|
||||
>
|
||||
新增成员
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="!props.readonly"
|
||||
v-auth="{ code: 'project:product:update', source: 'object' }"
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="selectedCount === 0"
|
||||
@click="handleBatchRemove"
|
||||
>
|
||||
批量移出{{ selectedCount > 0 ? `(${selectedCount})` : '' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElTable
|
||||
ref="tableRef"
|
||||
v-loading="props.loading"
|
||||
:data="filteredMembers"
|
||||
:height="teamTableHeight"
|
||||
:empty-text="hasFilter ? '未找到匹配成员' : '暂无成员'"
|
||||
border
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<ElTableColumn
|
||||
v-if="!props.readonly"
|
||||
type="selection"
|
||||
width="48"
|
||||
align="center"
|
||||
:selectable="(row: Api.Product.ProductMember) => isRowSelectable(row)"
|
||||
/>
|
||||
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
||||
<ElTableColumn prop="userNickname" label="成员姓名" min-width="140" />
|
||||
<ElTableColumn label="当前角色" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="setting-team-panel__role-cell">
|
||||
<span class="setting-team-panel__role-main">{{ row.roleName || '--' }}</span>
|
||||
<ElTag
|
||||
v-for="extra in row.additionalRoleNames"
|
||||
:key="extra"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
class="setting-team-panel__role-extra"
|
||||
>
|
||||
{{ extra }}
|
||||
</ElTag>
|
||||
</div>
|
||||
{{ row.roleName || '--' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="成员状态" width="110" align="center">
|
||||
@@ -196,15 +229,31 @@ function getMemberStatusTagType(status: Api.Product.ProductMemberStatus) {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-cell {
|
||||
display: inline-flex;
|
||||
.setting-team-panel__role-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-extra {
|
||||
font-weight: 400;
|
||||
.setting-team-panel__role-option-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-option-info {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-option-info:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
|
||||
1157
src/views/product/shared/components/product-team-batch-dialog.vue
Normal file
1157
src/views/product/shared/components/product-team-batch-dialog.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, reactive, watch } from 'vue';
|
||||
import { PROJECT_MANAGER_ROLE_CODE } from '@/constants/business';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
import BusinessUserSelect from '@/components/custom/business-user-select.vue';
|
||||
|
||||
defineOptions({ name: 'ProjectCreateTeamMemberDialog' });
|
||||
|
||||
type OperateMode = 'create' | 'edit';
|
||||
|
||||
interface DraftMemberInput {
|
||||
userId: string;
|
||||
roleId: string;
|
||||
@@ -16,22 +12,16 @@ interface DraftMemberInput {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
mode: OperateMode;
|
||||
initial: DraftMemberInput | null;
|
||||
userOptions: Api.SystemManage.UserSimple[];
|
||||
roleOptions: Api.SystemManage.RoleSimple[];
|
||||
/** 已使用且不可选的 userId(编辑模式应当排除当前行自身) */
|
||||
disabledUserIds?: readonly string[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', payload: DraftMemberInput): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
disabledUserIds: () => []
|
||||
});
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', { default: false });
|
||||
@@ -39,31 +29,19 @@ const visible = defineModel<boolean>('visible', { default: false });
|
||||
const { formRef, validate } = useForm();
|
||||
const { createRequiredRule } = useFormRules();
|
||||
|
||||
const model = reactive<DraftMemberInput>({
|
||||
userId: '',
|
||||
roleId: '',
|
||||
remark: ''
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => (props.mode === 'create' ? '新增团队成员' : '调整成员角色'));
|
||||
const model = reactive<DraftMemberInput>({ userId: '', roleId: '', remark: '' });
|
||||
|
||||
const userLabelMap = computed(() => new Map(props.userOptions.map(item => [String(item.id), item.nickname])));
|
||||
|
||||
const rules = computed(
|
||||
() =>
|
||||
({
|
||||
userId: props.mode === 'create' ? [createRequiredRule('请选择成员用户')] : [],
|
||||
roleId: [createRequiredRule('请选择角色')]
|
||||
}) satisfies Record<string, App.Global.FormRule[]>
|
||||
);
|
||||
|
||||
function isManagerRole(role: Api.SystemManage.RoleSimple) {
|
||||
return role.code === PROJECT_MANAGER_ROLE_CODE;
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
await validate();
|
||||
|
||||
emit('submit', {
|
||||
userId: model.userId,
|
||||
roleId: model.roleId,
|
||||
@@ -72,34 +50,21 @@ async function handleConfirm() {
|
||||
}
|
||||
|
||||
watch(visible, async value => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value) return;
|
||||
model.userId = props.initial?.userId || '';
|
||||
model.roleId = props.initial?.roleId || '';
|
||||
model.remark = props.initial?.remark || '';
|
||||
|
||||
await nextTick();
|
||||
formRef.value?.clearValidate();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog v-model="visible" :title="dialogTitle" preset="sm" @confirm="handleConfirm">
|
||||
<BusinessFormDialog v-model="visible" title="调整成员角色" preset="sm" @confirm="handleConfirm">
|
||||
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top" :validate-on-rule-change="false">
|
||||
<ElRow :gutter="16">
|
||||
<ElCol :span="24">
|
||||
<ElFormItem v-if="mode === 'create'" label="成员用户" prop="userId">
|
||||
<BusinessUserSelect
|
||||
v-model="model.userId"
|
||||
:options="userOptions"
|
||||
:disabled-user-ids="disabledUserIds"
|
||||
disabled-label="已添加"
|
||||
placeholder="请选择成员用户"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-else label="成员用户">
|
||||
<ElFormItem label="成员用户">
|
||||
<ElInput
|
||||
:model-value="userLabelMap.get(String(model.userId)) || ''"
|
||||
readonly
|
||||
@@ -111,18 +76,7 @@ watch(visible, async value => {
|
||||
<ElCol :span="24">
|
||||
<ElFormItem label="目标角色" prop="roleId">
|
||||
<ElSelect v-model="model.roleId" class="w-full" placeholder="请选择角色">
|
||||
<ElOption
|
||||
v-for="role in roleOptions"
|
||||
:key="role.id"
|
||||
:label="role.name"
|
||||
:value="role.id"
|
||||
:disabled="isManagerRole(role)"
|
||||
>
|
||||
<span>{{ role.name }}</span>
|
||||
<span v-if="isManagerRole(role)" class="project-create-team-member-dialog__role-hint">
|
||||
(已由第 1 步指定)
|
||||
</span>
|
||||
</ElOption>
|
||||
<ElOption v-for="role in roleOptions" :key="role.id" :label="role.name" :value="role.id" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
@@ -144,12 +98,6 @@ watch(visible, async value => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.project-create-team-member-dialog__role-hint {
|
||||
margin-left: 8px;
|
||||
color: rgb(148 163 184 / 96%);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.project-create-team-member-dialog__readonly-input .el-input__wrapper) {
|
||||
background: linear-gradient(180deg, rgb(241 245 249 / 98%), rgb(226 232 240 / 94%)), rgb(241 245 249);
|
||||
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { PROJECT_MANAGER_ROLE_CODE } from '@/constants/business';
|
||||
import { fetchGetRoleSimpleList } from '@/service/api';
|
||||
import { getProjectTeamTableHeight } from '@/views/project/project/setting/shared';
|
||||
import ProjectTeamBatchDialog, {
|
||||
type BatchMemberPayload
|
||||
} from '@/views/project/shared/components/project-team-batch-dialog.vue';
|
||||
import ProjectCreateTeamMemberDialog from './project-create-team-member-dialog.vue';
|
||||
import type { ProjectCreateBaseForm } from './project-create-base-form.vue';
|
||||
|
||||
@@ -21,57 +22,34 @@ interface DraftMember {
|
||||
interface Props {
|
||||
baseInfo: ProjectCreateBaseForm;
|
||||
userOptions: Api.SystemManage.UserSimple[];
|
||||
roleOptions: Api.SystemManage.RoleSimple[];
|
||||
roleLoading: boolean;
|
||||
managerRoleError: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:members', members: Api.Project.CreateProjectMemberParams[]): void;
|
||||
(e: 'update:watcherUserIds', watcherUserIds: string[]): void;
|
||||
}>();
|
||||
|
||||
const roleOptions = ref<Api.SystemManage.RoleSimple[]>([]);
|
||||
const roleLoading = ref(false);
|
||||
const managerRoleError = ref('');
|
||||
const members = ref<DraftMember[]>([]);
|
||||
|
||||
const memberDialogVisible = ref(false);
|
||||
const memberDialogMode = ref<'create' | 'edit'>('create');
|
||||
const editingKey = ref<string | null>(null);
|
||||
|
||||
const watcherUserIds = ref<string[]>([]);
|
||||
const batchDialogVisible = ref(false);
|
||||
|
||||
// 关心人候选用户:排除已在团队成员列表中的用户(包含项目负责人本人)
|
||||
const watcherUserOptions = computed(() => {
|
||||
const memberUserIds = new Set(members.value.map(item => item.userId).filter(Boolean));
|
||||
return props.userOptions.filter(user => !memberUserIds.has(user.id));
|
||||
});
|
||||
|
||||
const teamTableHeight = getProjectTeamTableHeight(4);
|
||||
const batchDisabledUserIds = computed(() => members.value.map(item => item.userId).filter(Boolean));
|
||||
|
||||
const userLabelMap = computed(() => new Map(props.userOptions.map(item => [String(item.id), item.nickname])));
|
||||
|
||||
const managerRole = computed(() => roleOptions.value.find(item => item.code === PROJECT_MANAGER_ROLE_CODE) ?? null);
|
||||
|
||||
// 弹框传入的禁选用户列表:新增时排除所有已选;编辑时排除自身以外的已选
|
||||
const dialogDisabledUserIds = computed(() => {
|
||||
return members.value
|
||||
.filter(item => !editingKey.value || item.key !== editingKey.value)
|
||||
.map(item => item.userId)
|
||||
.filter(Boolean);
|
||||
});
|
||||
const managerRole = computed(() => props.roleOptions.find(item => item.code === PROJECT_MANAGER_ROLE_CODE) ?? null);
|
||||
|
||||
const dialogInitial = computed(() => {
|
||||
if (memberDialogMode.value === 'create' || !editingKey.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!editingKey.value) return null;
|
||||
const target = members.value.find(item => item.key === editingKey.value);
|
||||
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!target) return null;
|
||||
return { userId: target.userId, roleId: target.roleId, remark: target.remark };
|
||||
});
|
||||
|
||||
@@ -80,31 +58,13 @@ function getUserNickname(userId: string) {
|
||||
}
|
||||
|
||||
function getRoleName(roleId: string) {
|
||||
return roleOptions.value.find(item => item.id === roleId)?.name || '--';
|
||||
return props.roleOptions.find(item => item.id === roleId)?.name || '--';
|
||||
}
|
||||
|
||||
function generateKey() {
|
||||
return `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
async function loadRoles() {
|
||||
roleLoading.value = true;
|
||||
managerRoleError.value = '';
|
||||
|
||||
const { data } = await fetchGetRoleSimpleList({ scopeType: 'object', objectType: 'project' });
|
||||
|
||||
roleLoading.value = false;
|
||||
|
||||
roleOptions.value = data ?? [];
|
||||
|
||||
if (!managerRole.value) {
|
||||
managerRoleError.value = '未找到项目经理角色,请联系管理员';
|
||||
return;
|
||||
}
|
||||
|
||||
refreshManagerRow();
|
||||
}
|
||||
|
||||
function refreshManagerRow() {
|
||||
const managerUserId = props.baseInfo.managerUserId;
|
||||
|
||||
@@ -132,14 +92,25 @@ function refreshManagerRow() {
|
||||
emitMembers();
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
memberDialogMode.value = 'create';
|
||||
editingKey.value = null;
|
||||
memberDialogVisible.value = true;
|
||||
function openBatch() {
|
||||
batchDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleBatchSubmit(payloads: BatchMemberPayload[]) {
|
||||
for (const p of payloads) {
|
||||
members.value.push({
|
||||
key: generateKey(),
|
||||
userId: p.userId,
|
||||
roleId: p.roleId,
|
||||
remark: p.remark,
|
||||
locked: false
|
||||
});
|
||||
}
|
||||
batchDialogVisible.value = false;
|
||||
emitMembers();
|
||||
}
|
||||
|
||||
function openEdit(row: DraftMember) {
|
||||
memberDialogMode.value = 'edit';
|
||||
editingKey.value = row.key;
|
||||
memberDialogVisible.value = true;
|
||||
}
|
||||
@@ -149,27 +120,16 @@ function removeMember(key: string) {
|
||||
emitMembers();
|
||||
}
|
||||
|
||||
function handleMemberSubmit(payload: { userId: string; roleId: string; remark: string }) {
|
||||
if (memberDialogMode.value === 'create') {
|
||||
members.value.push({
|
||||
key: generateKey(),
|
||||
userId: payload.userId,
|
||||
function handleMemberEditSubmit(payload: { userId: string; roleId: string; remark: string }) {
|
||||
if (!editingKey.value) return;
|
||||
const idx = members.value.findIndex(item => item.key === editingKey.value);
|
||||
if (idx >= 0) {
|
||||
members.value[idx] = {
|
||||
...members.value[idx],
|
||||
roleId: payload.roleId,
|
||||
remark: payload.remark,
|
||||
locked: false
|
||||
});
|
||||
} else if (editingKey.value) {
|
||||
const idx = members.value.findIndex(item => item.key === editingKey.value);
|
||||
|
||||
if (idx >= 0) {
|
||||
members.value[idx] = {
|
||||
...members.value[idx],
|
||||
roleId: payload.roleId,
|
||||
remark: payload.remark
|
||||
};
|
||||
}
|
||||
remark: payload.remark
|
||||
};
|
||||
}
|
||||
|
||||
memberDialogVisible.value = false;
|
||||
emitMembers();
|
||||
}
|
||||
@@ -188,8 +148,8 @@ function emitMembers() {
|
||||
}
|
||||
|
||||
async function runValidate(): Promise<boolean> {
|
||||
if (managerRoleError.value) {
|
||||
window.$message?.error(managerRoleError.value);
|
||||
if (props.managerRoleError) {
|
||||
window.$message?.error(props.managerRoleError);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -214,43 +174,32 @@ async function runValidate(): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleWatcherChange(ids: string[]) {
|
||||
watcherUserIds.value = ids;
|
||||
emit('update:watcherUserIds', ids);
|
||||
}
|
||||
|
||||
// 团队成员变化时,剔除已被加入团队的关心人,避免重叠
|
||||
watch(
|
||||
() => members.value.map(item => item.userId).join(','),
|
||||
() => {
|
||||
const memberUserIds = new Set(members.value.map(item => item.userId).filter(Boolean));
|
||||
const filtered = watcherUserIds.value.filter(id => !memberUserIds.has(id));
|
||||
|
||||
if (filtered.length !== watcherUserIds.value.length) {
|
||||
handleWatcherChange(filtered);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(loadRoles);
|
||||
|
||||
watch(
|
||||
() => props.baseInfo.managerUserId,
|
||||
() => {
|
||||
if (!managerRoleError.value && managerRole.value) {
|
||||
if (!props.managerRoleError && managerRole.value) {
|
||||
refreshManagerRow();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// roleOptions 异步加载到位后,补一次 locked 行刷新
|
||||
watch(managerRole, () => {
|
||||
if (!props.managerRoleError && managerRole.value && props.baseInfo.managerUserId) {
|
||||
refreshManagerRow();
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({ validate: runValidate });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="roleLoading" class="team-step">
|
||||
<div class="team-step__toolbar">
|
||||
<ElButton type="primary" plain :disabled="Boolean(managerRoleError)" @click="openCreate">新增成员</ElButton>
|
||||
</div>
|
||||
<button type="button" class="team-step__add" :disabled="Boolean(managerRoleError)" @click="openBatch">
|
||||
<icon-ep:plus class="team-step__add-icon" />
|
||||
<span>新增成员</span>
|
||||
<span class="team-step__add-hint">从部门 / 管理链路 / 全公司 批量选人</span>
|
||||
</button>
|
||||
|
||||
<ElAlert
|
||||
v-if="managerRoleError"
|
||||
@@ -261,62 +210,49 @@ defineExpose({ validate: runValidate });
|
||||
class="team-step__alert"
|
||||
/>
|
||||
|
||||
<ElTable :data="members" :height="teamTableHeight" border row-key="key" empty-text="点击右上角“新增成员”添加">
|
||||
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
||||
<ElTableColumn label="成员姓名" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getUserNickname(row.userId) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="当前角色" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ getRoleName(row.roleId) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="备注" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.remark || '--' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="team-step__actions">
|
||||
<ElButton link type="primary" :disabled="row.locked" @click="openEdit(row)">编辑</ElButton>
|
||||
<ElButton link type="danger" :disabled="row.locked" @click="removeMember(row.key)">移除</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<div class="watcher-row">
|
||||
<span class="watcher-row__label">
|
||||
关心人
|
||||
<span class="watcher-row__optional">(选填)</span>
|
||||
</span>
|
||||
<ElSelect
|
||||
:model-value="watcherUserIds"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:max-collapse-tags="3"
|
||||
placeholder="可在列表 / 概览看到此项目的关注人"
|
||||
class="watcher-row__select"
|
||||
@update:model-value="handleWatcherChange"
|
||||
>
|
||||
<ElOption v-for="item in watcherUserOptions" :key="item.id" :value="item.id" :label="item.nickname" />
|
||||
</ElSelect>
|
||||
<div class="team-step__table-wrap">
|
||||
<ElTable :data="members" height="100%" border row-key="key" empty-text="点击右上角“新增成员”添加">
|
||||
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
||||
<ElTableColumn label="成员姓名" min-width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getUserNickname(row.userId) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="当前角色" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ getRoleName(row.roleId) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="备注" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
{{ row.remark || '--' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="150" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<div class="team-step__actions">
|
||||
<ElButton link type="primary" :disabled="row.locked" @click="openEdit(row)">编辑</ElButton>
|
||||
<ElButton link type="danger" :disabled="row.locked" @click="removeMember(row.key)">移除</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</div>
|
||||
|
||||
<ProjectCreateTeamMemberDialog
|
||||
v-model:visible="memberDialogVisible"
|
||||
:mode="memberDialogMode"
|
||||
:initial="dialogInitial"
|
||||
:user-options="userOptions"
|
||||
:role-options="roleOptions"
|
||||
:disabled-user-ids="dialogDisabledUserIds"
|
||||
@submit="handleMemberSubmit"
|
||||
@submit="handleMemberEditSubmit"
|
||||
/>
|
||||
|
||||
<ProjectTeamBatchDialog
|
||||
v-model:visible="batchDialogVisible"
|
||||
:user-options="userOptions"
|
||||
:role-options="roleOptions"
|
||||
:disabled-user-ids="batchDisabledUserIds"
|
||||
@submit="handleBatchSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -326,16 +262,65 @@ defineExpose({ validate: runValidate });
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.team-step__toolbar {
|
||||
.team-step__add {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
padding: 0 16px;
|
||||
border: 1px dashed var(--el-border-color-darker);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
color 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.team-step__add:hover:not(:disabled) {
|
||||
border-color: var(--el-color-primary);
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
.team-step__add:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.team-step__add-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.team-step__add-hint {
|
||||
color: var(--el-text-color-placeholder);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.team-step__add:hover:not(:disabled) .team-step__add-hint {
|
||||
color: var(--el-color-primary);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.team-step__alert {
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.team-step__table-wrap {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.team-step__actions {
|
||||
@@ -343,27 +328,4 @@ defineExpose({ validate: runValidate });
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.watcher-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.watcher-row__label {
|
||||
flex: 0 0 auto;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: rgb(60 70 95 / 96%);
|
||||
}
|
||||
|
||||
.watcher-row__optional {
|
||||
color: rgb(140 150 170 / 96%);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.watcher-row__select {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { ElCol, ElDatePicker, ElFormItem, ElInput, ElRow } from 'element-plus';
|
||||
import dayjs from 'dayjs';
|
||||
import { PROJECT_MANAGER_ROLE_CODE } from '@/constants/business';
|
||||
import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_PROJECT_TYPE_DICT_CODE } from '@/constants/dict';
|
||||
import { fetchCreateProjectWithTeam, fetchGetProductPage, fetchUpdateProject } from '@/service/api';
|
||||
import {
|
||||
fetchCreateProjectWithTeam,
|
||||
fetchGetProductPage,
|
||||
fetchGetRoleSimpleList,
|
||||
fetchUpdateProject
|
||||
} from '@/service/api';
|
||||
import { useDict } from '@/hooks/business/dict';
|
||||
import { useForm, useFormRules } from '@/hooks/common/form';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
@@ -265,9 +271,29 @@ const baseFormRef = ref<InstanceType<typeof ProjectCreateBaseForm> | null>(null)
|
||||
const teamStepRef = ref<InstanceType<typeof ProjectCreateTeamStep> | null>(null);
|
||||
const currentStep = ref<1 | 2>(1);
|
||||
|
||||
// === 新增模式:角色列表(父级加载,下发给 team-step 与批量弹层) ===
|
||||
const roleOptions = ref<Api.SystemManage.RoleSimple[]>([]);
|
||||
const roleLoading = ref(false);
|
||||
const managerRoleError = ref('');
|
||||
|
||||
const managerRole = computed(() => roleOptions.value.find(item => item.code === PROJECT_MANAGER_ROLE_CODE) ?? null);
|
||||
|
||||
async function loadRoles() {
|
||||
roleLoading.value = true;
|
||||
managerRoleError.value = '';
|
||||
|
||||
const { data } = await fetchGetRoleSimpleList({ scopeType: 'object', objectType: 'project' });
|
||||
|
||||
roleLoading.value = false;
|
||||
roleOptions.value = data ?? [];
|
||||
|
||||
if (!managerRole.value) {
|
||||
managerRoleError.value = '未找到项目经理角色,请联系管理员';
|
||||
}
|
||||
}
|
||||
|
||||
const createBaseModel = ref<ProjectCreateBaseFormModel>(createBaseInfo());
|
||||
const draftMembers = ref<Api.Project.CreateProjectMemberParams[]>([]);
|
||||
const draftWatcherUserIds = ref<string[]>([]);
|
||||
|
||||
function createBaseInfo(): ProjectCreateBaseFormModel {
|
||||
return {
|
||||
@@ -325,8 +351,7 @@ async function handleCreateSubmit() {
|
||||
plannedEndDate: createBaseModel.value.plannedEndDate,
|
||||
projectDesc: getNullableText(createBaseModel.value.projectDesc)
|
||||
},
|
||||
members: draftMembers.value,
|
||||
watcherUserIds: draftWatcherUserIds.value.length > 0 ? draftWatcherUserIds.value : undefined
|
||||
members: draftMembers.value
|
||||
};
|
||||
|
||||
const { error, data } = await fetchCreateProjectWithTeam(payload);
|
||||
@@ -355,8 +380,8 @@ watch(visible, async value => {
|
||||
editModel.value = createEditModel();
|
||||
createBaseModel.value = createBaseInfo();
|
||||
draftMembers.value = [];
|
||||
draftWatcherUserIds.value = [];
|
||||
await nextTick();
|
||||
await loadRoles();
|
||||
editFormRef.value?.clearValidate();
|
||||
return;
|
||||
}
|
||||
@@ -521,7 +546,7 @@ watch(visible, async value => {
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
|
||||
<!-- 新增模式:两步向导(复合内容特例,自定义 ElDialog 880px) -->
|
||||
<!-- 新增模式:两步向导(复合内容特例,1080px,左侧概念区 + 右侧步骤面板) -->
|
||||
<ElDialog
|
||||
v-else
|
||||
v-model="visible"
|
||||
@@ -530,37 +555,79 @@ watch(visible, async value => {
|
||||
:close-on-click-modal="false"
|
||||
destroy-on-close
|
||||
align-center
|
||||
width="760px"
|
||||
width="1080px"
|
||||
>
|
||||
<div class="project-create-dialog__stepbar">
|
||||
<div class="project-create-dialog__step" :class="{ 'is-active': currentStep === 1, 'is-done': currentStep > 1 }">
|
||||
<span class="project-create-dialog__step-index">1</span>
|
||||
<span class="project-create-dialog__step-text">
|
||||
<strong>基础资料</strong>
|
||||
<small>定义项目身份和负责人</small>
|
||||
</span>
|
||||
</div>
|
||||
<div class="project-create-dialog__step" :class="{ 'is-active': currentStep === 2 }">
|
||||
<span class="project-create-dialog__step-index">2</span>
|
||||
<span class="project-create-dialog__step-text">
|
||||
<strong>初始化团队</strong>
|
||||
<small>配置对象域成员角色</small>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-create-dialog__split">
|
||||
<aside class="project-create-dialog__guide">
|
||||
<div class="project-create-dialog__guide-hero">
|
||||
<div class="project-create-dialog__guide-hero-icon">
|
||||
<icon-ep:files />
|
||||
</div>
|
||||
<div class="project-create-dialog__guide-hero-text">
|
||||
<div class="project-create-dialog__guide-hero-title">项目</div>
|
||||
<div class="project-create-dialog__guide-hero-sub">需求 · 任务 · 交付 的协同载体</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-create-dialog__body">
|
||||
<div v-show="currentStep === 1" class="project-create-dialog__panel">
|
||||
<ProjectCreateBaseForm ref="baseFormRef" v-model="createBaseModel" :manager-user-options="managerUserOptions" />
|
||||
</div>
|
||||
<div v-show="currentStep === 2" class="project-create-dialog__panel">
|
||||
<ProjectCreateTeamStep
|
||||
ref="teamStepRef"
|
||||
:base-info="createBaseModel"
|
||||
:user-options="managerUserOptions"
|
||||
@update:members="draftMembers = $event"
|
||||
@update:watcher-user-ids="draftWatcherUserIds = $event"
|
||||
/>
|
||||
<p class="project-create-dialog__guide-lead">
|
||||
项目帮助组织持续规划和交付业务价值,围绕计划周期、负责人和团队推进研发工作,串起需求、任务、缺陷和成果。
|
||||
</p>
|
||||
|
||||
<section class="project-create-dialog__guide-section">
|
||||
<h4>包含</h4>
|
||||
<p>需求、执行、任务、工作日志、缺陷、文档、统计报表。</p>
|
||||
</section>
|
||||
|
||||
<section class="project-create-dialog__guide-section">
|
||||
<h4>参与人</h4>
|
||||
<p>项目经理(必填,创建后锁定) · 团队角色(开发 / 测试 / 业务 / 关注人)。</p>
|
||||
</section>
|
||||
|
||||
<section class="project-create-dialog__guide-section">
|
||||
<h4>命名建议</h4>
|
||||
<p>建议使用业务团队约定俗成的简短名称,关联所属产品后会继承产品方向,影响下游归类。</p>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<div class="project-create-dialog__main">
|
||||
<div class="project-create-dialog__stepbar">
|
||||
<div
|
||||
class="project-create-dialog__step"
|
||||
:class="{ 'is-active': currentStep === 1, 'is-done': currentStep > 1 }"
|
||||
>
|
||||
<span class="project-create-dialog__step-index">1</span>
|
||||
<span class="project-create-dialog__step-text">
|
||||
<strong>基础资料</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="project-create-dialog__step" :class="{ 'is-active': currentStep === 2 }">
|
||||
<span class="project-create-dialog__step-index">2</span>
|
||||
<span class="project-create-dialog__step-text">
|
||||
<strong>初始化团队</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="project-create-dialog__body">
|
||||
<div v-show="currentStep === 1" class="project-create-dialog__panel">
|
||||
<ProjectCreateBaseForm
|
||||
ref="baseFormRef"
|
||||
v-model="createBaseModel"
|
||||
:manager-user-options="managerUserOptions"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="currentStep === 2" class="project-create-dialog__panel">
|
||||
<ProjectCreateTeamStep
|
||||
ref="teamStepRef"
|
||||
:base-info="createBaseModel"
|
||||
:user-options="managerUserOptions"
|
||||
:role-options="roleOptions"
|
||||
:role-loading="roleLoading"
|
||||
:manager-role-error="managerRoleError"
|
||||
@update:members="draftMembers = $event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -608,6 +675,92 @@ watch(visible, async value => {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.project-create-dialog__split {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.project-create-dialog__guide {
|
||||
padding: 28px 24px;
|
||||
background: linear-gradient(180deg, #f7f9fc 0%, #fafbfc 100%);
|
||||
border-right: 1px solid rgb(229 233 242 / 96%);
|
||||
overflow-y: auto;
|
||||
max-height: min(720px, calc(100vh - 160px));
|
||||
}
|
||||
|
||||
.project-create-dialog__guide-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid rgb(229 233 242 / 96%);
|
||||
}
|
||||
|
||||
.project-create-dialog__guide-hero-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
font-size: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.project-create-dialog__guide-hero-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.project-create-dialog__guide-hero-title {
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
color: var(--el-text-color-primary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.project-create-dialog__guide-hero-sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.project-create-dialog__guide-lead {
|
||||
margin: 0 0 24px;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.8;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.project-create-dialog__guide-section + .project-create-dialog__guide-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.project-create-dialog__guide-section h4 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 700;
|
||||
color: var(--el-text-color-primary);
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
.project-create-dialog__guide-section p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.75;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.project-create-dialog__main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.project-create-dialog__stepbar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -663,13 +816,15 @@ watch(visible, async value => {
|
||||
}
|
||||
|
||||
.project-create-dialog__body {
|
||||
min-height: 0;
|
||||
max-height: min(560px, calc(100vh - 240px));
|
||||
overflow: auto;
|
||||
height: min(520px, calc(100vh - 240px));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.project-create-dialog__panel {
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.project-create-dialog__footer {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useMediaQuery } from '@vueuse/core';
|
||||
import { LAYOUT_SCROLL_EL_ID } from '@sa/materials';
|
||||
import { objectContextDomainConfigs } from '@/constants/object-context';
|
||||
import {
|
||||
fetchBatchCreateProjectMembers,
|
||||
fetchBatchInactiveProjectMembers,
|
||||
fetchChangeProjectStatus,
|
||||
fetchCreateProjectMember,
|
||||
fetchDeleteProject,
|
||||
@@ -18,8 +20,12 @@ import {
|
||||
import { useObjectContextStore } from '@/store/modules/object-context';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { useRouterPush } from '@/hooks/common/router';
|
||||
import ProjectTeamBatchDialog, {
|
||||
type BatchMemberPayload
|
||||
} from '@/views/project/shared/components/project-team-batch-dialog.vue';
|
||||
import { useCurrentProject } from '../../shared/use-current-project';
|
||||
import BaseInfoDialog from './modules/base-info-dialog.vue';
|
||||
import MemberBatchRemoveDialog from './modules/member-batch-remove-dialog.vue';
|
||||
import MemberOperateDialog from './modules/member-operate-dialog.vue';
|
||||
import MemberRemoveDialog from './modules/member-remove-dialog.vue';
|
||||
import ProjectDeleteDialog from './modules/project-delete-dialog.vue';
|
||||
@@ -68,7 +74,11 @@ const pageLoading = ref(false);
|
||||
const memberLoading = ref(false);
|
||||
const baseInfoVisible = ref(false);
|
||||
const memberOperateVisible = ref(false);
|
||||
const memberBatchVisible = ref(false);
|
||||
const memberRemoveVisible = ref(false);
|
||||
const memberBatchRemoveVisible = ref(false);
|
||||
const teamPanelRef = ref<InstanceType<typeof SettingTeamPanel> | null>(null);
|
||||
const selectedBatchRemoveMembers = ref<Api.Project.ProjectMember[]>([]);
|
||||
const statusActionVisible = ref(false);
|
||||
const deleteVisible = ref(false);
|
||||
const memberOperateMode = ref<'create' | 'edit'>('create');
|
||||
@@ -211,9 +221,7 @@ function scrollToSection(key: string) {
|
||||
}
|
||||
|
||||
function openCreateMember() {
|
||||
memberOperateMode.value = 'create';
|
||||
selectedMember.value = null;
|
||||
memberOperateVisible.value = true;
|
||||
memberBatchVisible.value = true;
|
||||
}
|
||||
|
||||
function openEditMember(member: Api.Project.ProjectMember) {
|
||||
@@ -227,6 +235,12 @@ function openRemoveMember(member: Api.Project.ProjectMember) {
|
||||
memberRemoveVisible.value = true;
|
||||
}
|
||||
|
||||
function openBatchRemoveMember(targetMembers: Api.Project.ProjectMember[]) {
|
||||
if (!targetMembers.length) return;
|
||||
selectedBatchRemoveMembers.value = targetMembers;
|
||||
memberBatchRemoveVisible.value = true;
|
||||
}
|
||||
|
||||
function openLifecycleAction(action: Api.Project.ProjectLifecycleAction) {
|
||||
selectedAction.value = action;
|
||||
statusActionVisible.value = true;
|
||||
@@ -299,6 +313,53 @@ async function handleSubmitRemoveMember(payload: Api.Project.InactiveProjectMemb
|
||||
await Promise.all([loadMembers(), loadSettings()]);
|
||||
}
|
||||
|
||||
async function handleSubmitMemberBatch(payloads: BatchMemberPayload[]) {
|
||||
if (!currentObjectId.value || !payloads.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { error } = await fetchBatchCreateProjectMembers(currentObjectId.value, {
|
||||
members: payloads.map(item => ({
|
||||
userId: item.userId,
|
||||
roleId: item.roleId,
|
||||
remark: item.remark.trim() || null
|
||||
}))
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.$message?.success(`已新增 ${payloads.length} 名成员`);
|
||||
memberBatchVisible.value = false;
|
||||
|
||||
await Promise.all([loadMembers(), loadSettings()]);
|
||||
}
|
||||
|
||||
async function handleSubmitBatchRemoveMember(payload: { reason: string | null }) {
|
||||
if (!currentObjectId.value || !selectedBatchRemoveMembers.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const memberIds = selectedBatchRemoveMembers.value.map(item => item.id).filter((id): id is string => Boolean(id));
|
||||
|
||||
if (!memberIds.length) return;
|
||||
|
||||
const { error } = await fetchBatchInactiveProjectMembers(currentObjectId.value, {
|
||||
memberIds,
|
||||
reason: payload.reason
|
||||
});
|
||||
|
||||
if (error) return;
|
||||
|
||||
window.$message?.success(`已移出 ${memberIds.length} 名成员`);
|
||||
memberBatchRemoveVisible.value = false;
|
||||
selectedBatchRemoveMembers.value = [];
|
||||
teamPanelRef.value?.clearSelection();
|
||||
|
||||
await Promise.all([loadMembers(), loadSettings()]);
|
||||
}
|
||||
|
||||
async function handleSubmitLifecycleAction(payload: Api.Project.ChangeProjectStatusParams) {
|
||||
if (!currentObjectId.value || !selectedAction.value) {
|
||||
return;
|
||||
@@ -387,6 +448,7 @@ watch(
|
||||
|
||||
<section :id="sectionIdMap.team" class="project-setting-page__section">
|
||||
<SettingTeamPanel
|
||||
ref="teamPanelRef"
|
||||
:members="members"
|
||||
:role-options="roleOptions"
|
||||
:loading="memberLoading"
|
||||
@@ -394,6 +456,7 @@ watch(
|
||||
@create="openCreateMember"
|
||||
@edit="openEditMember"
|
||||
@remove="openRemoveMember"
|
||||
@batch-remove="openBatchRemoveMember"
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -432,6 +495,18 @@ watch(
|
||||
:member="selectedMember"
|
||||
@submit="handleSubmitRemoveMember"
|
||||
/>
|
||||
<ProjectTeamBatchDialog
|
||||
v-model:visible="memberBatchVisible"
|
||||
:user-options="userOptions"
|
||||
:role-options="roleOptions"
|
||||
:disabled-user-ids="members.filter(member => member.status === 0).map(member => member.userId)"
|
||||
@submit="handleSubmitMemberBatch"
|
||||
/>
|
||||
<MemberBatchRemoveDialog
|
||||
v-model:visible="memberBatchRemoveVisible"
|
||||
:members="selectedBatchRemoveMembers"
|
||||
@submit="handleSubmitBatchRemoveMember"
|
||||
/>
|
||||
<StatusActionDialog
|
||||
v-model:visible="statusActionVisible"
|
||||
:action="selectedAction"
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
|
||||
|
||||
defineOptions({ name: 'ProjectMemberBatchRemoveDialog' });
|
||||
|
||||
interface Props {
|
||||
members: Api.Project.ProjectMember[];
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'submit', payload: { reason: string | null }): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const visible = defineModel<boolean>('visible', {
|
||||
default: false
|
||||
});
|
||||
|
||||
const model = reactive({
|
||||
reason: ''
|
||||
});
|
||||
|
||||
const previewNames = computed(() => {
|
||||
const names = props.members.map(item => item.userNickname || item.userId || '').filter(Boolean);
|
||||
|
||||
if (names.length <= 5) {
|
||||
return names.join('、');
|
||||
}
|
||||
|
||||
return `${names.slice(0, 5).join('、')} 等 ${names.length} 人`;
|
||||
});
|
||||
|
||||
function handleConfirm() {
|
||||
emit('submit', {
|
||||
reason: model.reason.trim() || null
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
value => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.reason = '';
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BusinessFormDialog v-model="visible" title="批量移出成员" preset="sm" @confirm="handleConfirm">
|
||||
<ElAlert
|
||||
:title="`确认将选中的 ${props.members.length} 名成员(${previewNames})从当前项目团队中移出吗?`"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
class="mb-12px"
|
||||
/>
|
||||
<ElAlert
|
||||
title="若任一成员仍担任未关闭执行的负责人,后端会整体拒绝;请先完成执行负责人交接后再批量移出。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="mb-16px"
|
||||
/>
|
||||
<ElForm label-position="top">
|
||||
<ElFormItem label="移出原因">
|
||||
<ElInput
|
||||
v-model="model.reason"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
placeholder="请输入移出原因(统一应用到所有选中成员)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</BusinessFormDialog>
|
||||
</template>
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { TableInstance } from 'element-plus';
|
||||
import { filterProjectMembers, formatProjectMemberDate, getProjectTeamTableHeight } from '../shared';
|
||||
|
||||
defineOptions({ name: 'ProjectSettingTeamPanel' });
|
||||
@@ -15,6 +16,7 @@ interface Emits {
|
||||
(e: 'create'): void;
|
||||
(e: 'edit', member: Api.Project.ProjectMember): void;
|
||||
(e: 'remove', member: Api.Project.ProjectMember): void;
|
||||
(e: 'batch-remove', members: Api.Project.ProjectMember[]): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -26,19 +28,41 @@ const emit = defineEmits<Emits>();
|
||||
const searchKeyword = ref('');
|
||||
const selectedRoleId = ref('');
|
||||
const teamTableHeight = getProjectTeamTableHeight(5);
|
||||
const tableRef = ref<TableInstance | null>(null);
|
||||
const selectedRows = ref<Api.Project.ProjectMember[]>([]);
|
||||
const selectedCount = computed(() => selectedRows.value.length);
|
||||
|
||||
function isRowSelectable(row: Api.Project.ProjectMember) {
|
||||
return row.status === 0 && !row.managerFlag;
|
||||
}
|
||||
|
||||
function handleSelectionChange(rows: Api.Project.ProjectMember[]) {
|
||||
selectedRows.value = rows;
|
||||
}
|
||||
|
||||
function handleBatchRemove() {
|
||||
if (!selectedRows.value.length) return;
|
||||
emit('batch-remove', [...selectedRows.value]);
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
tableRef.value?.clearSelection();
|
||||
selectedRows.value = [];
|
||||
}
|
||||
|
||||
defineExpose({ clearSelection });
|
||||
const roleFilterOptions = computed(() => {
|
||||
const roleMap = new Map<string, string>();
|
||||
const seen = new Set<string>();
|
||||
const result: Api.SystemManage.RoleSimple[] = [];
|
||||
|
||||
props.roleOptions.forEach(role => {
|
||||
if (!roleMap.has(role.id)) {
|
||||
roleMap.set(role.id, role.name);
|
||||
}
|
||||
if (role.visible === 0) return;
|
||||
if (seen.has(role.id)) return;
|
||||
seen.add(role.id);
|
||||
result.push(role);
|
||||
});
|
||||
|
||||
return [...roleMap.entries()].map(([value, label]) => ({
|
||||
value,
|
||||
label
|
||||
}));
|
||||
return result;
|
||||
});
|
||||
const filteredMembers = computed(() =>
|
||||
filterProjectMembers(props.members, {
|
||||
@@ -49,7 +73,7 @@ const filteredMembers = computed(() =>
|
||||
const hasFilter = computed(() => Boolean(searchKeyword.value.trim() || selectedRoleId.value));
|
||||
|
||||
watch(roleFilterOptions, options => {
|
||||
if (selectedRoleId.value && !options.some(item => item.value === selectedRoleId.value)) {
|
||||
if (selectedRoleId.value && !options.some(item => item.id === selectedRoleId.value)) {
|
||||
selectedRoleId.value = '';
|
||||
}
|
||||
});
|
||||
@@ -72,44 +96,46 @@ function getMemberStatusTagType(status: Api.Project.ProjectMemberStatus) {
|
||||
</div>
|
||||
<div class="setting-team-panel__toolbar">
|
||||
<ElSelect v-model="selectedRoleId" clearable placeholder="筛选角色" class="setting-team-panel__role-filter">
|
||||
<ElOption
|
||||
v-for="option in roleFilterOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
<ElOption v-for="role in roleFilterOptions" :key="role.id" :label="role.name" :value="role.id">
|
||||
<div class="setting-team-panel__role-option">
|
||||
<span class="setting-team-panel__role-option-name">{{ role.name }}</span>
|
||||
<ElTooltip v-if="role.remark" :content="role.remark" placement="right" :show-after="120">
|
||||
<icon-ep:info-filled class="setting-team-panel__role-option-info" @click.stop />
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</ElOption>
|
||||
</ElSelect>
|
||||
<ElInput v-model="searchKeyword" clearable placeholder="搜索成员姓名" class="setting-team-panel__search" />
|
||||
<ElButton type="primary" plain :disabled="props.readonly" @click="emit('create')">新增成员</ElButton>
|
||||
<ElButton type="danger" plain :disabled="props.readonly || selectedCount === 0" @click="handleBatchRemove">
|
||||
批量移出{{ selectedCount > 0 ? `(${selectedCount})` : '' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElTable
|
||||
ref="tableRef"
|
||||
v-loading="props.loading"
|
||||
:data="filteredMembers"
|
||||
:height="teamTableHeight"
|
||||
:empty-text="hasFilter ? '未找到匹配成员' : '暂无成员'"
|
||||
border
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<ElTableColumn
|
||||
v-if="!props.readonly"
|
||||
type="selection"
|
||||
width="48"
|
||||
align="center"
|
||||
:selectable="(row: Api.Project.ProjectMember) => isRowSelectable(row)"
|
||||
/>
|
||||
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
||||
<ElTableColumn prop="userNickname" label="成员姓名" min-width="140" />
|
||||
<ElTableColumn label="当前角色" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="setting-team-panel__role-cell">
|
||||
<span class="setting-team-panel__role-main">{{ row.roleName || '--' }}</span>
|
||||
<ElTag
|
||||
v-for="extra in row.additionalRoleNames"
|
||||
:key="extra"
|
||||
size="small"
|
||||
type="info"
|
||||
effect="plain"
|
||||
class="setting-team-panel__role-extra"
|
||||
>
|
||||
{{ extra }}
|
||||
</ElTag>
|
||||
</div>
|
||||
{{ row.roleName || '--' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="成员状态" width="110" align="center">
|
||||
@@ -186,15 +212,31 @@ function getMemberStatusTagType(status: Api.Project.ProjectMemberStatus) {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-cell {
|
||||
display: inline-flex;
|
||||
.setting-team-panel__role-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-extra {
|
||||
font-weight: 400;
|
||||
.setting-team-panel__role-option-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-option-info {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.setting-team-panel__role-option-info:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
|
||||
1157
src/views/project/shared/components/project-team-batch-dialog.vue
Normal file
1157
src/views/project/shared/components/project-team-batch-dialog.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user