Files
cn-rdms-web/src/views/project/list/modules/project-create-base-form.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>