edit.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. <template>
  2. <el-drawer
  3. title="编辑项目商机"
  4. v-model="visible"
  5. direction="rtl"
  6. size="85%"
  7. :close-on-click-modal="false"
  8. class="opp-drawer"
  9. >
  10. <template #default>
  11. <div class="drawer-body" v-loading="loading">
  12. <el-form ref="formRef" :model="form" :rules="rules" label-width="110px" class="opp-form" label-position="left">
  13. <!-- 基本信息 -->
  14. <div class="section-block">
  15. <div class="section-title">基本信息</div>
  16. <el-row :gutter="24">
  17. <el-col :span="12">
  18. <el-form-item label="归属公司" prop="companyId">
  19. <el-select v-model="form.companyId" placeholder="请选择" style="width: 100%" clearable filterable>
  20. <el-option v-for="item in computedCompanyOptions" :key="item.companyCode || item.id"
  21. :label="item.companyName" :value="String(item.companyCode || item.id)" />
  22. </el-select>
  23. </el-form-item>
  24. </el-col>
  25. <el-col :span="12">
  26. <el-form-item label="客户名称" prop="customerNo">
  27. <el-select
  28. v-model="form.customerNo"
  29. placeholder="请输入关键字搜索客户"
  30. style="width: 100%"
  31. clearable
  32. filterable
  33. @change="handleCustomerChange"
  34. >
  35. <el-option v-for="item in computedCustomerOptions" :key="item.id || item.customerNo"
  36. :label="item.customerName" :value="String(item.id || item.customerNo)" />
  37. </el-select>
  38. </el-form-item>
  39. </el-col>
  40. </el-row>
  41. <el-row :gutter="24">
  42. <el-col :span="12">
  43. <el-form-item label="项目负责人" prop="managerId">
  44. <el-select v-model="form.managerId" placeholder="请选择" style="width: 100%" clearable filterable @change="handleLeaderChange">
  45. <el-option v-for="item in computedUserOptions" :key="item.staffId || item.userId"
  46. :label="(item.staffCode || item.userName ? '(' + (item.staffCode || item.userName) + ') ' : '') + (item.staffName || item.nickName)" :value="String(item.staffId || item.userId)" />
  47. </el-select>
  48. </el-form-item>
  49. </el-col>
  50. <el-col :span="12">
  51. <el-form-item label="项目名称" prop="projectName">
  52. <el-input v-model="form.projectName" placeholder="请输入" maxlength="200" show-word-limit />
  53. </el-form-item>
  54. </el-col>
  55. </el-row>
  56. <el-row :gutter="24">
  57. <el-col :span="8">
  58. <el-form-item label="金额(万)" prop="amount">
  59. <el-input-number v-model="form.amount" :precision="2" :step="1" :min="0" controls-position="right" style="width: 100%" />
  60. </el-form-item>
  61. </el-col>
  62. <el-col :span="8">
  63. <el-form-item label="赢单率(%)" prop="winRate">
  64. <el-input v-model="form.winRate" placeholder="请输入" type="number" maxlength="6">
  65. <template #append>%</template>
  66. </el-input>
  67. </el-form-item>
  68. </el-col>
  69. <el-col :span="8">
  70. <el-form-item label="立项时间" prop="setupTime">
  71. <el-date-picker v-model="form.setupTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择" style="width: 100%" />
  72. </el-form-item>
  73. </el-col>
  74. </el-row>
  75. <el-row :gutter="24">
  76. <el-col :span="8">
  77. <el-form-item label="截止时间" prop="deadline">
  78. <el-date-picker v-model="form.deadline" type="date" value-format="YYYY-MM-DD" placeholder="请选择" style="width: 100%" />
  79. </el-form-item>
  80. </el-col>
  81. <el-col :span="16">
  82. <el-form-item label="营销活动" prop="marketingActivity">
  83. <el-select v-model="form.marketingActivity" placeholder="请选择" style="width: 100%" clearable filterable>
  84. <el-option v-for="item in marketingActivityOptions" :key="item.value" :label="item.label" :value="item.value" />
  85. </el-select>
  86. </el-form-item>
  87. </el-col>
  88. </el-row>
  89. </div>
  90. <!-- 项目信息 -->
  91. <div class="section-block">
  92. <div class="section-title">项目信息</div>
  93. <el-row :gutter="24">
  94. <el-col :span="8">
  95. <el-form-item label="项目级别" prop="projectLevel">
  96. <el-select v-model="form.projectLevel" placeholder="请选择" style="width: 100%" clearable filterable>
  97. <el-option v-for="item in projectLevelOptions" :key="item.value"
  98. :label="item.label" :value="item.value" />
  99. </el-select>
  100. </el-form-item>
  101. </el-col>
  102. <el-col :span="8">
  103. <el-form-item label="项目区域" prop="projectArea">
  104. <el-input v-model="form.projectArea" placeholder="请输入" maxlength="100" />
  105. </el-form-item>
  106. </el-col>
  107. <el-col :span="8">
  108. <el-form-item label="采购方式" prop="purchaseMethod">
  109. <el-select v-model="form.purchaseMethod" placeholder="请选择" style="width: 100%" clearable filterable>
  110. <el-option v-for="item in procurementMethodOptions" :key="item.value"
  111. :label="item.label" :value="item.value" />
  112. </el-select>
  113. </el-form-item>
  114. </el-col>
  115. </el-row>
  116. <el-row :gutter="24">
  117. <el-col :span="8">
  118. <el-form-item label="商机来源" prop="source">
  119. <el-select v-model="form.source" placeholder="请选择" style="width: 100%" clearable filterable>
  120. <el-option v-for="item in infoSourceOptions" :key="item.value"
  121. :label="item.label" :value="item.value" />
  122. </el-select>
  123. </el-form-item>
  124. </el-col>
  125. </el-row>
  126. <el-form-item label="项目描述" prop="description">
  127. <el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入项目描述" maxlength="500" show-word-limit />
  128. </el-form-item>
  129. <el-form-item label="竞争对手" prop="competitor">
  130. <el-input v-model="form.competitor" type="textarea" :rows="3" placeholder="请输入竞争对手" maxlength="500" show-word-limit />
  131. </el-form-item>
  132. </div>
  133. <!-- 附件 -->
  134. <div class="section-block no-border">
  135. <div class="section-title attachment-header">
  136. <span>附件</span>
  137. <el-upload
  138. :action="uploadFileUrl"
  139. :headers="headers"
  140. :on-success="handleUploadSuccess"
  141. :show-file-list="false"
  142. multiple
  143. class="upload-btn"
  144. >
  145. <el-button link type="primary" icon="Upload">上传附件</el-button>
  146. </el-upload>
  147. </div>
  148. <el-table :data="fileList" border stripe class="file-table">
  149. <el-table-column label="文件名称" prop="name" show-overflow-tooltip>
  150. <template #default="scope">
  151. <el-link type="primary" :underline="false" @click="downloadFile(scope.row)" class="file-link">
  152. <el-icon style="margin-right: 4px;"><Document /></el-icon>
  153. {{ scope.row.name }}
  154. </el-link>
  155. </template>
  156. </el-table-column>
  157. <el-table-column label="操作" width="120" align="center">
  158. <template #default="scope">
  159. <el-button link type="primary" @click="downloadFile(scope.row)">下载</el-button>
  160. <el-button link type="danger" @click="handleDeleteFile(scope.$index)">删除</el-button>
  161. </template>
  162. </el-table-column>
  163. </el-table>
  164. </div>
  165. </el-form>
  166. </div>
  167. </template>
  168. <template #footer>
  169. <div class="drawer-footer">
  170. <el-button type="primary" @click="submitForm" :loading="submitting">保存</el-button>
  171. <el-button @click="visible = false">取消</el-button>
  172. </div>
  173. </template>
  174. </el-drawer>
  175. </template>
  176. <script setup>
  177. import { ref, reactive, watch, computed, getCurrentInstance } from 'vue';
  178. import { useDebounceFn } from '@vueuse/core';
  179. import { getOpportunity, updateOpportunity } from '@/api/saleManage/opportunity/index';
  180. import { listCustomerInfo } from "@/api/customer/customerInfo/index";
  181. import { listByIds } from '@/api/system/oss/index';
  182. import { globalHeaders } from '@/utils/request';
  183. import { Close, Upload, Document } from '@element-plus/icons-vue';
  184. const props = defineProps({
  185. modelValue: Boolean,
  186. id: [String, Number],
  187. companyOptions: Array,
  188. customerOptions: Array,
  189. userOptions: Array,
  190. projectLevelOptions: Array,
  191. procurementMethodOptions: Array,
  192. infoSourceOptions: Array,
  193. marketingActivityOptions: Array
  194. });
  195. const emit = defineEmits(['update:modelValue', 'success']);
  196. const proxy = getCurrentInstance().proxy;
  197. const visible = ref(false);
  198. const loading = ref(false);
  199. const submitting = ref(false);
  200. const formRef = ref(null);
  201. const fileList = ref([]);
  202. const uploadFileUrl = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
  203. const headers = globalHeaders();
  204. const form = reactive({});
  205. const customerLoading = ref(false);
  206. const localCustomerOptions = ref([]);
  207. const remoteLoadCustomers = useDebounceFn((query) => {
  208. customerLoading.value = true;
  209. listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(res => {
  210. const list = res.rows || [];
  211. localCustomerOptions.value = list.map(i => ({ ...i, id: String(i.id || i.customerNo) }));
  212. }).finally(() => {
  213. customerLoading.value = false;
  214. });
  215. }, 300);
  216. const rules = reactive({
  217. companyId: [{ required: true, message: '归属公司不能为空', trigger: 'change' }],
  218. customerNo: [{ required: true, message: '客户名称不能为空', trigger: 'change' }],
  219. managerId: [{ required: true, message: '项目负责人不能为空', trigger: 'change' }],
  220. projectName: [
  221. { required: true, message: '项目名称不能为空', trigger: 'blur' },
  222. { max: 200, message: '项目名称不能超过200个字符', trigger: 'blur' }
  223. ],
  224. amount: [
  225. { required: true, message: '金额不能为空', trigger: 'blur' }
  226. ],
  227. winRate: [
  228. { required: true, message: '赢单率不能为空', trigger: 'blur' },
  229. { pattern: /^(100|[1-9]?\d(\.\d+)?)$/, message: '请输入正确的赢单率(0-100)', trigger: 'blur' }
  230. ],
  231. setupTime: [{ required: true, message: '立项时间不能为空', trigger: 'change' }],
  232. projectArea: [
  233. { max: 100, message: '项目区域不能超过100个字符', trigger: 'blur' }
  234. ],
  235. description: [
  236. { required: true, message: '项目描述不能为空', trigger: 'blur' },
  237. { max: 500, message: '项目描述不能超过500个字符', trigger: 'blur' }
  238. ],
  239. competitor: [
  240. { max: 500, message: '竞争对手不能超过500个字符', trigger: 'blur' }
  241. ]
  242. });
  243. // 【增强回显】通过计算属性确保当前项在选项列表中,防止分页或数据不全导致回显显示为 ID
  244. const computedCustomerOptions = computed(() => {
  245. const options = [...localCustomerOptions.value];
  246. if (form.customerNo && form.customerName) {
  247. const exists = options.some(i => String(i.id || i.customerNo) === String(form.customerNo));
  248. if (!exists) options.unshift({ id: form.customerNo, customerNo: form.customerNo, customerName: form.customerName });
  249. }
  250. return options;
  251. });
  252. const computedUserOptions = computed(() => {
  253. const options = [...(props.userOptions || [])];
  254. if (form.managerId && form.leaderName) {
  255. const exists = options.some(i => String(i.staffId || i.userId) === String(form.managerId));
  256. if (!exists) options.unshift({ staffId: form.managerId, staffName: form.leaderName });
  257. }
  258. return options;
  259. });
  260. const computedCompanyOptions = computed(() => {
  261. const options = [...(props.companyOptions || [])];
  262. if (form.companyId && form.companyName) {
  263. const exists = options.some(i => String(i.companyCode || i.id) === String(form.companyId));
  264. if (!exists) options.unshift({ companyCode: form.companyId, companyName: form.companyName });
  265. }
  266. return options;
  267. });
  268. watch(() => props.modelValue, (val) => {
  269. visible.value = val;
  270. if (val && props.id) {
  271. loadDetail(props.id);
  272. remoteLoadCustomers('');
  273. }
  274. });
  275. watch(() => visible.value, (val) => { emit('update:modelValue', val); });
  276. const loadDetail = (id) => {
  277. loading.value = true;
  278. getOpportunity(id).then(res => {
  279. const data = res.data || res;
  280. Object.assign(form, data);
  281. // 映射与强制转字符串以适配下拉回显
  282. form.companyId = data.companyNo ? String(data.companyNo) : '';
  283. form.customerNo = data.customerNo ? String(data.customerNo) : '';
  284. form.managerId = data.leader ? String(data.leader) : '';
  285. form.amount = data.projectBudget;
  286. form.setupTime = data.approvalDate;
  287. form.deadline = data.expectedCompletionTime;
  288. form.marketingActivity = data.activityNo ? String(data.activityNo) : '';
  289. form.projectLevel = data.projectLevel ? String(data.projectLevel) : '';
  290. form.purchaseMethod = data.procurementMethod ? String(data.procurementMethod) : '';
  291. form.source = data.infoSource ? String(data.infoSource) : '';
  292. form.description = data.projectDescription;
  293. if (form.fileNo) {
  294. listByIds(form.fileNo).then(ossRes => {
  295. fileList.value = ossRes.data.map(i => ({
  296. name: i.originalName || i.fileName,
  297. url: i.url,
  298. ossId: i.ossId
  299. }));
  300. });
  301. } else {
  302. fileList.value = [];
  303. }
  304. }).finally(() => { loading.value = false; });
  305. };
  306. const handleCustomerChange = (val) => {
  307. const customer = computedCustomerOptions.value.find(c => String(c.id || c.customerNo) === val);
  308. if (customer) {
  309. form.customerName = customer.customerName;
  310. form.profession = customer.industryCategoryId;
  311. }
  312. };
  313. const handleLeaderChange = (val) => {
  314. const user = computedUserOptions.value.find(u => String(u.staffId || u.userId) === val);
  315. if (user) {
  316. form.leaderName = user.staffName || user.nickName;
  317. }
  318. };
  319. const handleUploadSuccess = (res) => {
  320. if (res.code === 200) {
  321. const file = res.data;
  322. fileList.value.push({
  323. name: file.originalName || file.fileName,
  324. url: file.url,
  325. ossId: file.ossId
  326. });
  327. form.fileNo = fileList.value.map(f => f.ossId).join(',');
  328. proxy.$modal.msgSuccess("上传成功");
  329. }
  330. };
  331. const downloadFile = (row) => {
  332. if (row.url) {
  333. proxy.$modal.msgInfo("正在启动下载...");
  334. if (proxy.$download && proxy.$download.resource) {
  335. proxy.$download.resource(row.url);
  336. } else {
  337. window.open(row.url, '_blank');
  338. }
  339. }
  340. };
  341. const handleDeleteFile = (index) => {
  342. fileList.value.splice(index, 1);
  343. form.fileNo = fileList.value.map(f => f.ossId).join(',');
  344. };
  345. const submitForm = () => {
  346. formRef.value.validate(valid => {
  347. if (valid) {
  348. submitting.value = true;
  349. const data = {
  350. id: form.id,
  351. projectName: form.projectName,
  352. projectBudget: Number(form.amount),
  353. winRate: Number(form.winRate),
  354. approvalDate: form.setupTime,
  355. expectedCompletionTime: form.deadline,
  356. companyNo: form.companyId,
  357. leader: form.managerId,
  358. leaderName: form.leaderName,
  359. activityNo: form.marketingActivity,
  360. procurementMethod: form.purchaseMethod,
  361. infoSource: form.source,
  362. projectDescription: form.description,
  363. competitor: form.competitor,
  364. customerNo: form.customerNo,
  365. customerName: form.customerName,
  366. fileNo: form.fileNo,
  367. projectLevel: form.projectLevel,
  368. projectArea: form.projectArea,
  369. profession: form.profession,
  370. izClue: form.izClue || 0,
  371. projectType: form.projectType || '销售商机',
  372. status: form.status || '0'
  373. };
  374. updateOpportunity(data).then(() => {
  375. proxy.$modal.msgSuccess("修改成功");
  376. visible.value = false;
  377. emit('success');
  378. }).finally(() => { submitting.value = false; });
  379. }
  380. });
  381. };
  382. </script>
  383. <style scoped lang="scss">
  384. .opp-drawer {
  385. :deep(.el-drawer__header) {
  386. margin: 0;
  387. padding: 0 20px;
  388. height: 48px;
  389. line-height: 48px;
  390. border-bottom: 1px solid #f0f0f0;
  391. span {
  392. font-size: 16px;
  393. font-weight: normal;
  394. color: #333;
  395. }
  396. }
  397. :deep(.el-drawer__body) { padding: 0 !important; background-color: #fff; }
  398. }
  399. .drawer-body {
  400. padding: 0;
  401. height: 100%;
  402. overflow-y: auto;
  403. }
  404. .opp-form {
  405. padding: 15px 24px;
  406. :deep(.el-form-item__label) {
  407. font-weight: normal !important;
  408. color: #666;
  409. }
  410. }
  411. .section-block {
  412. margin-bottom: 24px;
  413. .section-title {
  414. padding: 8px 15px;
  415. background-color: #f8fbff;
  416. color: #409eff;
  417. font-size: 14px;
  418. margin-bottom: 15px;
  419. border-bottom: none;
  420. &.attachment-header {
  421. display: flex;
  422. justify-content: space-between;
  423. align-items: center;
  424. padding-top: 4px;
  425. padding-bottom: 4px;
  426. }
  427. }
  428. }
  429. .file-table {
  430. :deep(th.el-table__cell) {
  431. background-color: #f8fafc !important;
  432. color: #475569;
  433. font-weight: 500;
  434. }
  435. .file-link {
  436. font-weight: normal;
  437. font-size: 13px;
  438. }
  439. }
  440. .drawer-footer {
  441. padding: 12px 24px;
  442. border-top: 1px solid #f0f2f5;
  443. text-align: right;
  444. background-color: #fff;
  445. .el-button { font-weight: normal !important; padding: 8px 20px; }
  446. }
  447. </style>