318 lines
9.6 KiB
Vue
318 lines
9.6 KiB
Vue
<script setup lang="ts">
|
|
import { computed, nextTick, onMounted, ref } from 'vue';
|
|
import { ElCol, ElDatePicker, ElFormItem, ElInput, ElOption, ElRow, ElSelect } from 'element-plus';
|
|
import dayjs from 'dayjs';
|
|
import { RDMS_OBJECT_DIRECTION_DICT_CODE, RDMS_PROJECT_TYPE_DICT_CODE } from '@/constants/dict';
|
|
import { fetchGetProductPage } from '@/service/api';
|
|
import { useDict } from '@/hooks/business/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: 'ProjectCreateBaseForm' });
|
|
|
|
export interface ProjectCreateBaseForm {
|
|
projectCode: string;
|
|
projectName: string;
|
|
directionCode: string;
|
|
projectType: string;
|
|
productId: string | null;
|
|
managerUserId: string | null;
|
|
plannedStartDate: string | null;
|
|
plannedEndDate: string | null;
|
|
projectDesc: string;
|
|
}
|
|
|
|
interface ProductOption {
|
|
id: string;
|
|
name: string;
|
|
directionCode: string;
|
|
}
|
|
|
|
interface Props {
|
|
managerUserOptions: Api.SystemManage.UserSimple[];
|
|
}
|
|
|
|
defineProps<Props>();
|
|
|
|
const model = defineModel<ProjectCreateBaseForm>('modelValue', { required: true });
|
|
|
|
const { formRef, validate } = useForm();
|
|
const { createRequiredRule } = useFormRules();
|
|
const { getLabel: getDirectionLabel } = useDict(RDMS_OBJECT_DIRECTION_DICT_CODE);
|
|
|
|
const productOptions = ref<ProductOption[]>([]);
|
|
|
|
const hasAssociatedProduct = computed(() => Boolean(model.value.productId));
|
|
const directionReadonly = computed(() => hasAssociatedProduct.value);
|
|
|
|
const selectedProductDirection = computed(() => {
|
|
if (!model.value.productId) {
|
|
return '';
|
|
}
|
|
|
|
return productOptions.value.find(p => p.id === model.value.productId)?.directionCode || '';
|
|
});
|
|
|
|
const effectiveDirectionCode = computed({
|
|
get: () => {
|
|
if (hasAssociatedProduct.value) {
|
|
return selectedProductDirection.value || model.value.directionCode;
|
|
}
|
|
|
|
return model.value.directionCode;
|
|
},
|
|
set: (val: string) => {
|
|
if (!hasAssociatedProduct.value) {
|
|
model.value.directionCode = val;
|
|
}
|
|
}
|
|
});
|
|
|
|
const directionDisplayName = computed(() => {
|
|
const directionCode = effectiveDirectionCode.value;
|
|
|
|
if (!directionCode) {
|
|
return '';
|
|
}
|
|
|
|
return getDirectionLabel(directionCode, directionCode);
|
|
});
|
|
|
|
function parsePlannedDate(value: string | null | undefined) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
const date = new Date(value);
|
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
}
|
|
|
|
function isPlannedDateRangeValid(startDate: string | null, endDate: string | null) {
|
|
if (!startDate || !endDate) {
|
|
return true;
|
|
}
|
|
|
|
return !dayjs(endDate).isBefore(dayjs(startDate), 'day');
|
|
}
|
|
|
|
function buildEndDateShortcut(text: string, mutator: (date: Date) => void) {
|
|
return {
|
|
text,
|
|
value: () => {
|
|
let startDate = parsePlannedDate(model.value.plannedStartDate);
|
|
|
|
if (!startDate) {
|
|
startDate = new Date();
|
|
model.value.plannedStartDate = dayjs(startDate).format('YYYY-MM-DD');
|
|
window.$message?.info('未选择计划开始日期,已按今日为基准计算');
|
|
nextTick(() => formRef.value?.clearValidate('plannedStartDate'));
|
|
}
|
|
|
|
const endDate = new Date(startDate.getTime());
|
|
mutator(endDate);
|
|
return endDate;
|
|
}
|
|
};
|
|
}
|
|
|
|
const plannedEndDateShortcuts = [
|
|
buildEndDateShortcut('一星期', date => date.setDate(date.getDate() + 7)),
|
|
buildEndDateShortcut('两星期', date => date.setDate(date.getDate() + 14)),
|
|
buildEndDateShortcut('一个月', date => date.setMonth(date.getMonth() + 1)),
|
|
buildEndDateShortcut('三个月', date => date.setMonth(date.getMonth() + 3)),
|
|
buildEndDateShortcut('半年', date => date.setMonth(date.getMonth() + 6)),
|
|
buildEndDateShortcut('一年', date => date.setFullYear(date.getFullYear() + 1))
|
|
];
|
|
|
|
const rules = computed(
|
|
() =>
|
|
({
|
|
projectName: [createRequiredRule('请输入项目名称')],
|
|
directionCode: [createRequiredRule('请选择项目方向')],
|
|
projectType: [createRequiredRule('请选择项目类型')],
|
|
managerUserId: [createRequiredRule('请选择项目经理')],
|
|
plannedStartDate: [createRequiredRule('请选择计划开始日期')],
|
|
plannedEndDate: [
|
|
createRequiredRule('请选择计划结束日期'),
|
|
{
|
|
validator: (_rule, value: string | null, callback) => {
|
|
if (!isPlannedDateRangeValid(model.value.plannedStartDate, value)) {
|
|
callback(new Error('计划结束日期不能早于计划开始日期'));
|
|
return;
|
|
}
|
|
|
|
callback();
|
|
},
|
|
trigger: 'change'
|
|
}
|
|
]
|
|
}) satisfies Record<string, App.Global.FormRule[]>
|
|
);
|
|
|
|
async function loadProductOptions() {
|
|
const { error, data } = await fetchGetProductPage({ pageNo: 1, pageSize: 200 });
|
|
|
|
if (error || !data) {
|
|
productOptions.value = [];
|
|
return;
|
|
}
|
|
|
|
productOptions.value = data.list.map(item => ({
|
|
id: item.id,
|
|
name: item.name || item.code || item.id,
|
|
directionCode: item.directionCode || ''
|
|
}));
|
|
}
|
|
|
|
function onProductChange(newProductId: string | null) {
|
|
if (!newProductId) {
|
|
return;
|
|
}
|
|
|
|
const product = productOptions.value.find(p => p.id === newProductId);
|
|
|
|
if (product) {
|
|
model.value.directionCode = product.directionCode;
|
|
}
|
|
}
|
|
|
|
async function runValidate(): Promise<boolean> {
|
|
try {
|
|
await validate();
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
onMounted(loadProductOptions);
|
|
|
|
defineExpose({ validate: runValidate });
|
|
</script>
|
|
|
|
<template>
|
|
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
|
|
<ElRow :gutter="16">
|
|
<ElCol :span="12">
|
|
<ElFormItem label="项目名称" prop="projectName">
|
|
<ElInput v-model="model.projectName" clearable maxlength="128" placeholder="请输入项目名称" />
|
|
</ElFormItem>
|
|
</ElCol>
|
|
<ElCol :span="12">
|
|
<ElFormItem label="项目编码" prop="projectCode">
|
|
<ElInput v-model="model.projectCode" clearable placeholder="不填则由后端自动生成" />
|
|
</ElFormItem>
|
|
</ElCol>
|
|
<ElCol :span="12">
|
|
<ElFormItem label="项目方向" prop="directionCode">
|
|
<DictSelect
|
|
v-if="!directionReadonly"
|
|
v-model="effectiveDirectionCode"
|
|
:dict-code="RDMS_OBJECT_DIRECTION_DICT_CODE"
|
|
filterable
|
|
placeholder="请选择项目方向"
|
|
/>
|
|
<ElInput
|
|
v-else
|
|
:model-value="directionDisplayName"
|
|
readonly
|
|
class="project-create-base-form__readonly-input"
|
|
placeholder="未获取到项目方向"
|
|
/>
|
|
</ElFormItem>
|
|
</ElCol>
|
|
<ElCol :span="12">
|
|
<ElFormItem label="项目类型" prop="projectType">
|
|
<DictSelect
|
|
v-model="model.projectType"
|
|
:dict-code="RDMS_PROJECT_TYPE_DICT_CODE"
|
|
filterable
|
|
placeholder="请选择项目类型"
|
|
/>
|
|
</ElFormItem>
|
|
</ElCol>
|
|
<ElCol :span="12">
|
|
<ElFormItem label="所属产品" prop="productId">
|
|
<ElSelect
|
|
v-model="model.productId"
|
|
clearable
|
|
filterable
|
|
placeholder="选择所属产品(可选),选择后将锁定项目方向"
|
|
@change="onProductChange"
|
|
>
|
|
<ElOption v-for="item in productOptions" :key="item.id" :label="item.name" :value="item.id" />
|
|
</ElSelect>
|
|
</ElFormItem>
|
|
</ElCol>
|
|
<ElCol :span="12">
|
|
<ElFormItem label="项目经理" prop="managerUserId">
|
|
<BusinessUserSelect
|
|
v-model="model.managerUserId"
|
|
:options="managerUserOptions"
|
|
placeholder="请选择项目经理"
|
|
/>
|
|
</ElFormItem>
|
|
</ElCol>
|
|
<ElCol :span="12">
|
|
<ElFormItem label="计划开始日期" prop="plannedStartDate">
|
|
<ElDatePicker
|
|
v-model="model.plannedStartDate"
|
|
type="date"
|
|
value-format="YYYY-MM-DD"
|
|
placeholder="选择开始日期"
|
|
class="project-create-base-form__date-picker"
|
|
/>
|
|
</ElFormItem>
|
|
</ElCol>
|
|
<ElCol :span="12">
|
|
<ElFormItem label="计划结束日期" prop="plannedEndDate">
|
|
<ElDatePicker
|
|
v-model="model.plannedEndDate"
|
|
type="date"
|
|
value-format="YYYY-MM-DD"
|
|
placeholder="选择结束日期"
|
|
:shortcuts="plannedEndDateShortcuts"
|
|
class="project-create-base-form__date-picker"
|
|
/>
|
|
</ElFormItem>
|
|
</ElCol>
|
|
<ElCol :span="24">
|
|
<ElFormItem label="项目说明" prop="projectDesc">
|
|
<ElInput
|
|
v-model="model.projectDesc"
|
|
type="textarea"
|
|
:rows="4"
|
|
maxlength="500"
|
|
show-word-limit
|
|
placeholder="请输入项目说明"
|
|
/>
|
|
</ElFormItem>
|
|
</ElCol>
|
|
</ElRow>
|
|
</ElForm>
|
|
</template>
|
|
|
|
<style scoped>
|
|
:deep(.project-create-base-form__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(.project-create-base-form__readonly-input .el-input__wrapper:hover),
|
|
:deep(.project-create-base-form__readonly-input.is-focus .el-input__wrapper) {
|
|
box-shadow: 0 0 0 1px rgb(203 213 225 / 96%) inset;
|
|
}
|
|
|
|
:deep(.project-create-base-form__readonly-input .el-input__inner) {
|
|
color: rgb(51 65 85 / 96%);
|
|
cursor: default;
|
|
-webkit-text-fill-color: rgb(51 65 85 / 96%);
|
|
}
|
|
|
|
:deep(.project-create-base-form__date-picker.el-date-editor.el-input) {
|
|
width: 100%;
|
|
}
|
|
</style>
|