refactor(projects): 1、优化新增 产品和新增项目;2、调整角色提示信息
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user