moduleList.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. <template>
  2. <Page :title='`${queryParameters.name}列表`'>
  3. <template v-slot:body>
  4. <div class="flex flex-col h-full">
  5. <!-- 搜索 -->
  6. <div class="mx-1 headerModeuleList">
  7. <van-search v-model.trim="searchVal" background="#F8F8F8" :placeholder="`请输入${queryParameters?.name}关键词`"
  8. @search="onRefresh(true)" @clear="onRefresh(true)">
  9. <template v-slot:left-icon>
  10. <van-icon name="search" class="themeTextColor font-bold" />
  11. </template>
  12. </van-search>
  13. </div>
  14. <!-- 主题内容 -->
  15. <div class="flex-1 overflow-y-auto">
  16. <template v-if="listData?.records && listData.records.length && !loadingList">
  17. <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
  18. <van-list v-model:loading="isLoading" :finished="finished" finished-text="没有更多了" @load="onLoad">
  19. <div v-for="item in listData.records" :key="item.id" @click="toDetail(item)">
  20. <van-swipe-cell>
  21. <div
  22. :class="`${item.needPin ? 'bg-slate-50' : 'bg-white'} px-5 py-5 flex items-center flex-row w-full listContent`">
  23. <div class="listOfImages items-justify-center rounded-full overflow-hidden bg-[#FFEEEC]">
  24. <img :src="queryParameters.homeImage" class="w-full h-full">
  25. </div>
  26. <div class="flex-1 h-full">
  27. <!-- 商机 -->
  28. <template v-if="queryParameters?.key == 'business'">
  29. <div>
  30. <div class="flex items-center flex-row">
  31. <div class="flex-1 truncate mr-8 titles relative">{{ item.name }}</div>
  32. <div><van-tag type="primary">{{ item.stageValue }}</van-tag></div>
  33. </div>
  34. <div class="flex items-center flex-row mt-1">
  35. <van-text-ellipsis :content="item.customerName"
  36. class="flex-1 text-[#B9B9B9] text-size-small" />
  37. <div class="text-[#B9B9B9] text-size-small" v-if="item.inchargerName">负责人:
  38. <TranslationComponent :openId="item.inchargerName" />
  39. </div>
  40. </div>
  41. </div>
  42. </template>
  43. <!-- 线索 -->
  44. <template v-if="queryParameters?.key == 'thread'">
  45. <div>
  46. <div class="flex items-center flex-row">
  47. <div class="flex-1 truncate mr-8 titles relative">{{ item.clueName }}</div>
  48. <div class="text-[#B9B9B9]">{{ item.customerLevelValue }}</div>
  49. </div>
  50. <div class="flex items-center flex-row mt-1">
  51. <div class="text-[#B9B9B9] text-size-small" v-if="item.inchargerName">负责人:
  52. <TranslationComponent :openId="item.inchargerName" />
  53. </div>
  54. <div></div>
  55. </div>
  56. </div>
  57. </template>
  58. <!-- 客户 -->
  59. <template v-if="queryParameters?.key == 'customer'">
  60. <div>
  61. <div class="flex items-center flex-row">
  62. <div class="flex-1 truncate mr-8 titles relative">{{ item.customName }}</div>
  63. <div class="text-[#B9B9B9]">{{ item.customerLevelValue }}</div>
  64. </div>
  65. <div class="flex items-center flex-row mt-1">
  66. <div class="text-[#B9B9B9] text-size-small" v-if="item.inchargerName">负责人:
  67. <TranslationComponent :openId="item.inchargerName" />
  68. </div>
  69. <div></div>
  70. </div>
  71. </div>
  72. </template>
  73. <!-- 联系人 -->
  74. <template v-if="queryParameters?.key == 'contacts'">
  75. <div>
  76. <div class="flex items-center flex-row">
  77. <div class="flex-1 truncate mr-8 titles relative">{{ item.name }}</div>
  78. <div class="text-[#B9B9B9]">{{ item.position }}</div>
  79. </div>
  80. <div class="flex items-center justify-between flex-row mt-1">
  81. <div class="text-[#B9B9B9] text-size-small w-2/5 truncate" v-if="item.ownerName">客户:{{
  82. item.customName }}</div>
  83. <div class="text-[#B9B9B9] text-size-small w-2/5 truncate text-right" v-if="item.ownerName">负责人:
  84. <TranslationComponent :openId="item.ownerName" />
  85. </div>
  86. </div>
  87. </div>
  88. </template>
  89. <!-- 任务 -->
  90. <template v-if="queryParameters?.key == 'tasks'">
  91. <div>
  92. <div class="flex items-center flex-row">
  93. <div class="flex-1 truncate mr-8 titles relative">{{ item.taskName }}</div>
  94. <div :style="`color: ${fixedFieldTaskStatus.find(subItem => subItem.value == item.status)?.color}`">
  95. {{ fixedFieldTaskStatus.find(subItem => subItem.value == item.status)?.label || '' }}
  96. </div>
  97. </div>
  98. <div class="flex items-center justify-between flex-row mt-1">
  99. <div class="text-[#B9B9B9] text-size-small w-2/5 truncate">
  100. {{ item.businessName || item.orderName || item.clueName || item.customName }}
  101. </div>
  102. <div class="text-[#B9B9B9] text-size-small w-2/5 truncate text-right" v-if="user.company.isSimple != 1">优先级:
  103. {{ fixedFieldPriority.find(subItem => subItem.value == item.priority)?.label || '' }}
  104. </div>
  105. </div>
  106. </div>
  107. </template>
  108. <!-- 产品 -->
  109. <template v-if="queryParameters?.key == 'product'">
  110. <div>
  111. <div class="flex items-center flex-row">
  112. <div class="flex-1 truncate mr-8 titles relative">{{ item.productName }}</div>
  113. <div class="text-[#B9B9B9]">{{ item.status ? '上架' : '下架' }}</div>
  114. </div>
  115. <div class="flex items-center justify-between flex-row mt-1">
  116. <div class="text-[#B9B9B9] text-size-small w-2/5 truncate">负责人:
  117. <TranslationComponent :openId="item.inchargerName" />
  118. </div>
  119. <div class="text-[#B9B9B9] text-size-small w-2/5 truncate text-right">库存:{{
  120. item.inventory || 0 }}</div>
  121. </div>
  122. </div>
  123. </template>
  124. <!-- 合同 -->
  125. <template v-if="queryParameters?.key == 'contract'">
  126. <div>
  127. <div class="flex items-center flex-row">
  128. <div class="flex-1 truncate mr-8 titles relative">{{ item.name }}</div>
  129. <div class="text-[#B9B9B9]">
  130. <span :style="fixedFieldStatusArray[item.status].color">
  131. {{ fixedFieldStatusArray[item.status].label }}
  132. </span>
  133. </div>
  134. </div>
  135. <div class="flex items-center justify-between flex-row mt-1">
  136. <div class="text-[#B9B9B9] text-size-small w-2/6 truncate">{{ item.number }}</div>
  137. <div class="text-[#B9B9B9] text-size-small w-2/3 truncate text-right">
  138. 合同金额¥{{ item.amounts }} / 回款进度 {{ item.payment ? (100 * item.payment / item.amounts).toFixed(1) + '%' : '0%' }}
  139. </div>
  140. </div>
  141. </div>
  142. </template>
  143. <!-- 销售 -->
  144. <template v-if="queryParameters?.key == 'order'">
  145. <div>
  146. <div class="flex items-center flex-row">
  147. <div class="flex-1 truncate mr-8 titles relative">{{ item.orderName }}</div>
  148. <div class="text-[#B9B9B9]">{{ fixedFieldPaymentStatus.find(subItem => subItem.value == item.receivedStatus)?.label || '' }}</div>
  149. </div>
  150. <div class="flex items-center justify-between flex-row mt-1">
  151. <div class="text-[#B9B9B9] text-size-small w-2/6 truncate">{{ item.customName }}</div>
  152. <div class="text-[#B9B9B9] text-size-small w-2/3 truncate text-right">
  153. 订单金额¥{{ item.price }} / 已回款¥ {{ item.receivedPayment }}
  154. </div>
  155. </div>
  156. </div>
  157. </template>
  158. </div>
  159. </div>
  160. <template #right>
  161. <div class="flex items-center h-full bg-white">
  162. <template v-if="!item.inchargerName">
  163. <div class="buttonCircle rounded-full" @click="claimAndClaim(item)" v-if="['business', 'thread',
  164. 'customer', 'product'].includes(queryParameters?.key)">
  165. <img src="/src/assets/image/claimAndClaim.png" class="w-full h-full">
  166. </div>
  167. </template>
  168. <template v-if="item.inchargerName || item.ownerName">
  169. <div class="buttonCircle rounded-full" @click="transfer(item)" v-if="['business', 'thread',
  170. 'customer', 'contacts', 'product', 'order'].includes(queryParameters?.key)">
  171. <img src="/src/assets/image/transfer.png" class="w-full h-full">
  172. </div>
  173. </template>
  174. <template v-if="!item.needPin">
  175. <div class="buttonCircle rounded-full" @click="topMounted(item)">
  176. <img src="/src/assets/image/topMounted.png" class="w-full h-full">
  177. </div>
  178. </template>
  179. <template v-if="item.needPin">
  180. <div class="buttonCircle rounded-full" @click="noTopMounted(item)">
  181. <img src="/src/assets/image/noTopMounted.png" class="w-full h-full">
  182. </div>
  183. </template>
  184. <div class="buttonCircle rounded-full" @click="edit(item)" v-permission="[queryParameters?.jurisdiction?.edit]">
  185. <img src="/src/assets/image/edit.png" class="w-full h-full">
  186. </div>
  187. <div class="buttonCircle rounded-full" @click="deleteRow(item)" v-permission="[queryParameters?.jurisdiction?.delete]">
  188. <img src="/src/assets/image/delete.png" class="w-full h-full">
  189. </div>
  190. </div>
  191. </template>
  192. </van-swipe-cell>
  193. </div>
  194. </van-list>
  195. </van-pull-refresh>
  196. </template>
  197. <template v-else>
  198. <template v-if="!isLoading && listData?.records && !listData.records.length">
  199. <van-empty description="暂无数据" />
  200. </template>
  201. <template v-else>
  202. <van-skeleton title :row="20" class="w-full h-full" />
  203. </template>
  204. </template>
  205. </div>
  206. </div>
  207. <!-- 可拖拽添加 -->
  208. <DragBox>
  209. <div class="addButton" @click="toAddEditor()" v-permission="[queryParameters?.jurisdiction?.newlyAdded]">
  210. <img src="/src/assets/image/add.png" class="w-full h-full" />
  211. </div>
  212. </DragBox>
  213. <!-- 转移弹窗 -->
  214. <van-dialog v-model:show="showDialog" :title="`转移${queryParameters?.name || ''}`" show-cancel-button
  215. @confirm="confirmTransfer" :before-close="dialogCloseBefo">
  216. <van-cell title="转移至" is-link @click="showSelect = true">
  217. <template #value>
  218. <TranslationComponent :openId="dialogSelection.label" />
  219. </template>
  220. </van-cell>
  221. <div class="themeTextColor text-size-small pl-4 pt-2 pb-2">转移后,将看不到此{{ queryParameters?.name || '' }}</div>
  222. </van-dialog>
  223. <!-- select 选择器 -->
  224. <van-popup v-model:show="showSelect" destroy-on-close position="bottom" :style="{ height: '80%' }">
  225. <PullDownSelector :showElement="showSelect" @change="selectChange" />
  226. </van-popup>
  227. </template>
  228. </Page>
  229. </template>
  230. <script setup>
  231. import { ref, onActivated } from 'vue';
  232. import { showConfirmDialog } from 'vant';
  233. import { useLifecycle } from '@hooks/useCommon.js';
  234. import { resetListData, getListFieldKey } from '@components/common/formForm/formCorrespondenceProcessing'
  235. import useShowToast from '@hooks/useToast'
  236. import { GET_CUSTOM_FORM_JSON } from '@hooks/useApi'
  237. import { fixedFieldPaymentStatus, fixedFieldPriority, fixedFieldTaskStatus, fixedFieldStatusArray } from "@utility/defaultData"
  238. import requests from "@common/requests";
  239. import useRouterStore from "@store/useRouterStore.js";
  240. import useFixedData from "@store/useFixedData.js"
  241. import useInfoStore from '@store/useInfoStore'
  242. // import ElementLongPress from "@components/common/elementLongPress.vue";
  243. import DragBox from '@components/common/dragBox.vue';
  244. const TRANSFER = 'transfer';
  245. const DELETE = 'delete';
  246. const EDIT = 'edit';
  247. const TOP_MOUNTED = 'topMounted';
  248. const { toastSuccess, toastFail, toastText } = useShowToast()
  249. const router = useRouterStore()
  250. const fixedData = useFixedData()
  251. const userInfo = useInfoStore()
  252. const user = userInfo.userInfo
  253. const searchVal = ref()
  254. const queryParameters = ref({})
  255. const loadingList = ref(false)
  256. const popUpWindowArray = ref([
  257. { text: '转移', event: TRANSFER, icon: '/src/assets/image/transfer.png', removeModule: ['contacts', 'tasks', 'product', 'contract', 'order'] },
  258. { text: '顶置', event: TOP_MOUNTED, icon: '/src/assets/image/topMounted.png', removeModule: [] },
  259. { text: '编辑', event: EDIT, icon: '/src/assets/image/edit.png', removeModule: [] },
  260. { text: '删除', event: DELETE, icon: '/src/assets/image/delete.png', removeModule: [] },
  261. ])
  262. const showSelect = ref(false)
  263. const showDialog = ref(false)
  264. const refreshing = ref(false);
  265. const isLoading = ref(false);
  266. const finished = ref(false);
  267. const listData = ref({
  268. records: [],
  269. pageIndex: 0,
  270. pageSize: 20,
  271. total: 0,
  272. totalPage: 0,
  273. });
  274. const excessiveData = ref({});
  275. const dialogSelection = ref({});
  276. const timeout = ref(null)
  277. // 按钮触发的事件: row 为当前点击的行数据, item 为当前点击的按钮
  278. function longPress(row, item) {
  279. switch (item.event) {
  280. case EDIT:
  281. edit(row);
  282. break;
  283. case TRANSFER:
  284. transfer(row);
  285. break;
  286. case DELETE:
  287. deleteRow(row);
  288. break;
  289. case TOP_MOUNTED:
  290. topMounted(row);
  291. break;
  292. default:
  293. console.warn('未知的类型', item.event);
  294. break;
  295. }
  296. }
  297. // 编辑事件
  298. function edit(row) {
  299. const formJson = fixedData.formJson[queryParameters.value.key] || []
  300. const formList = resetListData(formJson?.list)
  301. const filedObj = getListFieldKey(formList, row)
  302. let other = {}
  303. if (queryParameters.value.key == 'tasks') {
  304. other = { ...row }
  305. }
  306. toAddEditor({ ...other, ...filedObj, id: row.id })
  307. }
  308. // 转移事件
  309. function transfer(row) {
  310. dialogSelection.value = {}
  311. excessiveData.value = row
  312. showDialog.value = true
  313. }
  314. function confirmTransfer() {
  315. if (!dialogSelection.value.label) {
  316. return toastText('请选择要转移的人员')
  317. }
  318. const { id } = excessiveData.value
  319. const { value } = dialogSelection.value
  320. let formVal = {}
  321. if(queryParameters?.value.key == 'contacts') {
  322. formVal = { id, inchargerId: value, ownerId: value }
  323. } else if(queryParameters?.value.key == 'product' || queryParameters?.value.key == 'order') {
  324. formVal = { id, userId: value }
  325. } else {
  326. formVal = { ids: id, inchargerId: value, }
  327. }
  328. requests.post(queryParameters?.value.transferInterface, { ...formVal }).then((res) => {
  329. toastSuccess('转移成功')
  330. onRefresh(true)
  331. showDialog.value = false
  332. })
  333. }
  334. // 认领
  335. function claimAndClaim(item) {
  336. const { id, name, clueName, customName, productName } = item
  337. const { key } = queryParameters.value
  338. const userId = userInfo.userInfo.id
  339. const formVal = {
  340. [key == 'product' ? 'id' : 'ids']: id,
  341. [key == 'product' ? 'userId' : 'inchargerId']: userId
  342. }
  343. showConfirmDialog({
  344. title: `认领${queryParameters.value.name}`,
  345. message: `确定认领【${name || clueName || customName || productName}】${queryParameters.value.name}吗?`,
  346. }).then(() => {
  347. requests.post(queryParameters?.value.transferInterface, { ...formVal }).then((res) => {
  348. toastSuccess('认领成功')
  349. onRefresh(true)
  350. })
  351. })
  352. }
  353. // 删除事件
  354. function deleteRow(row) {
  355. const { name = '', searchFiled = {}, deteleFiled = '' } = queryParameters.value
  356. const foemVal = { [queryParameters.value.key == 'tasks' ? 'taskIds' : queryParameters.value.key == 'contract' ? 'id' : 'ids']: row.id }
  357. showConfirmDialog({
  358. title: `删除${name}`,
  359. message: `确定删除【${row[searchFiled?.search]}】${name}吗?`,
  360. }).then(() => {
  361. requests.post(deteleFiled, { ...foemVal }).then((res) => {
  362. toastSuccess('删除成功')
  363. onRefresh(true)
  364. }).catch((err) => {
  365. toastFail(err.msg ? err.msg : '删除失败')
  366. })
  367. })
  368. }
  369. // 顶置事件
  370. function topMounted(row) {
  371. requests.post(queryParameters?.value.topMountedInterface, { id: row.id }).then((res) => {
  372. toastSuccess('顶置成功')
  373. onRefresh(true)
  374. })
  375. }
  376. function noTopMounted(row) {
  377. requests.post(queryParameters?.value.cancelTheTopMountedInterface, { id: row.id }).then((res) => {
  378. toastSuccess('取消顶置成功')
  379. onRefresh(true)
  380. })
  381. }
  382. function toDetail(item) {
  383. router.navigateTo({
  384. pathName: 'details',
  385. success: () => {
  386. router.emit('detailParameter', {
  387. routerInfo: JSON.stringify(queryParameters.value),
  388. parameter: JSON.stringify(item)
  389. })
  390. }
  391. })
  392. }
  393. function toAddEditor(value) {
  394. router.navigateTo({
  395. pathName: 'addEditor',
  396. success: () => {
  397. router.emit('addEditorParameter', {
  398. routerInfo: JSON.stringify(queryParameters.value),
  399. filedValue: JSON.stringify(value)
  400. })
  401. }
  402. })
  403. }
  404. function onRefresh(flag = false) {
  405. finished.value = false;
  406. isLoading.value = true;
  407. listData.value.pageIndex = 1;
  408. fetchListData(flag);
  409. }
  410. function onLoad() {
  411. isLoading.value = true;
  412. listData.value.pageIndex++
  413. fetchListData()
  414. }
  415. async function fetchListData(flag = false) {
  416. console.log(listData.value.totalPage, listData.value.pageIndex)
  417. if (
  418. // 如果总页数小于等于现页数,并且不是第一次加载, 或者正在加载数据 直接跳出不请求
  419. listData.value.totalPage < listData.value.pageIndex &&
  420. listData.value.pageIndex !== 1
  421. ) {
  422. finished.value = true;
  423. return;
  424. }
  425. const res = await getListData(flag)
  426. if (res.code === 'ok') {
  427. const list = res.data.data || res.data.records || res.data.record
  428. const total = res.data.total
  429. if (refreshing.value) {
  430. refreshing.value = false;
  431. }
  432. isLoading.value = false;
  433. listData.value.records = (
  434. listData.value.pageIndex === 1 ? [] : listData.value.records
  435. ).concat(list || [])
  436. listData.value.totalPage = Math.ceil(total / listData.value.pageSize)
  437. listData.value.total = +total
  438. }
  439. }
  440. async function getListData(flag = false) {
  441. const url = queryParameters.value.listUrl
  442. if (flag) {
  443. loadingList.value = true
  444. }
  445. let formVal = {
  446. pageIndex: listData.value.pageIndex,
  447. pageSize: listData.value.pageSize,
  448. pageFrom: listData.value.pageSize,
  449. [queryParameters.value?.searchFiled?.search]: searchVal.value,
  450. }
  451. const res = await requests.post(url, { ...formVal }).finally(() => {
  452. if (flag) {
  453. loadingList.value = false
  454. }
  455. })
  456. return res
  457. }
  458. function reloadListData(data) {
  459. queryParameters.value = JSON.parse(data.row || '{}')
  460. refreshing.value = false
  461. isLoading.value = false
  462. finished.value = false
  463. searchVal.value = ''
  464. listData.value = { records: [], pageIndex: 0, pageSize: 20, total: 0, totalPage: 0 }
  465. popUpWindowArray.value = popUpWindowArray.value.filter(item => !item.removeModule.includes(queryParameters.value?.key))
  466. if (!fixedData.formJson[queryParameters.value.key]) {
  467. getFormJson(queryParameters.value)
  468. }
  469. onLoad()
  470. }
  471. function getFormJson(info = {}) {
  472. requests.get(`${GET_CUSTOM_FORM_JSON}/${info.key}`).then(res => {
  473. const { data = [] } = res
  474. fixedData.updateState({
  475. formJson: {
  476. ...fixedData.formJson,
  477. [info.key]: JSON.parse(data[0]?.config)
  478. }
  479. })
  480. })
  481. }
  482. function selectChange(value, label) {
  483. dialogSelection.value = {
  484. value, label
  485. }
  486. showSelect.value = false
  487. }
  488. function dialogCloseBefo(val) {
  489. if (val == 'confirm' && showDialog.value) {
  490. return false
  491. }
  492. return true
  493. }
  494. function chuliReloadListData(data) {
  495. clearTimeout(timeout.value);
  496. timeout.value = setTimeout(() => {
  497. reloadListData(data)
  498. }, 100);
  499. }
  500. function chuliOnRefresh(data) {
  501. clearTimeout(timeout.value);
  502. timeout.value = setTimeout(() => {
  503. onRefresh(true)
  504. }, 100);
  505. }
  506. useLifecycle({
  507. load: () => {
  508. chuliOnRefresh(true)
  509. router.on('moduleListDetailParameter', (data) => {
  510. chuliReloadListData(data)
  511. })
  512. router.eventOn('moduleListRefreshData', (data) => {
  513. chuliOnRefresh(true)
  514. })
  515. }
  516. });
  517. </script>
  518. <style lang='scss' scoped>
  519. .buttonCircle {
  520. width: 37px;
  521. height: 37px;
  522. margin-left: 10px;
  523. &:last-child {
  524. margin-right: 10px;
  525. }
  526. }
  527. .addButton {
  528. width: 60px;
  529. height: 60px;
  530. }
  531. .listContent {
  532. border: 1px solid #F6F6FA;
  533. .listOfImages {
  534. width: 40px;
  535. height: 40px;
  536. margin-right: 10px;
  537. }
  538. }
  539. .headerModeuleList :deep(.van-search__content) {
  540. background: #fff !im\portant;
  541. height: 42px;
  542. align-items: center;
  543. border-radius: 6px;
  544. }
  545. </style>