|
|
@@ -63,7 +63,7 @@
|
|
|
</div>
|
|
|
<div class="info-item">
|
|
|
<span class="label">部门</span>
|
|
|
- <span class="value">{{ form.deptNo || '' }}</span>
|
|
|
+ <span class="value">{{ deptName }}</span>
|
|
|
</div>
|
|
|
<div class="info-item">
|
|
|
<span class="label">项目负责人</span>
|
|
|
@@ -288,13 +288,84 @@
|
|
|
</el-select>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="record-list">
|
|
|
+ <div class="record-list custom-scroll">
|
|
|
<div v-if="!(form.followRecords && form.followRecords.length)" class="tab-empty">
|
|
|
<el-empty description="暂无跟进记录" :image-size="60" />
|
|
|
</div>
|
|
|
- <div v-else class="log-item" v-for="(r, i) in form.followRecords" :key="i">
|
|
|
- <div class="log-time">{{ r.createTime }}</div>
|
|
|
- <div class="log-content">{{ r.content }}</div>
|
|
|
+ <div v-else class="timeline-container">
|
|
|
+ <div class="record-card-wrapper" v-for="(r, i) in form.followRecords" :key="i">
|
|
|
+ <!-- 时间轴点与时间 -->
|
|
|
+ <div class="timeline-header">
|
|
|
+ <div class="timeline-dot"></div>
|
|
|
+ <span class="timeline-time">{{ r.visitDate || r.createTime }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 内容卡片 -->
|
|
|
+ <div class="record-card">
|
|
|
+ <div class="card-top">
|
|
|
+ <div class="user-info">
|
|
|
+ <el-avatar :size="32" class="user-avatar">{{ (r.visitorName || r.createBy || '').charAt(0) }}</el-avatar>
|
|
|
+ <div class="publish-info">
|
|
|
+ <span class="user-name">{{ r.visitorName || r.createBy }}</span>
|
|
|
+ <span class="publish-text">发布了条{{ options.visitType.find(o => String(o.dictValue) === String(r.visitType))?.dictLabel || '跟进' }}:</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-icon class="expand-icon"><ArrowDown /></el-icon>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="card-content">
|
|
|
+ <div class="detail-row">
|
|
|
+ <span class="label">客户:</span>
|
|
|
+ <span class="value">{{ form.customerName }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="detail-row">
|
|
|
+ <span class="label">拜访目的:</span>
|
|
|
+ <span class="value">{{ r.purpose || '--' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="detail-row">
|
|
|
+ <span class="label">跟进情况:</span>
|
|
|
+ <span class="value">{{ r.progress || r.content || '--' }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 记录图片 -->
|
|
|
+ <div class="image-section" v-if="r.imageList && r.imageList.length">
|
|
|
+ <span class="label">记录图片:</span>
|
|
|
+ <div class="image-list">
|
|
|
+ <el-image
|
|
|
+ v-for="(img, idx) in r.imageList"
|
|
|
+ :key="idx"
|
|
|
+ :src="img"
|
|
|
+ :preview-src-list="r.imageList"
|
|
|
+ class="record-img"
|
|
|
+ fit="cover"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="update-time-row">
|
|
|
+ <span class="label">更新时间:</span>
|
|
|
+ <span class="value">{{ r.updateTime || r.visitDate || r.createTime }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 回复区域 -->
|
|
|
+ <div class="reply-section">
|
|
|
+ <el-input
|
|
|
+ v-model="r.replyContent"
|
|
|
+ type="textarea"
|
|
|
+ placeholder="请输入回复内容"
|
|
|
+ :rows="3"
|
|
|
+ resize="none"
|
|
|
+ class="reply-input"
|
|
|
+ />
|
|
|
+ <div class="reply-btn-row">
|
|
|
+ <el-button type="primary" size="small" class="reply-submit-btn" @click="handleReplySubmit(r)">
|
|
|
+ <el-icon style="margin-right: 4px;"><ChatLineSquare /></el-icon> 回复
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -355,7 +426,7 @@
|
|
|
<div class="form-item">
|
|
|
<span class="field-label"><span class="required-star">*</span>归属公司</span>
|
|
|
<el-select v-model="form.companyNo" placeholder="请选择" size="small">
|
|
|
- <el-option v-for="item in options.company" :key="item.companyCode || item.id" :label="item.companyName" :value="item.companyCode || item.id" />
|
|
|
+ <el-option v-for="item in options.company" :key="item.companyCode || item.id" :label="item.companyName" :value="String(item.companyCode || item.id)" />
|
|
|
</el-select>
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
@@ -385,7 +456,7 @@
|
|
|
<div class="form-item">
|
|
|
<span class="field-label"><span class="required-star">*</span>项目类型</span>
|
|
|
<el-select v-model="form.businessType" placeholder="请选择" size="small">
|
|
|
- <el-option v-for="item in options.type" :key="item.dictValue" :label="item.dictLabel" :value="Number(item.dictValue)" />
|
|
|
+ <el-option v-for="item in options.type" :key="item.dictValue" :label="item.dictLabel" :value="String(item.dictValue)" />
|
|
|
</el-select>
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
@@ -393,7 +464,7 @@
|
|
|
<div class="form-item">
|
|
|
<span class="field-label"><span class="required-star">*</span>项目类别</span>
|
|
|
<el-select v-model="form.projectLevel" placeholder="请选择" size="small">
|
|
|
- <el-option v-for="item in options.level" :key="item.dictValue" :label="item.dictLabel" :value="Number(item.dictValue)" />
|
|
|
+ <el-option v-for="item in options.level" :key="item.dictValue" :label="item.dictLabel" :value="String(item.dictValue)" />
|
|
|
</el-select>
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
@@ -482,7 +553,7 @@
|
|
|
<div class="form-item">
|
|
|
<span class="field-label"><span class="required-star">*</span>入围类型</span>
|
|
|
<el-select v-model="form.shortlistedType" placeholder="请选择" size="small">
|
|
|
- <el-option v-for="item in options.shortlisted" :key="item.dictValue" :label="item.dictLabel" :value="Number(item.dictValue)" />
|
|
|
+ <el-option v-for="item in options.shortlisted" :key="item.dictValue" :label="item.dictLabel" :value="String(item.dictValue)" />
|
|
|
</el-select>
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
@@ -491,7 +562,7 @@
|
|
|
<div class="form-item">
|
|
|
<span class="field-label"><span class="required-star">*</span>物资类目</span>
|
|
|
<el-select v-model="form.profession" placeholder="请选择" size="small">
|
|
|
- <el-option v-for="item in options.profession" :key="item.dictValue" :label="item.dictLabel" :value="Number(item.dictValue)" />
|
|
|
+ <el-option v-for="item in options.profession" :key="item.dictValue" :label="item.dictLabel" :value="String(item.dictValue)" />
|
|
|
</el-select>
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
@@ -814,7 +885,7 @@
|
|
|
<el-col :span="12">
|
|
|
<el-form-item label="拜访人:" prop="visitor">
|
|
|
<el-select v-model="recordForm.visitor" placeholder="请选择" style="width: 100%" filterable>
|
|
|
- <el-option v-for="item in options.user" :key="item.staffId || item.userId" :label="item.staffName || item.nickName" :value="String(item.staffId || item.userId)" />
|
|
|
+ <el-option v-for="item in (form.memberList || [])" :key="item.staffId || item.userId || item.id" :label="item.memberName || item.staffName || item.realName || item.name" :value="String(item.staffId || item.userId)" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
@@ -898,6 +969,7 @@ import { listTeamMember, addTeamMember, updateTeamMember, delTeamMember } from "
|
|
|
import { listOperationLog } from '@/api/customer/operationLog';
|
|
|
import { listContactPerson, addContactPerson } from "@/api/customer/contactPerson";
|
|
|
import { addRecord, listRecord } from "@/api/visit/record";
|
|
|
+import { deptTreeSelect } from "@/api/system/user";
|
|
|
|
|
|
const props = defineProps({ modelValue: { type: Boolean, default: false }, data: Object, title: String });
|
|
|
const emit = defineEmits(['update:modelValue', 'submit']);
|
|
|
@@ -922,7 +994,8 @@ const options = ref({
|
|
|
industryList: [], // 行业分类数据
|
|
|
teamRole: [], // 团队成员角色字典
|
|
|
permission: [], // 团队成员权限字典
|
|
|
- visitType: [] // 拜访方式字典
|
|
|
+ visitType: [], // 拜访方式字典
|
|
|
+ dept: [] // 部门数据
|
|
|
});
|
|
|
const recordFormRef = ref(null);
|
|
|
const recordImages = ref([]);
|
|
|
@@ -1061,6 +1134,12 @@ const professionName = computed(() => {
|
|
|
);
|
|
|
return p?.industryCategoryName || p?.name || p?.categoryName || p?.dictLabel || '--';
|
|
|
});
|
|
|
+const deptName = computed(() => {
|
|
|
+ const val = String(form.value.deptNo || '');
|
|
|
+ if (!val) return '--';
|
|
|
+ const target = (options.value.dept || []).find(o => String(o.value) === val);
|
|
|
+ return target?.label || val;
|
|
|
+});
|
|
|
|
|
|
/** 行业名称(使用行业分类数据,与列表页一致) */
|
|
|
const industryName = computed(() => {
|
|
|
@@ -1095,6 +1174,16 @@ watch(() => props.modelValue, (val) => {
|
|
|
}
|
|
|
});
|
|
|
|
|
|
+// 监听 data 变化,确保在不打开详情抽屉的情况下(如点击进度按钮),内部数据也能同步
|
|
|
+watch(() => props.data, (newVal) => {
|
|
|
+ if (newVal) {
|
|
|
+ form.value = { ...newVal };
|
|
|
+ if (!form.value.customerName && form.value.customName) {
|
|
|
+ form.value.customerName = form.value.customName;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}, { deep: true });
|
|
|
+
|
|
|
/** 加载项目团队成员 */
|
|
|
const fetchTeamMembers = async () => {
|
|
|
if (!form.value.id) return;
|
|
|
@@ -1132,22 +1221,57 @@ const fetchFollowRecords = async () => {
|
|
|
try {
|
|
|
const params = {
|
|
|
objectNo: form.value.id,
|
|
|
- dataType: 2
|
|
|
+ dataType: 2,
|
|
|
+ pageSize: 50 // 加大加载量,确保显示完整
|
|
|
};
|
|
|
if (recordFilterType.value) {
|
|
|
params.visitType = recordFilterType.value;
|
|
|
}
|
|
|
const res = await listRecord(params);
|
|
|
- form.value.followRecords = (res.rows || res.data || []).map(r => ({
|
|
|
- createTime: r.visitDate || r.createTime,
|
|
|
- content: r.progress || r.purpose || r.content,
|
|
|
- visitType: r.visitType
|
|
|
- }));
|
|
|
+ const records = res.rows || res.data || [];
|
|
|
+
|
|
|
+ // 处理图片 URL
|
|
|
+ for (const r of records) {
|
|
|
+ if (r.recordPicture && !r.imageList) {
|
|
|
+ const ids = r.recordPicture.split(',');
|
|
|
+ try {
|
|
|
+ const ossRes = await listByIds(ids.join(','));
|
|
|
+ r.imageList = (ossRes.data || []).map(img => img.url);
|
|
|
+ } catch (e) {
|
|
|
+ r.imageList = [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 初始化回复内容
|
|
|
+ r.replyContent = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ form.value.followRecords = records;
|
|
|
} catch (err) {
|
|
|
console.error('加载跟进记录失败:', err);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+/** 提交回复 */
|
|
|
+const handleReplySubmit = async (record) => {
|
|
|
+ if (!record.replyContent?.trim()) {
|
|
|
+ proxy.$modal.msgWarning("请输入回复内容");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await publishProjectProgress({
|
|
|
+ objectNo: String(form.value.id),
|
|
|
+ followUpCondition: record.replyContent.trim(),
|
|
|
+ dataType: '2',
|
|
|
+ parentId: record.id // 假设接口支持 parentId 作为回复
|
|
|
+ });
|
|
|
+ proxy.$modal.msgSuccess("回复成功");
|
|
|
+ record.replyContent = '';
|
|
|
+ fetchFollowRecords(); // 刷新列表
|
|
|
+ } catch (err) {
|
|
|
+ console.error('回复失败:', err);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
/** 加载操作日志 */
|
|
|
const fetchOperationLogs = async () => {
|
|
|
if (!form.value.id) return;
|
|
|
@@ -1305,10 +1429,11 @@ const handleOpenLink = (url) => {
|
|
|
};
|
|
|
|
|
|
/** 打开进度弹窗 */
|
|
|
-const openProgressDrawer = () => {
|
|
|
+const openProgressDrawer = (id) => {
|
|
|
showProgressDrawer.value = true;
|
|
|
- if (form.value.id) {
|
|
|
- progressQueryParams.objectNo = String(form.value.id);
|
|
|
+ const targetId = id || form.value.id;
|
|
|
+ if (targetId) {
|
|
|
+ progressQueryParams.objectNo = String(targetId);
|
|
|
progressQueryParams.pageNum = 1;
|
|
|
loadProgressRecordList();
|
|
|
}
|
|
|
@@ -1685,6 +1810,18 @@ onMounted(() => {
|
|
|
listCompanyOption().then(r => options.value.company = r.data);
|
|
|
remoteLoadCustomers(); // 默认加载前 500 条客户
|
|
|
selectStaffOptionList().then(r => options.value.user = r.data);
|
|
|
+ // 加载部门数据并打平
|
|
|
+ deptTreeSelect().then(res => {
|
|
|
+ const flatList = (list) => {
|
|
|
+ let arr = [];
|
|
|
+ for (const item of list || []) {
|
|
|
+ arr.push({ label: item.label, value: String(item.id) });
|
|
|
+ if (item.children?.length) arr.push(...flatList(item.children));
|
|
|
+ }
|
|
|
+ return arr;
|
|
|
+ };
|
|
|
+ options.value.dept = flatList(res.data);
|
|
|
+ });
|
|
|
});
|
|
|
|
|
|
/** 远程加载客户信息 */
|