Merge branch 'main' of http://192.168.1.22:3000/Web/cn-rdms-web
# Conflicts: # src/service/api/product.ts # src/service/api/project.ts # src/typings/api/project.d.ts
This commit is contained in:
@@ -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,
|
||||
|
||||
96
src/views/product/list/modules/product-create-base-form.vue
Normal file
96
src/views/product/list/modules/product-create-base-form.vue
Normal 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>
|
||||
@@ -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>
|
||||
298
src/views/product/list/modules/product-create-team-step.vue
Normal file
298
src/views/product/list/modules/product-create-team-step.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user