# Conflicts:
#	src/service/api/product.ts
#	src/service/api/project.ts
#	src/typings/api/project.d.ts
This commit is contained in:
dk
2026-05-13 21:20:59 +08:00
59 changed files with 8046 additions and 919 deletions

View File

@@ -10,8 +10,7 @@ import { fetchGetProductOverviewSummary, fetchGetProductPage, fetchGetUserSimple
import { useDict } from '@/hooks/business/dict';
import { useRouterPush } from '@/hooks/common/router';
import { useUIPaginatedTable } from '@/hooks/common/table';
import BusinessTableActionCell from '@/components/custom/business-table-action-cell';
import { getProductStatusLabel, getProductStatusTagType, isProductEditable } from '../shared/product-master-data';
import { getProductStatusLabel, getProductStatusTagType } from '../shared/product-master-data';
import ProductOperateDialog from './modules/product-operate-dialog.vue';
import ProductSearch from './modules/product-search.vue';
@@ -235,26 +234,6 @@ const { columns, columnChecks, data, loading, getDataByPage, mobilePagination }
width: 170,
align: 'center',
formatter: row => formatDateTime(row.updateTime)
},
{
prop: 'operate',
label: '操作',
width: 108,
align: 'center',
fixed: 'right',
formatter: row => (
<BusinessTableActionCell
actions={[
{
key: 'edit',
label: '编辑',
buttonType: 'primary',
disabled: !isProductEditable(row.statusCode),
onClick: () => openEdit(row)
}
]}
/>
)
}
],
immediate: false
@@ -317,11 +296,6 @@ function openCreate() {
operateVisible.value = true;
}
function openEdit(row: Api.Product.Product) {
editingRow.value = row;
operateVisible.value = true;
}
async function enterProductContext(row: Api.Product.Product) {
await routerPush({
path: PRODUCT_ENTRY_ROUTE_PATH,

View File

@@ -0,0 +1,96 @@
<script setup lang="ts">
import { computed } from 'vue';
import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict';
import { useForm, useFormRules } from '@/hooks/common/form';
import BusinessUserSelect from '@/components/custom/business-user-select.vue';
import DictSelect from '@/components/custom/dict-select.vue';
defineOptions({ name: 'ProductCreateBaseForm' });
export interface ProductCreateBaseForm {
code: string;
name: string;
directionCode: string;
managerUserId: string | null;
description: string;
}
interface Props {
managerUserOptions: Api.SystemManage.UserSimple[];
}
defineProps<Props>();
const model = defineModel<ProductCreateBaseForm>('modelValue', { required: true });
const { formRef, validate } = useForm();
const { createRequiredRule } = useFormRules();
const rules = computed(
() =>
({
name: [createRequiredRule('请输入产品名称')],
directionCode: [createRequiredRule('请选择产品方向')],
managerUserId: [createRequiredRule('请选择产品经理')]
}) satisfies Record<string, App.Global.FormRule[]>
);
async function runValidate(): Promise<boolean> {
try {
await validate();
return true;
} catch {
return false;
}
}
defineExpose({ validate: runValidate });
</script>
<template>
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
<ElRow :gutter="16">
<ElCol :span="12">
<ElFormItem label="产品名称" prop="name">
<ElInput v-model="model.name" clearable maxlength="128" placeholder="请输入产品名称" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="产品编码" prop="code">
<ElInput v-model="model.code" clearable placeholder="不填则由后端自动生成" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="产品方向" prop="directionCode">
<DictSelect
v-model="model.directionCode"
:dict-code="RDMS_OBJECT_DIRECTION_DICT_CODE"
filterable
placeholder="请选择产品方向"
/>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="产品经理" prop="managerUserId">
<BusinessUserSelect
v-model="model.managerUserId"
:options="managerUserOptions"
placeholder="请选择产品经理"
/>
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="产品描述" prop="description">
<ElInput
v-model="model.description"
type="textarea"
:rows="4"
maxlength="500"
show-word-limit
placeholder="请输入产品描述"
/>
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
</template>

View File

@@ -0,0 +1,169 @@
<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;
remark: string;
}
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 emit = defineEmits<Emits>();
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 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 === PRODUCT_MANAGER_ROLE_CODE;
}
async function handleConfirm() {
await validate();
emit('submit', {
userId: model.userId,
roleId: model.roleId,
remark: model.remark.trim()
});
}
watch(visible, async value => {
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">
<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="成员用户">
<ElInput
:model-value="userLabelMap.get(String(model.userId)) || ''"
readonly
class="product-create-team-member-dialog__readonly-input"
placeholder="未获取到成员用户"
/>
</ElFormItem>
</ElCol>
<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>
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="备注">
<ElInput
v-model="model.remark"
type="textarea"
:rows="3"
maxlength="200"
show-word-limit
placeholder="请输入备注"
/>
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
</BusinessFormDialog>
</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;
cursor: default;
}
:deep(.product-create-team-member-dialog__readonly-input .el-input__wrapper:hover),
:deep(.product-create-team-member-dialog__readonly-input.is-focus .el-input__wrapper) {
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
}
:deep(.product-create-team-member-dialog__readonly-input .el-input__inner) {
color: rgb(51 65 85 / 96%);
-webkit-text-fill-color: rgb(51 65 85 / 96%);
cursor: default;
}
</style>

View File

@@ -0,0 +1,298 @@
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue';
import { PRODUCT_MANAGER_ROLE_CODE } from '@/constants/business';
import { fetchGetRoleSimpleList } from '@/service/api';
import { getProductTeamTableHeight } from '../../setting/shared';
import ProductCreateTeamMemberDialog from './product-create-team-member-dialog.vue';
import type { ProductCreateBaseForm } from './product-create-base-form.vue';
defineOptions({ name: 'ProductCreateTeamStep' });
interface DraftMember {
/** 客户端临时主键,仅用于 v-for 稳定 */
key: string;
userId: string;
roleId: string;
remark: string;
/** true 表示由产品经理自动派生的锁定行 */
locked: boolean;
}
interface Props {
baseInfo: ProductCreateBaseForm;
userOptions: Api.SystemManage.UserSimple[];
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: 'update:members', members: Api.Product.CreateProductMemberParams[]): 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 teamTableHeight = getProductTeamTableHeight(5);
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 dialogInitial = computed(() => {
if (memberDialogMode.value === 'create' || !editingKey.value) {
return null;
}
const target = members.value.find(item => item.key === editingKey.value);
if (!target) {
return null;
}
return { userId: target.userId, roleId: target.roleId, remark: target.remark };
});
function getUserNickname(userId: string) {
return userLabelMap.value.get(String(userId)) || userId;
}
function getRoleName(roleId: string) {
return roleOptions.value.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;
if (!managerUserId || !managerRole.value) {
members.value = members.value.filter(item => !item.locked);
emitMembers();
return;
}
const lockedIndex = members.value.findIndex(item => item.locked);
const lockedRow: DraftMember = {
key: lockedIndex >= 0 ? members.value[lockedIndex].key : generateKey(),
userId: managerUserId,
roleId: managerRole.value.id,
remark: lockedIndex >= 0 ? members.value[lockedIndex].remark : '',
locked: true
};
if (lockedIndex >= 0) {
members.value[lockedIndex] = lockedRow;
} else {
members.value = [lockedRow, ...members.value];
}
emitMembers();
}
function openCreate() {
memberDialogMode.value = 'create';
editingKey.value = null;
memberDialogVisible.value = true;
}
function openEdit(row: DraftMember) {
memberDialogMode.value = 'edit';
editingKey.value = row.key;
memberDialogVisible.value = true;
}
function removeMember(key: string) {
members.value = members.value.filter(item => item.key !== key);
emitMembers();
}
function handleMemberSubmit(payload: { userId: string; roleId: string; remark: string }) {
if (memberDialogMode.value === 'create') {
members.value.push({
key: generateKey(),
userId: payload.userId,
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
};
}
}
memberDialogVisible.value = false;
emitMembers();
}
function emitMembers() {
emit(
'update:members',
members.value.map(item => ({
userId: item.userId,
roleId: item.roleId,
remark: item.remark.trim() || null,
previousManagerUserId: null,
previousManagerRoleId: null
}))
);
}
async function runValidate(): Promise<boolean> {
if (managerRoleError.value) {
window.$message?.error(managerRoleError.value);
return false;
}
for (const item of members.value) {
if (!item.userId || !item.roleId) {
window.$message?.error('请补全所有成员的用户和角色');
return false;
}
}
const userIdSet = new Set<string>();
for (const item of members.value) {
if (userIdSet.has(item.userId)) {
window.$message?.error(`成员「${getUserNickname(item.userId)}」重复,请检查`);
return false;
}
userIdSet.add(item.userId);
}
return true;
}
onMounted(loadRoles);
watch(
() => props.baseInfo.managerUserId,
() => {
if (!managerRoleError.value && managerRole.value) {
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>
<ElAlert
v-if="managerRoleError"
:title="managerRoleError"
type="error"
:closable="false"
show-icon
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>
<ProductCreateTeamMemberDialog
v-model:visible="memberDialogVisible"
:mode="memberDialogMode"
:initial="dialogInitial"
:user-options="userOptions"
:role-options="roleOptions"
:disabled-user-ids="dialogDisabledUserIds"
@submit="handleMemberSubmit"
/>
</div>
</template>
<style scoped>
.team-step {
display: flex;
flex-direction: column;
gap: 14px;
min-height: 0;
}
.team-step__toolbar {
display: flex;
justify-content: flex-end;
}
.team-step__alert {
margin: 0;
}
.team-step__actions {
display: inline-flex;
align-items: center;
gap: 12px;
}
</style>

View File

@@ -1,11 +1,14 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { RDMS_OBJECT_DIRECTION_DICT_CODE } from '@/constants/dict';
import { fetchCreateProduct, fetchGetProduct, fetchUpdateProduct } from '@/service/api';
import { fetchCreateProductWithTeam, fetchGetProduct, fetchUpdateProduct } from '@/service/api';
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';
import DictSelect from '@/components/custom/dict-select.vue';
import ProductCreateBaseForm, {
type ProductCreateBaseForm as ProductCreateBaseFormModel
} from './product-create-base-form.vue';
import ProductCreateTeamStep from './product-create-team-step.vue';
defineOptions({ name: 'ProductOperateDialog' });
@@ -22,11 +25,10 @@ interface Emits {
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const visible = defineModel<boolean>('visible', { default: false });
interface Model {
// === 编辑模式(单步) ===
interface EditModel {
code: string;
directionCode: string;
name: string;
@@ -34,17 +36,26 @@ interface Model {
description: string;
}
const { formRef, validate } = useForm();
const { formRef: editFormRef, validate: editValidate } = useForm();
const { createRequiredRule } = useFormRules();
const editModel = ref<EditModel>(createEditModel());
const editLoading = ref(false);
const submitting = ref(false);
const isEditMode = computed(() => Boolean(props.rowData?.id));
const dialogTitle = computed(() => (isEditMode.value ? '编辑产品' : '新增产品'));
const submitting = ref(false);
const loading = ref(false);
const model = ref<Model>(createDefaultModel());
const editRules = {
directionCode: [createRequiredRule('请选择产品方向')],
name: [createRequiredRule('请输入产品名称')],
managerUserId: [createRequiredRule('请选择产品经理')]
} satisfies Record<string, App.Global.FormRule[]>;
const managerLabelMap = computed(() => new Map(props.managerUserOptions.map(item => [String(item.id), item.nickname])));
const managerDisplayName = computed(() => {
const managerUserId = model.value.managerUserId;
const managerUserId = editModel.value.managerUserId;
if (!managerUserId) {
return '';
@@ -53,20 +64,8 @@ const managerDisplayName = computed(() => {
return managerLabelMap.value.get(String(managerUserId)) || String(managerUserId);
});
const rules = {
directionCode: [createRequiredRule('请选择产品方向')],
name: [createRequiredRule('请输入产品名称')],
managerUserId: [createRequiredRule('请选择产品经理')]
} satisfies Record<string, App.Global.FormRule[]>;
function createDefaultModel(): Model {
return {
code: '',
directionCode: '',
name: '',
managerUserId: null,
description: ''
};
function createEditModel(): EditModel {
return { code: '', directionCode: '', name: '', managerUserId: null, description: '' };
}
function getNullableText(value?: string | null) {
@@ -77,80 +76,132 @@ function closeDialog() {
visible.value = false;
}
async function handleSubmit() {
await validate();
async function handleEditSubmit() {
await editValidate();
const managerUserId = model.value.managerUserId;
const managerUserId = editModel.value.managerUserId;
if (!managerUserId) {
if (!managerUserId || !props.rowData?.id) {
return;
}
const payload: Api.Product.SaveProductParams = {
code: getNullableText(model.value.code),
directionCode: model.value.directionCode,
name: model.value.name.trim(),
// Long ID 必须以 string 提交,禁止再转成 number。
managerUserId,
description: getNullableText(model.value.description)
};
submitting.value = true;
if (isEditMode.value && props.rowData?.id) {
const result = await fetchUpdateProduct({
id: props.rowData.id,
...payload
});
submitting.value = false;
if (result.error) {
return;
}
window.$message?.success('产品编辑成功');
closeDialog();
emit('submitted', props.rowData.id);
return;
}
const result = await fetchCreateProduct(payload);
const { error } = await fetchUpdateProduct({
id: props.rowData.id,
code: getNullableText(editModel.value.code),
directionCode: editModel.value.directionCode,
name: editModel.value.name.trim(),
managerUserId,
description: getNullableText(editModel.value.description)
});
submitting.value = false;
if (result.error) {
if (error) {
return;
}
window.$message?.success('产品编辑成功');
closeDialog();
emit('submitted', props.rowData.id);
}
// === 新增模式(两步向导) ===
const baseFormRef = ref<InstanceType<typeof ProductCreateBaseForm> | null>(null);
const teamStepRef = ref<InstanceType<typeof ProductCreateTeamStep> | null>(null);
const currentStep = ref<1 | 2>(1);
const createBaseModel = ref<ProductCreateBaseFormModel>(createBaseInfo());
const draftMembers = ref<Api.Product.CreateProductMemberParams[]>([]);
function createBaseInfo(): ProductCreateBaseFormModel {
return { code: '', name: '', directionCode: '', managerUserId: null, description: '' };
}
async function goNext() {
const valid = await baseFormRef.value?.validate();
if (!valid) {
return;
}
currentStep.value = 2;
}
function goPrev() {
currentStep.value = 1;
}
async function handleCreateSubmit() {
const baseValid = await baseFormRef.value?.validate();
if (!baseValid) {
currentStep.value = 1;
return;
}
const teamValid = await teamStepRef.value?.validate();
if (!teamValid) {
return;
}
submitting.value = true;
const payload: Api.Product.CreateProductWithTeamParams = {
product: {
code: getNullableText(createBaseModel.value.code),
name: createBaseModel.value.name.trim(),
directionCode: createBaseModel.value.directionCode,
managerUserId: createBaseModel.value.managerUserId as string,
description: getNullableText(createBaseModel.value.description)
},
members: draftMembers.value
};
const { error, data } = await fetchCreateProductWithTeam(payload);
submitting.value = false;
if (error) {
return;
}
window.$message?.success('产品新增成功');
closeDialog();
emit('submitted', result.data);
emit('submitted', data);
}
// === 公共:弹框可见性变化时重置 / 加载数据 ===
watch(visible, async value => {
if (!value) {
return;
}
submitting.value = false;
currentStep.value = 1;
if (!isEditMode.value || !props.rowData?.id) {
model.value = createDefaultModel();
editModel.value = createEditModel();
createBaseModel.value = createBaseInfo();
draftMembers.value = [];
await nextTick();
formRef.value?.clearValidate();
editFormRef.value?.clearValidate();
return;
}
loading.value = true;
editLoading.value = true;
const { error, data } = await fetchGetProduct(props.rowData.id);
loading.value = false;
editLoading.value = false;
if (error || !data) {
return;
}
model.value = {
editModel.value = {
code: data.code || '',
directionCode: data.directionCode || '',
name: data.name || '',
@@ -159,43 +210,42 @@ watch(visible, async value => {
};
await nextTick();
formRef.value?.clearValidate();
editFormRef.value?.clearValidate();
});
</script>
<template>
<!-- 编辑模式单步表单与改造前一致 -->
<BusinessFormDialog
v-if="isEditMode"
v-model="visible"
:title="dialogTitle"
preset="sm"
:loading="loading"
:loading="editLoading"
:confirm-loading="submitting"
@confirm="handleSubmit"
@confirm="handleEditSubmit"
>
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
<ElForm ref="editFormRef" :model="editModel" :rules="editRules" label-position="top">
<ElRow :gutter="16">
<ElCol :span="24">
<ElFormItem v-if="isEditMode" label="产品编码" prop="code">
<ElFormItem label="产品编码" prop="code">
<ElInput
:model-value="model.code"
:model-value="editModel.code"
readonly
class="product-operate-dialog__readonly-input"
placeholder="未获取到产品编码"
/>
</ElFormItem>
<ElFormItem v-else label="产品编码" prop="code">
<ElInput v-model="model.code" clearable placeholder="不填则由后端自动生成" />
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="产品名称" prop="name">
<ElInput v-model="model.name" clearable maxlength="128" placeholder="请输入产品名称" />
<ElInput v-model="editModel.name" clearable maxlength="128" placeholder="请输入产品名称" />
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="产品方向" prop="directionCode">
<DictSelect
v-model="model.directionCode"
v-model="editModel.directionCode"
:dict-code="RDMS_OBJECT_DIRECTION_DICT_CODE"
filterable
placeholder="请选择产品方向"
@@ -203,7 +253,7 @@ watch(visible, async value => {
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem v-if="isEditMode">
<ElFormItem>
<template #label>
<span class="business-form-label-with-tip">
<ElTooltip
@@ -225,18 +275,11 @@ watch(visible, async value => {
placeholder="未配置产品经理"
/>
</ElFormItem>
<ElFormItem v-else label="产品经理" prop="managerUserId">
<BusinessUserSelect
v-model="model.managerUserId"
:options="managerUserOptions"
placeholder="请选择产品经理"
/>
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="产品描述" prop="description">
<ElInput
v-model="model.description"
v-model="editModel.description"
type="textarea"
:rows="4"
maxlength="500"
@@ -248,6 +291,63 @@ watch(visible, async value => {
</ElRow>
</ElForm>
</BusinessFormDialog>
<!-- 新增模式两步向导复合内容特例自定义 ElDialog 880px -->
<ElDialog
v-else
v-model="visible"
class="product-create-dialog"
:title="dialogTitle"
:close-on-click-modal="false"
destroy-on-close
align-center
width="760px"
>
<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__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"
/>
</div>
</div>
<template #footer>
<div class="product-create-dialog__footer">
<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>
<ElButton v-if="currentStep === 1" type="primary" @click="goNext">下一步</ElButton>
<ElButton v-if="currentStep === 2" type="primary" :loading="submitting" @click="handleCreateSubmit">
确定
</ElButton>
</ElSpace>
</div>
</template>
</ElDialog>
</template>
<style scoped>
@@ -267,4 +367,86 @@ watch(visible, async value => {
cursor: default;
-webkit-text-fill-color: rgb(51 65 85 / 96%);
}
.product-create-dialog :deep(.el-dialog__body) {
display: flex;
flex-direction: column;
padding: 0;
}
.product-create-dialog__stepbar {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
padding: 14px 24px;
border-bottom: 1px solid rgb(229 233 242 / 96%);
background: #fbfcfe;
}
.product-create-dialog__step {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.product-create-dialog__step-index {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 28px;
height: 28px;
border: 1px solid rgb(215 222 235 / 96%);
border-radius: 999px;
background: #fff;
color: rgb(119 129 150 / 96%);
font-size: 13px;
font-weight: 650;
}
.product-create-dialog__step.is-active .product-create-dialog__step-index,
.product-create-dialog__step.is-done .product-create-dialog__step-index {
border-color: var(--el-color-primary);
background: var(--el-color-primary);
color: #fff;
}
.product-create-dialog__step-text {
min-width: 0;
}
.product-create-dialog__step-text strong {
display: block;
font-size: 14px;
font-weight: 650;
}
.product-create-dialog__step-text small {
display: block;
margin-top: 2px;
color: rgb(119 129 150 / 96%);
font-size: 12px;
}
.product-create-dialog__body {
min-height: 0;
max-height: min(560px, calc(100vh - 240px));
overflow: auto;
}
.product-create-dialog__panel {
padding: 24px;
}
.product-create-dialog__footer {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.product-create-dialog__footer-meta {
color: rgb(119 129 150 / 96%);
font-size: 13px;
}
</style>

View File

@@ -423,9 +423,8 @@ watch(
:member="selectedMember"
:current-manager="currentManager"
:role-options="roleOptions"
:user-options="
userOptions.filter(user => !members.some(member => member.status === 0 && member.userId === user.id))
"
:user-options="userOptions"
:disabled-user-ids="members.filter(member => member.status === 0).map(member => member.userId)"
@submit="handleSubmitMemberOperate"
/>
<MemberRemoveDialog

View File

@@ -16,6 +16,8 @@ interface Props {
currentManager: Api.Product.ProductMember | null;
roleOptions: Api.SystemManage.RoleSimple[];
userOptions: Api.SystemManage.UserSimple[];
/** 已是有效成员、需在下拉中禁选并标记"已添加"的 userId 集合 */
disabledUserIds?: readonly string[];
}
interface SubmitPayload {
@@ -29,7 +31,9 @@ interface Emits {
(e: 'submit', payload: SubmitPayload): void;
}
const props = defineProps<Props>();
const props = withDefaults(defineProps<Props>(), {
disabledUserIds: () => []
});
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
@@ -143,7 +147,13 @@ watch(
<ElRow :gutter="16">
<ElCol :span="24">
<ElFormItem v-if="mode === 'create'" label="成员用户" prop="userId">
<BusinessUserSelect v-model="model.userId" :options="userOptions" placeholder="请选择成员用户" />
<BusinessUserSelect
v-model="model.userId"
:options="userOptions"
:disabled-user-ids="props.disabledUserIds"
disabled-label="已添加"
placeholder="请选择成员用户"
/>
</ElFormItem>
<ElFormItem v-else label="成员用户">
<ElInput

View File

@@ -78,18 +78,6 @@ const productLifecycleActionCardMetaMap: Record<Api.Product.ProductStatusActionC
}
};
const productSettingErrorMessageMap: Record<string, string> = {
'1008001002': '产品名称已存在,请更换名称',
'1008001007': '当前产品状态不允许编辑基础信息',
'1008001008': '当前产品已暂停,基础信息仅支持查看,不可编辑。',
'1008001013': '请选择原产品经理交接后的角色',
'1008001014': '当前产品经理不能直接移出,请先完成经理交接',
'1008001015': '当前产品经理不能直接调整为非经理角色,请先完成经理转交',
'1008001004': '当前状态不支持该动作',
'1008001005': '当前动作必须填写原因',
'1008001006': '删除确认名称与当前产品名称不一致'
};
const productTeamTableHeaderHeight = 40;
const productTeamTableRowHeight = 40;
@@ -225,9 +213,3 @@ export function canManageProductTeam(context: ProductTeamManageContext) {
return loginUserId === currentManagerUserId;
}
export function getProductSettingErrorMessage(code: string | number | null | undefined, backendMessage: string) {
const normalizedCode = String(code || '');
return productSettingErrorMessageMap[normalizedCode] || backendMessage;
}