优化项目管理

This commit is contained in:
2026-09-13 22:31:42 +08:00
parent 2f6cdbb3cd
commit a8c09c1db5
30 changed files with 1654 additions and 346 deletions
@@ -6,7 +6,7 @@
<el-tag v-else-if="party?.ref_name" size="small" type="info" effect="plain">
{{ party.ref_type === 2 ? "供应商" : "客户" }}
</el-tag>
<span v-if="locked" class="party-company">{{ tenantName }}</span>
<span v-if="locked" class="party-company">{{ party?.ref_name || tenantName }}</span>
</div>
<!-- 关联主体:当前租户方锁定,其余模糊搜索客户库 / 供应商库 -->
@@ -62,13 +62,30 @@
<!-- <div v-if="party.ref_name" class="picked-name">已关联:{{ party.ref_name }}</div> -->
</template>
<!-- 签约人 -->
<!-- 签约人:本公司方从组织架构人员中选择,其余方从关联企业联系人中选择,均自动带出联系电话;也支持手动输入 -->
<div class="party-row signer-row">
<el-input
<el-select
v-model="party.signer_name"
placeholder="签约人姓名"
maxlength="30"
/>
filterable
allow-create
default-first-option
clearable
:loading="signerLoading"
:placeholder="contactPlaceholder"
@change="handleSignerPick"
>
<el-option
v-for="c in contacts"
:key="c.id"
:label="c.contact_name"
:value="c.contact_name"
>
<span class="option-name">{{ c.contact_name }}</span>
<span class="option-sub">
{{ [c.position || c.department, signerPhoneOf(c)].filter(Boolean).join(" · ") }}
</span>
</el-option>
</el-select>
<el-input v-model="party.signer_phone" placeholder="联系电话" maxlength="20" />
</div>
</div>
@@ -78,6 +95,8 @@
import { ref, computed, watch } from "vue";
import { Plus } from "@element-plus/icons-vue";
import { getCrmCustomerList, getCrmSupplierList } from "@/api/crm";
import { listContacts } from "@/api/contact";
import { erpOrganizationApi } from "@/api/organization";
import { useAuthStore } from "@/stores/auth";
const props = defineProps({
@@ -102,6 +121,83 @@ const party = computed(() => props.modelValue);
const searching = ref(false);
const options = ref([]);
/* ------------------------------ 签约人:人员联动 ------------------------------ */
/** 是否为本公司方:锁定方,或 ref_type=0(本公司) */
const isOwnCompany = computed(() => props.locked || Number(party.value?.ref_type) === 0);
const contacts = ref([]);
const contactLoading = ref(false);
/** 本公司人员:来自租户组织架构(与组织架构模块同源) */
const ownEmployees = ref([]);
const ownLoading = ref(false);
/** 签约人下拉加载态:本公司方取员工加载态,其余取联系人加载态 */
const signerLoading = computed(() => (isOwnCompany.value ? ownLoading.value : contactLoading.value));
/** 联系人电话:手机优先,其次座机 */
const signerPhoneOf = (c) => c?.mobile || c?.phone || "";
const contactPlaceholder = computed(() => {
if (isOwnCompany.value) return "选择本公司人员(也可输入)";
return party.value?.ref_id ? "选择签约人(也可输入)" : "请先选择客户 / 供应商";
});
/** 拉取本公司人员(在职员工),映射为与联系人一致的字段结构,便于复用下拉渲染 */
async function fetchOwnEmployees() {
ownLoading.value = true;
try {
const res = await erpOrganizationApi.getEmployeeList({ status: 1 });
const list = Array.isArray(res) ? res : res?.data || [];
ownEmployees.value = (Array.isArray(list) ? list : []).map((e) => ({
id: `emp_${e.id}`,
contact_name: e.name || e.account || "",
position: e.position || "",
department: e.department_name || "",
mobile: e.phone || "",
phone: "",
}));
} catch {
ownEmployees.value = [];
} finally {
ownLoading.value = false;
}
}
/** 拉取签约人候选:本公司方取组织架构人员,其余取关联企业联系人(客户 / 供应商) */
async function fetchContacts() {
if (isOwnCompany.value) {
if (!ownEmployees.value.length) await fetchOwnEmployees();
contacts.value = ownEmployees.value;
return;
}
const refID = party.value?.ref_id;
if (!refID) {
contacts.value = [];
return;
}
contactLoading.value = true;
try {
const res = await listContacts({
related_type: Number(party.value.ref_type) === 2 ? 2 : 1,
related_id: refID,
page: 1,
pageSize: 100,
});
contacts.value = res?.data?.list || [];
} catch (e) {
contacts.value = [];
} finally {
contactLoading.value = false;
}
}
/** 选择签约人:自动带出该联系人的联系电话 */
function handleSignerPick(name) {
const hit = contacts.value.find((c) => c.contact_name === name);
if (hit) party.value.signer_phone = signerPhoneOf(hit);
}
const refTypeText = computed(() =>
Number(party.value.ref_type) === 2 ? "供应商" : "客户"
);
@@ -121,7 +217,11 @@ const ensureOption = () => {
watch(
() => party.value?.ref_id,
() => ensureOption(),
() => {
ensureOption();
// 关联企业变化时同步刷新其联系人,供签约人下拉选择
fetchContacts();
},
{ immediate: true }
);
@@ -160,14 +260,20 @@ const handlePick = (id) => {
const handleClear = () => {
party.value.ref_id = null;
party.value.ref_name = "";
party.value.signer_name = "";
party.value.signer_phone = "";
options.value = [];
contacts.value = [];
};
/** 丙/丁方切换客户 / 供应商来源时清空已选 */
const handleRefTypeChange = () => {
party.value.ref_id = null;
party.value.ref_name = "";
party.value.signer_name = "";
party.value.signer_phone = "";
options.value = [];
contacts.value = [];
};
</script>