2026-04-23 09:05:55 +08:00
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, ref, watch } from 'vue';
|
2026-05-18 22:25:04 +08:00
|
|
|
|
import type { TableInstance } from 'element-plus';
|
2026-04-23 09:05:55 +08:00
|
|
|
|
import { filterProductMembers, formatProductMemberDate, getProductTeamTableHeight } from '../shared';
|
|
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'SettingTeamPanel' });
|
|
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
|
members: Api.Product.ProductMember[];
|
|
|
|
|
|
roleOptions?: Api.SystemManage.RoleSimple[];
|
|
|
|
|
|
loading?: boolean;
|
|
|
|
|
|
readonly?: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface Emits {
|
|
|
|
|
|
(e: 'create'): void;
|
|
|
|
|
|
(e: 'edit', member: Api.Product.ProductMember): void;
|
|
|
|
|
|
(e: 'remove', member: Api.Product.ProductMember): void;
|
2026-05-18 22:25:04 +08:00
|
|
|
|
(e: 'batch-remove', members: Api.Product.ProductMember[]): void;
|
2026-04-23 09:05:55 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
|
|
|
|
loading: false,
|
|
|
|
|
|
readonly: false,
|
|
|
|
|
|
roleOptions: () => []
|
|
|
|
|
|
});
|
|
|
|
|
|
const emit = defineEmits<Emits>();
|
|
|
|
|
|
const searchKeyword = ref('');
|
|
|
|
|
|
const selectedRoleId = ref('');
|
|
|
|
|
|
const teamTableHeight = getProductTeamTableHeight(5);
|
2026-05-18 22:25:04 +08:00
|
|
|
|
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 });
|
2026-04-23 09:05:55 +08:00
|
|
|
|
const roleFilterOptions = computed(() => {
|
2026-05-18 22:25:04 +08:00
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
|
const result: Api.SystemManage.RoleSimple[] = [];
|
2026-04-23 09:05:55 +08:00
|
|
|
|
|
|
|
|
|
|
props.roleOptions.forEach(role => {
|
2026-05-18 22:25:04 +08:00
|
|
|
|
if (role.visible === 0) return;
|
|
|
|
|
|
if (seen.has(role.id)) return;
|
|
|
|
|
|
seen.add(role.id);
|
|
|
|
|
|
result.push(role);
|
2026-04-23 09:05:55 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-18 22:25:04 +08:00
|
|
|
|
return result;
|
2026-04-23 09:05:55 +08:00
|
|
|
|
});
|
|
|
|
|
|
const filteredMembers = computed(() =>
|
|
|
|
|
|
filterProductMembers(props.members, {
|
|
|
|
|
|
keyword: searchKeyword.value,
|
|
|
|
|
|
roleId: selectedRoleId.value
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
|
|
|
|
|
const hasFilter = computed(() => Boolean(searchKeyword.value.trim() || selectedRoleId.value));
|
|
|
|
|
|
|
|
|
|
|
|
watch(roleFilterOptions, options => {
|
2026-05-18 22:25:04 +08:00
|
|
|
|
if (selectedRoleId.value && !options.some(item => item.id === selectedRoleId.value)) {
|
2026-04-23 09:05:55 +08:00
|
|
|
|
selectedRoleId.value = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function getMemberStatusLabel(status: Api.Product.ProductMemberStatus) {
|
|
|
|
|
|
return status === 0 ? '有效' : '失效';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function getMemberStatusTagType(status: Api.Product.ProductMemberStatus) {
|
|
|
|
|
|
return status === 0 ? 'success' : 'info';
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<ElCard class="card-wrapper">
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="setting-team-panel__header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 class="text-16px text-[#0f172a] font-700">团队管理</h3>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="setting-team-panel__toolbar">
|
|
|
|
|
|
<ElSelect v-model="selectedRoleId" clearable placeholder="筛选角色" class="setting-team-panel__role-filter">
|
2026-05-18 22:25:04 +08:00
|
|
|
|
<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>
|
2026-04-23 09:05:55 +08:00
|
|
|
|
</ElSelect>
|
|
|
|
|
|
<ElInput v-model="searchKeyword" clearable placeholder="搜索成员姓名" class="setting-team-panel__search" />
|
|
|
|
|
|
<ElButton
|
|
|
|
|
|
v-if="!props.readonly"
|
|
|
|
|
|
v-auth="{ code: 'project:product:update', source: 'object' }"
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
plain
|
|
|
|
|
|
@click="emit('create')"
|
|
|
|
|
|
>
|
|
|
|
|
|
新增成员
|
|
|
|
|
|
</ElButton>
|
2026-05-18 22:25:04 +08:00
|
|
|
|
<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>
|
2026-04-23 09:05:55 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<ElTable
|
2026-05-18 22:25:04 +08:00
|
|
|
|
ref="tableRef"
|
2026-04-23 09:05:55 +08:00
|
|
|
|
v-loading="props.loading"
|
|
|
|
|
|
:data="filteredMembers"
|
|
|
|
|
|
:height="teamTableHeight"
|
|
|
|
|
|
:empty-text="hasFilter ? '未找到匹配成员' : '暂无成员'"
|
|
|
|
|
|
border
|
|
|
|
|
|
row-key="id"
|
2026-05-18 22:25:04 +08:00
|
|
|
|
@selection-change="handleSelectionChange"
|
2026-04-23 09:05:55 +08:00
|
|
|
|
>
|
2026-05-18 22:25:04 +08:00
|
|
|
|
<ElTableColumn
|
|
|
|
|
|
v-if="!props.readonly"
|
|
|
|
|
|
type="selection"
|
|
|
|
|
|
width="48"
|
|
|
|
|
|
align="center"
|
|
|
|
|
|
:selectable="(row: Api.Product.ProductMember) => isRowSelectable(row)"
|
|
|
|
|
|
/>
|
2026-04-23 09:05:55 +08:00
|
|
|
|
<ElTableColumn type="index" label="序号" width="64" align="center" />
|
|
|
|
|
|
<ElTableColumn prop="userNickname" label="成员姓名" min-width="140" />
|
2026-05-14 14:11:16 +08:00
|
|
|
|
<ElTableColumn label="当前角色" min-width="180">
|
|
|
|
|
|
<template #default="{ row }">
|
2026-05-18 22:25:04 +08:00
|
|
|
|
{{ row.roleName || '--' }}
|
2026-05-14 14:11:16 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</ElTableColumn>
|
2026-04-23 09:05:55 +08:00
|
|
|
|
<ElTableColumn label="成员状态" width="110" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<ElTag :type="getMemberStatusTagType(row.status)">{{ getMemberStatusLabel(row.status) }}</ElTag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</ElTableColumn>
|
|
|
|
|
|
<ElTableColumn prop="joinedTime" label="加入时间" min-width="132" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ formatProductMemberDate(row.joinedTime) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</ElTableColumn>
|
|
|
|
|
|
<ElTableColumn prop="leftTime" label="退出时间" min-width="170">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ formatProductMemberDate(row.leftTime) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</ElTableColumn>
|
|
|
|
|
|
<ElTableColumn prop="remark" label="备注" min-width="180" show-overflow-tooltip>
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
{{ row.remark || '--' }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</ElTableColumn>
|
|
|
|
|
|
<ElTableColumn v-if="!props.readonly" label="操作" width="180" fixed="right" align="center">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<div class="setting-team-panel__actions">
|
|
|
|
|
|
<ElButton
|
|
|
|
|
|
v-auth="{ code: 'project:product:update', source: 'object' }"
|
|
|
|
|
|
link
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
:disabled="row.status !== 0 || row.managerFlag"
|
|
|
|
|
|
@click="emit('edit', row)"
|
|
|
|
|
|
>
|
2026-05-09 11:30:34 +08:00
|
|
|
|
编辑
|
2026-04-23 09:05:55 +08:00
|
|
|
|
</ElButton>
|
|
|
|
|
|
<ElButton
|
|
|
|
|
|
v-auth="{ code: 'project:product:update', source: 'object' }"
|
|
|
|
|
|
link
|
|
|
|
|
|
type="danger"
|
|
|
|
|
|
:disabled="row.status !== 0 || row.managerFlag"
|
|
|
|
|
|
@click="emit('remove', row)"
|
|
|
|
|
|
>
|
|
|
|
|
|
移出成员
|
|
|
|
|
|
</ElButton>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</ElTableColumn>
|
|
|
|
|
|
</ElTable>
|
|
|
|
|
|
</ElCard>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.setting-team-panel__header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.setting-team-panel__toolbar {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.setting-team-panel__search {
|
|
|
|
|
|
width: 220px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.setting-team-panel__role-filter {
|
|
|
|
|
|
width: 180px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.setting-team-panel__actions {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 22:25:04 +08:00
|
|
|
|
.setting-team-panel__role-option {
|
|
|
|
|
|
display: flex;
|
2026-05-14 14:11:16 +08:00
|
|
|
|
align-items: center;
|
2026-05-18 22:25:04 +08:00
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
2026-05-14 14:11:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-18 22:25:04 +08:00
|
|
|
|
.setting-team-panel__role-option-info:hover {
|
|
|
|
|
|
color: var(--el-color-primary);
|
2026-05-14 14:11:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-23 09:05:55 +08:00
|
|
|
|
@media (width <= 768px) {
|
|
|
|
|
|
.setting-team-panel__header {
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.setting-team-panel__toolbar {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.setting-team-panel__search {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.setting-team-panel__role-filter {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|