index.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  1. <template>
  2. <div class="app-container">
  3. <!-- 列表页搜索区 -->
  4. <el-card shadow="never" class="search-card" :body-style="{ padding: '15px 20px 10px' }">
  5. <el-form :model="queryParams" ref="queryRef" label-width="80px">
  6. <el-row :gutter="24">
  7. <el-col :span="6">
  8. <el-form-item label="所属公司" prop="companyNo">
  9. <el-select v-model="queryParams.companyNo" placeholder="请选择" style="width: 100%" clearable>
  10. <el-option v-for="item in companyOptions" :key="item.companyCode || item.id" :label="item.companyName" :value="item.companyCode || item.id" />
  11. </el-select>
  12. </el-form-item>
  13. </el-col>
  14. <el-col :span="6">
  15. <el-form-item label="项目名称" prop="projectName">
  16. <el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable />
  17. </el-form-item>
  18. </el-col>
  19. <el-col :span="6">
  20. <el-form-item label="客户名称" prop="customName">
  21. <el-input v-model="queryParams.customName" placeholder="请输入客户名称" clearable />
  22. </el-form-item>
  23. </el-col>
  24. <el-col :span="6">
  25. <el-form-item label="负责人" prop="leader">
  26. <el-select v-model="queryParams.leader" placeholder="请选择" style="width: 100%" clearable>
  27. <el-option v-for="item in userOptions" :key="item.staffId || item.userId" :label="(item.staffCode || item.userName ? '(' + (item.staffCode || item.userName) + ') ' : '') + (item.staffName || item.nickName)" :value="String(item.staffId || item.userId)" />
  28. </el-select>
  29. </el-form-item>
  30. </el-col>
  31. </el-row>
  32. <el-row :gutter="24" class="search-row">
  33. <el-col :span="6">
  34. <el-form-item label="部门" prop="deptNo">
  35. <el-tree-select
  36. v-model="queryParams.deptNo"
  37. :data="deptOptions"
  38. :props="{ value: 'id', label: 'label', children: 'children' }"
  39. value-key="id"
  40. placeholder="所属部门"
  41. check-strictly
  42. style="width: 100%"
  43. clearable
  44. filterable
  45. />
  46. </el-form-item>
  47. </el-col>
  48. <el-col :span="6">
  49. <el-form-item label="产品支持" prop="productSupport">
  50. <el-select v-model="queryParams.productSupport" placeholder="请选择" style="width: 100%" clearable>
  51. <el-option v-for="item in userOptions" :key="item.staffId || item.userId" :label="(item.staffCode || item.userName ? '(' + (item.staffCode || item.userName) + ') ' : '') + (item.staffName || item.nickName)" :value="String(item.staffId || item.userId)" />
  52. </el-select>
  53. </el-form-item>
  54. </el-col>
  55. <el-col :span="6">
  56. <el-form-item label="项目级别" prop="projectLevel">
  57. <el-select v-model="queryParams.projectLevel" placeholder="请选择" style="width: 100%" clearable>
  58. <el-option v-for="item in projectLevelOptions" :key="item.value" :label="item.label" :value="Number(item.value)" />
  59. </el-select>
  60. </el-form-item>
  61. </el-col>
  62. <el-col :span="6">
  63. <el-form-item label="项目类型" prop="businessType">
  64. <el-select v-model="queryParams.businessType" placeholder="请选择" style="width: 100%" clearable>
  65. <el-option v-for="item in projectTypeOptions" :key="item.value" :label="item.label" :value="Number(item.value)" />
  66. </el-select>
  67. </el-form-item>
  68. </el-col>
  69. </el-row>
  70. <el-row :gutter="24" class="search-row">
  71. <el-col :span="6">
  72. <el-form-item label="入围类型" prop="finalizationType">
  73. <el-select v-model="queryParams.finalizationType" placeholder="请选择" style="width: 100%" clearable>
  74. <el-option v-for="item in shortlistedTypeOptions" :key="item.value" :label="item.label" :value="Number(item.value)" />
  75. </el-select>
  76. </el-form-item>
  77. </el-col>
  78. <el-col :span="6">
  79. <el-form-item label="项目状态" prop="projectStatus">
  80. <el-select v-model="queryParams.projectStatus" style="width: 100%" clearable>
  81. <el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="Number(item.value)" />
  82. </el-select>
  83. </el-form-item>
  84. </el-col>
  85. <el-col :span="6">
  86. <el-form-item label="成交结果" prop="result">
  87. <el-select v-model="queryParams.result" placeholder="请选择" style="width: 100%" clearable>
  88. <el-option v-for="item in resultOptions" :key="item.value" :label="item.label" :value="Number(item.value)" />
  89. </el-select>
  90. </el-form-item>
  91. </el-col>
  92. <el-col :span="6">
  93. <el-form-item label="时间查询类型" prop="timeType">
  94. <el-select v-model="queryParams.timeType" style="width: 100%" clearable>
  95. <el-option v-for="item in timeTypeOptions" :key="item.value" :label="item.label" :value="Number(item.value)" />
  96. </el-select>
  97. </el-form-item>
  98. </el-col>
  99. </el-row>
  100. <el-row :gutter="24" class="search-row">
  101. <el-col :span="7">
  102. <el-form-item label="时间范围" prop="dateRange">
  103. <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD" style="width: 100%" />
  104. </el-form-item>
  105. </el-col>
  106. <el-col :span="17">
  107. <el-form-item label-width="20px">
  108. <div class="search-btns">
  109. <el-button icon="Search" @click="handleQuery">搜索</el-button>
  110. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  111. <el-button type="primary" icon="Plus" @click="handleAdd" class="btn-new">新增</el-button>
  112. </div>
  113. </el-form-item>
  114. </el-col>
  115. </el-row>
  116. </el-form>
  117. </el-card>
  118. <!-- 数据表格 -->
  119. <el-card shadow="never" class="table-card">
  120. <div class="tabs-control">
  121. <div class="tabs-left">
  122. <span class="page-title">年度入围(平台)信息列表</span>
  123. <div class="tab-items">
  124. <div class="tab-item" :class="{ current: activeTab === 'mine' }" @click="handleTabChange('mine')">我负责的年度入围</div>
  125. <div class="tab-item" :class="{ current: activeTab === 'join' }" @click="handleTabChange('join')">我参与的年度入围</div>
  126. <div class="tab-item badge-item" :class="{ current: activeTab === 'all' }" @click="handleTabChange('all')">
  127. 全部年度入围
  128. <span class="badge" v-if="total > 0">{{ total > 99 ? '99+' : total }}</span>
  129. </div>
  130. </div>
  131. <div class="summary-info">
  132. <span class="total-label">金额总计</span>
  133. <span class="total-value">{{ Number(totalAmount).toFixed(2) }}</span>
  134. <span class="total-unit">(万)</span>
  135. </div>
  136. </div>
  137. <div class="tabs-right">
  138. <div class="action-btns">
  139. <el-button type="info" plain size="default" icon="Delete" :disabled="multipleSelection.length === 0" @click="handleBatchDelete">批量删除</el-button>
  140. <el-button type="primary" size="default" icon="Switch" :disabled="multipleSelection.length === 0" @click="handleTransfer">转移给他人</el-button>
  141. </div>
  142. </div>
  143. </div>
  144. <el-table ref="selectionTable" v-loading="loading" :data="dataList" border class="standard-table" @selection-change="handleSelectionChange">
  145. <el-table-column type="selection" width="50" align="center" fixed />
  146. <el-table-column label="项目名称" align="center" min-width="240" show-overflow-tooltip fixed>
  147. <template #default="scope">
  148. <span class="project-name-link" @click="handleDetail(scope.row)">{{ scope.row.projectName }}</span>
  149. </template>
  150. </el-table-column>
  151. <el-table-column label="所属公司" align="center" min-width="180" show-overflow-tooltip>
  152. <template #default="scope">
  153. {{ companyOptions.find(c => (c.companyCode || c.id) === scope.row.companyNo)?.companyName || scope.row.companyNo }}
  154. </template>
  155. </el-table-column>
  156. <el-table-column label="客户名称" align="center" prop="customName" min-width="200" show-overflow-tooltip />
  157. <el-table-column label="项目级别" align="center" prop="projectLevel" width="100">
  158. <template #default="scope">
  159. {{ projectLevelOptions.find(o => String(o.value) === String(scope.row.projectLevel))?.label || scope.row.projectLevel }}
  160. </template>
  161. </el-table-column>
  162. <el-table-column label="项目类型" align="center" prop="businessType" width="100">
  163. <template #default="scope">
  164. {{ projectTypeOptions.find(o => String(o.value) === String(scope.row.businessType))?.label || scope.row.businessType }}
  165. </template>
  166. </el-table-column>
  167. <el-table-column label="金额(万)" align="center" prop="amount" width="100" sortable />
  168. <el-table-column label="服务期(年)" align="center" prop="standardPeriod" width="100" />
  169. <el-table-column label="服务时间段" align="center" prop="serviceTime" width="150" show-overflow-tooltip />
  170. <el-table-column label="投标截止时间" align="center" prop="tenderDeadline" width="110">
  171. <template #default="scope">
  172. <span>{{ parseTime(scope.row.tenderDeadline, '{y}-{m}-{d}') }}</span>
  173. </template>
  174. </el-table-column>
  175. <el-table-column label="部门" align="center" prop="deptName" width="120" />
  176. <el-table-column label="负责人" align="center" prop="leaderName" width="100">
  177. <template #default="scope">
  178. {{ scope.row.leaderName || scope.row.managerName || userOptions.find(u => String(u.staffId || u.userId) === String(scope.row.leader))?.staffName || '' }}
  179. </template>
  180. </el-table-column>
  181. <el-table-column label="产品支持" align="center" prop="productSupportName" width="100">
  182. <template #default="{ row }">
  183. <span>{{ row.productSupportName || findUserName(row.productSupport) }}</span>
  184. </template>
  185. </el-table-column>
  186. <el-table-column label="赢单率(%)" align="center" width="100">
  187. <template #default="scope">
  188. {{ (scope.row.winningRate ?? scope.row.winRate) != null ? (scope.row.winningRate ?? scope.row.winRate) + '%' : '' }}
  189. </template>
  190. </el-table-column>
  191. <el-table-column label="创建时间" align="center" prop="createTime" width="120">
  192. <template #default="scope">
  193. <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
  194. </template>
  195. </el-table-column>
  196. <el-table-column label="报名截止时间" align="center" prop="signUpDeadline" width="110">
  197. <template #default="scope">
  198. <span>{{ parseTime(scope.row.signUpDeadline || scope.row.entryDeadline, '{y}-{m}-{d}') }}</span>
  199. </template>
  200. </el-table-column>
  201. <el-table-column label="行业" align="center" prop="industry" width="120">
  202. <template #default="scope">
  203. {{
  204. scope.row.industryCategoryName ||
  205. scope.row.industryName ||
  206. industryOptions.find(o => String(o.id) === String(scope.row.profession))?.industryCategoryName ||
  207. ''
  208. }}
  209. </template>
  210. </el-table-column>
  211. <el-table-column label="入围类型" align="center" prop="finalizationType" width="100">
  212. <template #default="scope">
  213. {{ shortlistedTypeOptions.find(o => String(o.value) === String(scope.row.finalizationType || scope.row.shortlistedType || scope.row.selectionType))?.label || scope.row.finalizationType || scope.row.selectionType }}
  214. </template>
  215. </el-table-column>
  216. <el-table-column label="项目状态" align="center" prop="projectStatus" width="100">
  217. <template #default="scope">
  218. <el-tag :type="scope.row.projectStatus === 5 ? 'info' : 'warning'" plain>{{ statusOptions.find(o => String(o.value) === String(scope.row.projectStatus))?.label || '跟进中' }}</el-tag>
  219. </template>
  220. </el-table-column>
  221. <el-table-column label="成交结果" align="center" width="100">
  222. <template #default="scope">
  223. <template v-if="String(scope.row.dealResult || scope.row.result) === '1'">
  224. <span style="color: #67c23a;">赢单</span>
  225. </template>
  226. <template v-else-if="String(scope.row.dealResult || scope.row.result) === '2'">
  227. <span style="color: #f56c6c;">丢单</span>
  228. </template>
  229. <template v-else>
  230. {{ resultOptions.find(o => String(o.value) === String(scope.row.dealResult || scope.row.result))?.label || scope.row.dealResult || scope.row.result }}
  231. </template>
  232. </template>
  233. </el-table-column>
  234. <el-table-column label="操作" align="center" width="180" fixed="right">
  235. <template #default="scope">
  236. <el-button link type="info" @click="handleDetail(scope.row)">详情</el-button>
  237. <el-button link type="primary" @click="handleProgress(scope.row)">进度</el-button>
  238. <el-button link type="info" @click="handleDelete(scope.row)">删除</el-button>
  239. </template>
  240. </el-table-column>
  241. </el-table>
  242. <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
  243. </el-card>
  244. <!-- 抽屉:查看/发布进度 -->
  245. <el-drawer title="查看进度" v-model="progressVisible" size="500px" class="progress-drawer">
  246. <div class="progress-container">
  247. <!-- 发布区域 -->
  248. <div class="progress-publish-box">
  249. <el-input
  250. v-model="progressContent"
  251. type="textarea"
  252. :rows="6"
  253. placeholder="请输入进度描述"
  254. maxlength="500"
  255. show-word-limit
  256. resize="none"
  257. />
  258. <div class="publish-footer">
  259. <el-button type="primary" icon="Plus" @click="submitProgress" :loading="publishLoading">发 布</el-button>
  260. </div>
  261. </div>
  262. <!-- 列表区域 -->
  263. <div class="progress-list" v-loading="progressListLoading">
  264. <div v-for="(item, index) in progressList" :key="index" class="progress-item">
  265. <div class="item-header">
  266. <span class="user-name">{{ item.createByName || item.userName || item.createBy }}</span>
  267. <span class="time">{{ item.createTime || item.followUpTime }}</span>
  268. </div>
  269. <div class="item-content">{{ item.content || item.followUpCondition }}</div>
  270. </div>
  271. <el-empty v-if="progressList.length === 0 && !progressListLoading" description="暂无进度数据" :image-size="60" />
  272. </div>
  273. <!-- 分页 -->
  274. <div class="progress-pagination" v-if="progressTotal > 0">
  275. <el-pagination
  276. v-model:current-page="progressPage"
  277. v-model:page-size="progressPageSize"
  278. layout="prev, pager, next"
  279. :total="progressTotal"
  280. @current-change="loadProgress"
  281. small
  282. />
  283. </div>
  284. </div>
  285. </el-drawer>
  286. <!-- 新增组件 -->
  287. <AddDialog v-model="addVisible" :options="drawerOptions" @success="getList" />
  288. <!-- 编辑组件 -->
  289. <EditDialog v-model="editVisible" :id="currentId" :options="drawerOptions" @success="getList" />
  290. <!-- 详情组件 -->
  291. <DetailDialog v-model="detailVisible" :id="currentId" :options="drawerOptions" @edit="handleEdit" @success="getList" />
  292. <!-- 转移负责人弹窗 -->
  293. <el-dialog v-model="transferVisible" title="是否将选中的年度入围转移给其他负责人?" width="500px" append-to-body>
  294. <el-form label-width="110px" style="padding: 10px 20px 0;">
  295. <el-form-item label="新负责人:" required>
  296. <el-select v-model="transferOwner" placeholder="请选择" style="width: 100%" filterable clearable>
  297. <el-option v-for="item in userOptions" :key="item.staffId || item.userId" :label="(item.staffCode || item.userName ? '(' + (item.staffCode || item.userName) + ') ' : '') + (item.staffName || item.nickName)" :value="String(item.staffId || item.userId)" />
  298. </el-select>
  299. </el-form-item>
  300. <el-form-item label="">
  301. <el-checkbox v-model="keepAsMember">保留【负责人】为销售机会团队成员</el-checkbox>
  302. </el-form-item>
  303. </el-form>
  304. <template #footer>
  305. <div class="dialog-footer">
  306. <el-button type="primary" @click="confirmTransfer">确定</el-button>
  307. <el-button @click="transferVisible = false">取消</el-button>
  308. </div>
  309. </template>
  310. </el-dialog>
  311. </div>
  312. </template>
  313. <script setup name="PlatformSelection">
  314. import { ref, reactive, onMounted, getCurrentInstance, toRefs } from 'vue';
  315. import {
  316. listPlatformSelection,
  317. delPlatformSelection,
  318. transferPlatformSelection
  319. } from '@/api/saleManage/platformSelection/index';
  320. import { listRecord, addRecord } from "@/api/visit/record";
  321. import { listComStaff } from "@/api/system/comStaff/index";
  322. import { listCompanyOption, listCustomerInfo } from "@/api/customer/customerInfo/index";
  323. import { listIndustryCategory } from "@/api/customer/industryCategory";
  324. import { listSalesResultAnalyze } from '@/api/saleManage/leads/salesResultAnalyze';
  325. import { deptTreeSelect } from "@/api/system/user";
  326. import { Search, Refresh, Plus, Delete, Switch } from '@element-plus/icons-vue';
  327. import AddDialog from './add.vue';
  328. import EditDialog from './edit.vue';
  329. import DetailDialog from './detail.vue';
  330. const proxy = getCurrentInstance().proxy;
  331. // 列表状态
  332. const loading = ref(false);
  333. const total = ref(0);
  334. const dataList = ref([]);
  335. const dateRange = ref([]);
  336. const multipleSelection = ref([]);
  337. const totalAmount = ref(0);
  338. const activeTab = ref('all');
  339. // 进度状态
  340. const progressVisible = ref(false);
  341. const progressContent = ref('');
  342. const publishLoading = ref(false);
  343. const progressList = ref([]);
  344. const progressListLoading = ref(false);
  345. const currentProjectNo = ref('');
  346. const currentCustomerName = ref('');
  347. const currentCustomerNo = ref('');
  348. const currentDeptName = ref('');
  349. const currentIndustry = ref('');
  350. const progressTotal = ref(0);
  351. const progressPage = ref(1);
  352. const progressPageSize = ref(5);
  353. // 转移弹窗状态
  354. const transferVisible = ref(false);
  355. const transferOwner = ref('');
  356. const keepAsMember = ref(false);
  357. // 组件显示控制
  358. const addVisible = ref(false);
  359. const editVisible = ref(false);
  360. const detailVisible = ref(false);
  361. const currentId = ref(null);
  362. // 查询参数
  363. const queryParams = reactive({
  364. pageNum: 1,
  365. pageSize: 10,
  366. companyNo: undefined,
  367. projectName: undefined,
  368. customName: undefined,
  369. leader: undefined,
  370. deptNo: undefined,
  371. projectLevel: undefined,
  372. businessType: undefined,
  373. projectStatus: 1,
  374. timeType: 1,
  375. result: undefined,
  376. queryType: undefined,
  377. finalizationType: 1
  378. });
  379. // 下拉选项
  380. const {
  381. XMJB0001, L0001, R0001, J0001, ZBPL0001, deal_result, time_query_type
  382. } = proxy.useDict('XMJB0001', 'L0001', 'R0001', 'J0001', 'ZBPL0001', 'deal_result', 'time_query_type');
  383. const projectLevelOptions = L0001;
  384. const projectTypeOptions = XMJB0001;
  385. const shortlistedTypeOptions = R0001;
  386. const statusOptions = J0001;
  387. const resultOptions = deal_result;
  388. const timeTypeOptions = time_query_type;
  389. const materialOptions = ZBPL0001;
  390. const userOptions = ref([]);
  391. const companyOptions = ref([]);
  392. const deptOptions = ref([]);
  393. const industryOptions = ref([]);
  394. // 使用 computed 确保传给子组件的是展开后的数组(而非 ref 对象),保持响应式
  395. const drawerOptionsBase = reactive({
  396. company: [],
  397. user: [],
  398. industryList: [],
  399. });
  400. const drawerOptions = computed(() => ({
  401. company: drawerOptionsBase.company,
  402. user: drawerOptionsBase.user,
  403. industryList: drawerOptionsBase.industryList,
  404. level: L0001.value || [],
  405. type: XMJB0001.value || [],
  406. shortlisted: R0001.value || [],
  407. material: ZBPL0001.value || [],
  408. status: J0001.value || [],
  409. dept: deptOptions.value || [],
  410. }));
  411. /** 查询列表 */
  412. const getList = async () => {
  413. loading.value = true;
  414. try {
  415. const params = { ...queryParams, dealResult: queryParams.result };
  416. const res = await listPlatformSelection(proxy.addDateRange(params, dateRange.value));
  417. const list = res.rows || [];
  418. // 补全成交结果数据
  419. if (list.length > 0) {
  420. const projectNos = list.map(i => i.projectNo).filter(Boolean);
  421. if (projectNos.length > 0) {
  422. try {
  423. const analysisRes = await listSalesResultAnalyze({ objectNos: projectNos.join(',') });
  424. const analysisMap = {};
  425. (analysisRes.rows || analysisRes.data || []).forEach(a => {
  426. analysisMap[a.objectNo] = a.dealResult;
  427. });
  428. list.forEach(item => {
  429. if (item.projectNo && analysisMap[item.projectNo]) {
  430. item.dealResult = analysisMap[item.projectNo];
  431. }
  432. });
  433. } catch (e) {
  434. console.error('补全成交结果失败', e);
  435. }
  436. }
  437. }
  438. dataList.value = list;
  439. total.value = res.total || 0;
  440. totalAmount.value = dataList.value.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
  441. } catch (err) {
  442. console.error(err);
  443. } finally {
  444. loading.value = false;
  445. }
  446. };
  447. const handleQuery = () => { queryParams.pageNum = 1; getList(); };
  448. const handleTabChange = (tab) => {
  449. if (activeTab.value === tab) return;
  450. activeTab.value = tab;
  451. switch (tab) {
  452. case 'all': delete queryParams.queryType; break;
  453. case 'mine': queryParams.queryType = 1; break;
  454. case 'join': queryParams.queryType = 2; break;
  455. }
  456. handleQuery();
  457. };
  458. const resetQuery = () => { dateRange.value = []; proxy.resetForm("queryRef"); handleQuery(); };
  459. const handleSelectionChange = (selection) => { multipleSelection.value = selection; };
  460. const handleAdd = () => {
  461. addVisible.value = true;
  462. };
  463. const handleDetail = (row) => {
  464. currentId.value = row.id;
  465. detailVisible.value = true;
  466. };
  467. const handleEdit = (id) => {
  468. currentId.value = id;
  469. detailVisible.value = false;
  470. editVisible.value = true;
  471. };
  472. const handleProgress = (row) => {
  473. currentId.value = row.id;
  474. currentProjectNo.value = row.projectNo;
  475. currentCustomerName.value = row.customName || row.customerName;
  476. currentCustomerNo.value = row.customNo || row.companyNo || '';
  477. currentDeptName.value = row.deptName || row.department || '';
  478. currentIndustry.value = row.industryCategoryName || row.industryName || row.industry || row.profession || '';
  479. progressContent.value = '';
  480. progressPage.value = 1;
  481. progressVisible.value = true;
  482. loadProgress();
  483. };
  484. const loadProgress = async () => {
  485. if (!currentProjectNo.value) return;
  486. progressListLoading.value = true;
  487. try {
  488. const res = await listRecord({
  489. objectNo: currentProjectNo.value,
  490. pageNum: progressPage.value,
  491. pageSize: progressPageSize.value
  492. });
  493. progressList.value = res.rows || [];
  494. progressTotal.value = res.total || 0;
  495. } catch (err) {
  496. } finally {
  497. progressListLoading.value = false;
  498. }
  499. };
  500. const submitProgress = async () => {
  501. if (!progressContent.value.trim()) return proxy.$modal.msgWarning("请输入进度内容");
  502. publishLoading.value = true;
  503. try {
  504. await addRecord({
  505. objectNo: currentProjectNo.value,
  506. customerNo: currentCustomerNo.value,
  507. customerName: currentCustomerName.value,
  508. department: currentDeptName.value,
  509. deptName: currentDeptName.value,
  510. profession: currentIndustry.value,
  511. followUpCondition: progressContent.value,
  512. dataType: '3', // 对应年度入围类型
  513. callDate: proxy.parseTime(new Date(), '{y}-{m}-{d}'),
  514. callTypeCode: '1',
  515. callAim: '项目进度跟进'
  516. });
  517. proxy.$modal.msgSuccess("发布成功");
  518. progressContent.value = '';
  519. progressPage.value = 1;
  520. loadProgress();
  521. getList();
  522. } catch (err) {
  523. console.error(err);
  524. } finally {
  525. publishLoading.value = false;
  526. }
  527. };
  528. const handleDelete = (row) => {
  529. const ids = row.id || multipleSelection.value.map(item => item.id);
  530. proxy.$modal.confirm(`是否确认删除记录?`).then(() => {
  531. return delPlatformSelection(ids);
  532. }).then(() => {
  533. getList();
  534. proxy.$modal.msgSuccess("删除成功");
  535. }).catch(() => {});
  536. };
  537. const handleBatchDelete = () => {
  538. handleDelete({});
  539. };
  540. const handleTransfer = () => {
  541. transferVisible.value = true;
  542. };
  543. const confirmTransfer = async () => {
  544. if (!multipleSelection.value || multipleSelection.value.length === 0) {
  545. proxy.$modal.msgWarning('请先选择要转移的数据');
  546. return;
  547. }
  548. if (!transferOwner.value) {
  549. proxy.$modal.msgWarning('请选择新负责人');
  550. return;
  551. }
  552. try {
  553. await transferPlatformSelection({
  554. ids: multipleSelection.value.map(i => i.id),
  555. newLeaderId: transferOwner.value,
  556. keepAsMember: keepAsMember.value
  557. });
  558. proxy.$modal.msgSuccess('转移成功');
  559. transferVisible.value = false;
  560. transferOwner.value = '';
  561. keepAsMember.value = false;
  562. getList();
  563. } catch (err) {
  564. console.error('权属转移失败:', err);
  565. }
  566. };
  567. /** 初始化基础数据 */
  568. const initData = () => {
  569. listIndustryCategory().then(res => {
  570. const flatten = (list) => {
  571. let result = [];
  572. list.forEach(item => {
  573. result.push(item);
  574. if (item.children) result = result.concat(flatten(item.children));
  575. });
  576. return result;
  577. };
  578. const listData = res.data || res.rows || [];
  579. const flatData = flatten(listData);
  580. industryOptions.value = flatData;
  581. drawerOptionsBase.industryList = listData;
  582. });
  583. listComStaff({ pageSize: 1000 }).then(res => {
  584. const list = res.rows || res.data || [];
  585. userOptions.value = list;
  586. drawerOptionsBase.user = list;
  587. });
  588. listCompanyOption().then(res => { companyOptions.value = res.data; drawerOptionsBase.company = res.data; });
  589. deptTreeSelect().then(res => deptOptions.value = res.data);
  590. };
  591. const getOptions = () => {
  592. listCompanyOption().then(res => { companyOptions.value = res.data || []; });
  593. listCustomerInfo({ pageNum: 1, pageSize: 500, isHighSeas: 'all' }).then(res => { customerList.value = res.rows || []; });
  594. };
  595. const findUserName = (id) => {
  596. if (!id) return '';
  597. const user = userOptions.value.find(u => String(u.staffId || u.userId) === String(id));
  598. return user ? (user.staffName || user.nickName) : id;
  599. };
  600. onMounted(() => {
  601. initData();
  602. getList();
  603. });
  604. </script>
  605. <style scoped lang="scss">
  606. .app-container { padding: 15px; background-color: #f7f9fb; min-height: calc(100vh - 84px); }
  607. .search-card {
  608. margin-bottom: 10px; border: none; background: #fff; border-radius: 8px;
  609. :deep(.el-form-item) { margin-bottom: 6px;
  610. .el-form-item__label { font-weight: normal; color: #4e5969; }
  611. }
  612. }
  613. .search-row { margin-top: 5px; }
  614. .search-btns { display: flex; gap: 12px; align-items: center; justify-content: flex-start; .el-button { border-radius: 6px; height: 32px; padding: 8px 16px; } }
  615. .tabs-control {
  616. display: flex; align-items: center; justify-content: space-between; margin-bottom: 15px;
  617. .tabs-left { display: flex; align-items: center; gap: 20px;
  618. .page-title { font-size: 16px; color: #333; font-weight: normal; }
  619. .tab-items { display: flex; gap: 10px;
  620. .tab-item { padding: 6px 15px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 14px; color: #606266; cursor: pointer; transition: all 0.2s; position: relative; background: #fff;
  621. &:hover { color: #409eff; border-color: #c6e2ff; background-color: #ecf5ff; }
  622. &.current { color: #409eff; border-color: #409eff; background-color: #ecf5ff; }
  623. &.badge-item { position: relative; padding-right: 25px; }
  624. .badge { position: absolute; top: -8px; right: -5px; background: #f56c6c; color: #fff; font-size: 12px; padding: 2px 6px; border-radius: 10px; line-height: 1; transform: scale(0.85); }
  625. }
  626. }
  627. .summary-info { font-size: 14px; color: #f56c6c; margin-left: 10px;
  628. .total-value { font-size: 16px; font-weight: normal; margin: 0 4px; }
  629. }
  630. }
  631. .tabs-right {
  632. .action-btns { display: flex; gap: 10px; }
  633. }
  634. }
  635. .table-card { border: none; border-radius: 8px; }
  636. .standard-table {
  637. :deep(th.el-table__cell) { background-color: #f7f8fa !important; color: #4e5969; font-weight: normal; font-size: 13px; }
  638. :deep(td.el-table__cell) { font-size: 13px; color: #1d2129; }
  639. }
  640. .project-name-link { color: #409eff; cursor: pointer; &:hover { text-decoration: underline; } }
  641. .dialog-footer { display: flex; justify-content: flex-end; gap: 12px; }
  642. .progress-publish-box {
  643. padding-bottom: 20px;
  644. .publish-footer {
  645. margin-top: 15px;
  646. display: flex;
  647. justify-content: flex-end;
  648. }
  649. }
  650. .progress-container {
  651. padding: 0 10px;
  652. display: flex;
  653. flex-direction: column;
  654. height: 100%;
  655. }
  656. .progress-list {
  657. flex: 1;
  658. overflow-y: auto;
  659. padding-top: 10px;
  660. .progress-item {
  661. padding: 15px 0;
  662. border-bottom: 1px solid #f2f3f5;
  663. &:last-child { border-bottom: none; }
  664. .item-header {
  665. display: flex;
  666. align-items: center;
  667. margin-bottom: 10px;
  668. .user-name { color: #333; font-weight: bold; font-size: 14px; margin-right: 15px; }
  669. .time { color: #86909c; font-size: 13px; }
  670. }
  671. .item-content {
  672. font-size: 14px;
  673. color: #4e5969;
  674. line-height: 1.6;
  675. word-break: break-all;
  676. }
  677. }
  678. }
  679. .progress-pagination {
  680. margin-top: 10px;
  681. padding-bottom: 20px;
  682. display: flex;
  683. justify-content: flex-end;
  684. }
  685. </style>