supply.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. <template>
  2. <div class="index">
  3. <el-row>
  4. <el-col :span="24" class="main" v-loading="loading">
  5. <el-col :span="24" class="one">
  6. <div class="one_left">
  7. <div class="button" @click="toAdd">发布供给</div>
  8. <div class="button" @click="toExpert">导入数据</div>
  9. <!-- <div class="button" @click="toDownload">导出数据</div> -->
  10. <custom-export table="Supply"></custom-export>
  11. </div>
  12. <div class="one_right">
  13. <el-input v-model="searchForm.name" style="width: 250px" size="large" placeholder="搜索" @change="search" :suffix-icon="Search" />
  14. </div>
  15. </el-col>
  16. <el-col :span="24" class="two">
  17. <el-table :data="list" style="width: 100%" size="large" :header-cell-style="{ backgroundColor: '#edf3ff' }">
  18. <template #empty>
  19. <el-empty description="暂无数据" />
  20. </template>
  21. <el-table-column prop="name" align="center" label="供给名称" />
  22. <el-table-column prop="start_time" align="center" label="开始时间" width="180" />
  23. <el-table-column prop="end_time" align="center" label="结束时间" width="180" />
  24. <el-table-column prop="status" align="center" label="状态" width="180">
  25. <template #default="scope">
  26. <div>{{ getDict(scope.row.status, 'status') }}</div>
  27. </template>
  28. </el-table-column>
  29. <el-table-column align="center" label="操作" width="180">
  30. <template #default="{ row }">
  31. <el-link v-if="row.status == '-2'" :underline="false" type="warning" size="mini" @click="toExam(row)" style="margin-right: 10px">提交审核</el-link>
  32. <el-link :underline="false" type="primary" size="mini" @click="toEdit(row)" style="margin-right: 10px">修改</el-link>
  33. <el-link :underline="false" type="danger" size="mini" @click="toDelete(row)"> 删除 </el-link>
  34. </template>
  35. </el-table-column>
  36. </el-table>
  37. </el-col>
  38. <el-col :span="24" class="thr">
  39. <el-pagination background layout="prev, pager, next" :total="total" :page-size="limit" v-model:current-page="currentPage" @current-change="changePage" @size-change="sizeChange" />
  40. </el-col>
  41. </el-col>
  42. </el-row>
  43. <el-dialog v-model="dialog.show" :title="dialog.title" :destroy-on-close="false" @close="toClose" :width="width">
  44. <el-row>
  45. <el-col :span="24" v-if="dialog.type == '1'">
  46. <custom-form v-model="form" :fields="formFields" :rules="rules" @save="toSave" @draftSave="toDraftSave">
  47. <template #is_use>
  48. <el-radio v-for="i in isUseList" :key="i.id" :label="i.value">{{ i.label }}</el-radio>
  49. </template>
  50. <template #field>
  51. <el-option v-for="i in fieldList" :key="i.id" :label="i.label" :value="i.label"></el-option>
  52. </template>
  53. <template #urgent>
  54. <el-option v-for="i in urgentList" :key="i.id" :label="i.label" :value="i.value"></el-option>
  55. </template>
  56. <template #method>
  57. <el-option v-for="i in methodList" :key="i.id" :label="i.label" :value="i.label"></el-option>
  58. </template>
  59. <template #industry>
  60. <el-option v-for="i in sectorList" :key="i.id" :label="i.title" :value="i.title"></el-option>
  61. </template>
  62. <template #area>
  63. <el-cascader v-model="form.area" :props="{ value: 'name', label: 'name' }" :options="cityList" style="width: 100%" />
  64. </template>
  65. <template #tags>
  66. <el-select v-model="form.tags" multiple filterable allow-create default-first-option :reserve-keyword="false" placeholder="请选择标签" style="width: 100%">
  67. <el-option v-for="item in tagsList" :key="item.id" :label="item.title" :value="item.title" />
  68. </el-select>
  69. </template>
  70. </custom-form>
  71. </el-col>
  72. <transition name="why">
  73. <el-col :span="24" v-if="dialog.type == '2'" class="dialog" v-loading="isLoading" element-loading-text="智能推荐中..." :element-loading-svg="svg" element-loading-svg-view-box="-10,-10,50,50">
  74. <el-empty v-if="demandtotal == 0" description="暂无数据" />
  75. <div class="list" v-for="(item, index) in demandList" :key="index" @click="toView(item)">
  76. <h2 class="name textMore">
  77. <span>{{ item.name || '暂无' }}</span>
  78. </h2>
  79. <div class="other">
  80. <span class="other_1">{{ getDict(item.urgent, 'urgent') || '暂无' }}</span>
  81. <div class="other_2">
  82. <span>应用行业:</span>
  83. {{ item.field || '暂无' }}
  84. </div>
  85. <div class="other_2">
  86. <span>资金预算:</span>
  87. {{ item.money || '面议' }}
  88. </div>
  89. <div class="other_2">
  90. <span>推荐指数:</span>
  91. <el-rate size="large" v-model="item._recommend" disabled show-score text-color="#ff9900" :score-template="`${item._recommend} 星`" />
  92. </div>
  93. <div class="other_2 textOne">
  94. <el-icon color="#0085f5"><Location /></el-icon>
  95. {{ getArea(item.area) }}
  96. </div>
  97. </div>
  98. </div>
  99. <el-col :span="24" class="page">
  100. <el-pagination background layout="prev, pager, next" :total="demandtotal" :page-size="demandlimit" v-model:current-page="dcurrentPage" @current-change="dchangePage" @size-change="dsizeChange" />
  101. </el-col>
  102. </el-col>
  103. </transition>
  104. <el-col :span="24" class="dialog_four" v-if="dialog.type == '4'">
  105. <el-row justify="center">
  106. <el-col :span="16">
  107. <el-steps style="max-width: 600px" :active="importActive" align-center>
  108. <el-step title="下载导入模板">
  109. <template #description v-if="importActive == 0">
  110. <el-button type="primary" size="mini" @click="toTemplate">下载导入模板</el-button>
  111. </template>
  112. </el-step>
  113. <el-step title="上传导入模板" description="上传导入模板">
  114. <template #description v-if="importActive == 1">
  115. <el-upload action="/files/web/cxyy_import/upload" :show-file-list="false" :on-success="onSuccess" accept=".xlsx">
  116. <el-button type="primary" size="mini">上传导入文件</el-button>
  117. </el-upload>
  118. </template>
  119. </el-step>
  120. <el-step title="确定导入" description="确定导入">
  121. <template #description v-if="importActive == 2">
  122. <el-button type="primary" size="mini" @click="onImport">确定</el-button>
  123. </template>
  124. </el-step>
  125. </el-steps>
  126. </el-col>
  127. </el-row>
  128. </el-col>
  129. </el-row>
  130. </el-dialog>
  131. </div>
  132. </template>
  133. <script setup>
  134. import { Search } from '@element-plus/icons-vue'
  135. import { cloneDeep, get } from 'lodash-es'
  136. const $checkRes = inject('$checkRes')
  137. import { UserStore } from '@/store/user'
  138. const userStore = UserStore()
  139. const user = computed(() => userStore.user)
  140. // 接口
  141. import { SupplyStore } from '@/store/api/platform/supply'
  142. import { DictDataStore } from '@/store/api/system/dictData'
  143. import { TagsStore } from '@/store/api/system/tags'
  144. import { SectorStore } from '@/store/api/platform/sector'
  145. import { RegionStore } from '@/store/api/system/region'
  146. import { UtilStore } from '@/store/api/util'
  147. import { EsStore } from '@/store/api/es'
  148. const esStore = EsStore()
  149. const utilStore = UtilStore()
  150. const regionStore = RegionStore()
  151. const store = SupplyStore()
  152. const dictDataStore = DictDataStore()
  153. const tagsStore = TagsStore()
  154. const sectorStore = SectorStore()
  155. // 加载中
  156. const loading = ref(false)
  157. const isLoading = ref(false)
  158. const svg = ref(`
  159. <path class="path" d="
  160. M 30 15
  161. L 28 17
  162. M 25.61 25.61
  163. A 15 15, 0, 0, 1, 15 30
  164. A 15 15, 0, 1, 1, 27.99 7.5
  165. L 15 15
  166. " style="stroke-width: 4px; fill: rgba(0, 0, 0, 0)"/>
  167. `)
  168. const router = useRouter()
  169. const searchForm = ref({})
  170. // 列表
  171. const list = ref([])
  172. let skip = 0
  173. let limit = inject('limit')
  174. const total = ref(0)
  175. const currentPage = ref(1)
  176. // 字典表
  177. const isUseList = ref([])
  178. const statusList = ref([])
  179. const methodList = ref([])
  180. const urgentList = ref([])
  181. const fieldList = ref([])
  182. const cityList = ref([])
  183. const supplyList = ref([])
  184. const tagsList = ref([])
  185. const sectorList = ref([])
  186. // 推荐需求列表
  187. const demandList = ref([])
  188. let demandskip = 0
  189. let demandlimit = 4
  190. const demandtotal = ref(0)
  191. const key = ref('')
  192. const width = ref('50%')
  193. const form = ref({ time: [] })
  194. const dialog = ref({ type: '1', show: false, title: '发布供给' })
  195. const formFields = ref([
  196. { label: '供给名称', model: 'name' },
  197. { label: '标签', model: 'tags', custom: true, mark: 'tags' },
  198. { label: '行业', model: 'industry', type: 'select' },
  199. { label: '行业领域', model: 'field', type: 'select' },
  200. { label: '供给紧急度', model: 'urgent', type: 'select', mark: 'dict', code: 'urgent' },
  201. { label: '合作方式', model: 'method', type: 'selectMany', mark: 'dict', code: 'method' },
  202. { label: '价格(万元)', model: 'money' },
  203. { label: '项目来源', model: 'source' },
  204. { label: '供给地区', model: 'area', custom: true, mark: 'area' },
  205. { label: '发布时间', model: 'time', type: 'daterange', mark: 'time' },
  206. { label: '是否公开', model: 'is_use', type: 'radio', mark: 'dict', code: 'isUse' },
  207. { label: '简介', model: 'brief', type: 'textarea' }
  208. ])
  209. const rules = reactive({
  210. name: [{ required: true, message: '请输入供给名称', trigger: 'blur' }],
  211. field: [{ required: true, message: '请输入行业领域', trigger: 'blur' }],
  212. urgent: [{ required: true, message: '请选择供给紧急度', trigger: 'blur' }],
  213. method: [{ required: true, message: '请选择合作方式', trigger: 'blur' }],
  214. money: [{ required: true, message: '请输入价格', trigger: 'blur' }],
  215. area: [{ required: true, message: '请选择供给地区', trigger: 'blur' }],
  216. time: [{ required: true, message: '请选择发布时间', trigger: 'blur' }]
  217. })
  218. // 导出文件
  219. const checkAll = ref(false)
  220. const checkedExport = ref([])
  221. const isIndeterminate = ref(true)
  222. // 导入文件
  223. const importActive = ref(0)
  224. const url = ref('')
  225. // 请求
  226. onMounted(async () => {
  227. loading.value = true
  228. await searchOther()
  229. await search()
  230. loading.value = false
  231. })
  232. const search = async (query = { skip, limit }) => {
  233. skip = query.skip
  234. limit = query.limit
  235. const info = {
  236. skip: query.skip,
  237. limit: query.limit,
  238. user: user.value.id,
  239. ...searchForm.value
  240. }
  241. const res = await store.query(info)
  242. if (res.errcode == '0') {
  243. list.value = res.data
  244. total.value = res.total
  245. }
  246. }
  247. const searchOther = async () => {
  248. let result
  249. // 是否使用
  250. result = await dictDataStore.query({ code: 'isUse', is_use: '0' })
  251. if ($checkRes(result)) isUseList.value = result.data
  252. // 状态
  253. result = await dictDataStore.query({ code: 'examStatus', is_use: '0' })
  254. if ($checkRes(result)) statusList.value = result.data
  255. // 合作方式
  256. result = await dictDataStore.query({ code: 'method', is_use: '0' })
  257. if ($checkRes(result)) methodList.value = result.data
  258. // 需求紧急度
  259. result = await dictDataStore.query({ code: 'urgent', is_use: '0' })
  260. if ($checkRes(result)) urgentList.value = result.data
  261. // 技术领域
  262. result = await dictDataStore.query({ code: 'field', is_use: '0' })
  263. if ($checkRes(result)) fieldList.value = result.data
  264. // 需求状态
  265. result = await dictDataStore.query({ code: 'supplyStatus', is_use: '0' })
  266. if ($checkRes(result)) supplyList.value = result.data
  267. // 标签
  268. result = await tagsStore.query({ is_use: '0' })
  269. if ($checkRes(result)) tagsList.value = result.data
  270. // 行业
  271. result = await sectorStore.query({ is_use: '0' })
  272. if ($checkRes(result)) sectorList.value = result.data
  273. // 城市
  274. result = await regionStore.area({ level: 'province', code: 22 })
  275. if ($checkRes(result)) cityList.value = result.data
  276. }
  277. // 字典数据转换
  278. const getDict = (data, model) => {
  279. if (data) {
  280. let res
  281. if (model == 'status') res = statusList.value.find((f) => f.value == data)
  282. else if (model == 'urgent') res = urgentList.value.find((f) => f.value == data)
  283. return get(res, 'label')
  284. }
  285. }
  286. // 添加
  287. const toAdd = () => {
  288. dialog.value = { type: '1', show: true, title: '发布供给' }
  289. }
  290. // 修改
  291. const toEdit = (data) => {
  292. data.time = [data.start_time, data.end_time]
  293. form.value = data
  294. dialog.value = { type: '1', show: true, title: '修改供给' }
  295. }
  296. // 删除
  297. const toDelete = (data) => {
  298. ElMessageBox.confirm(`您确认删除${data.name}该数据?`, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
  299. .then(async () => {
  300. const res = await store.del(data.id)
  301. if ($checkRes(res, true)) {
  302. search({ skip, limit })
  303. }
  304. })
  305. .catch(() => {})
  306. }
  307. const toSave = async () => {
  308. const data = cloneDeep(form.value)
  309. const other = { status: '0', user: user.value.id }
  310. if (data.time && data.time.length > 1) {
  311. data.start_time = data.time[0]
  312. data.end_time = data.time[1]
  313. }
  314. delete data.time
  315. let res
  316. if (get(data, 'id')) res = await store.update({ ...data, ...other })
  317. else res = await store.create({ ...data, ...other })
  318. if ($checkRes(res, true)) {
  319. key.value = form.value.name
  320. await search({ skip, limit })
  321. await toClose()
  322. await searchDemand({ demandskip, demandlimit })
  323. width.value = '90%'
  324. dialog.value = { type: '2', show: true, title: '相关需求推荐' }
  325. }
  326. }
  327. const toDraftSave = async () => {
  328. const data = cloneDeep(form.value)
  329. const other = { status: '-2', user: user.value.id }
  330. if (data.time && data.time.length > 1) {
  331. data.start_time = data.time[0]
  332. data.end_time = data.time[1]
  333. }
  334. delete data.time
  335. let res
  336. if (get(data, 'id')) res = await store.update({ ...data, ...other })
  337. else res = await store.create({ ...data, ...other })
  338. if ($checkRes(res, true)) {
  339. key.value = form.value.name
  340. await search({ skip, limit })
  341. await toClose()
  342. await searchDemand({ demandskip, demandlimit })
  343. width.value = '90%'
  344. dialog.value = { type: '2', show: true, title: '相关需求推荐' }
  345. }
  346. }
  347. // 审核保存
  348. const toExam = async (row) => {
  349. ElMessageBox.confirm(`您确认保存并提交审核该数据?`, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
  350. .then(async () => {
  351. const data = cloneDeep(row)
  352. let res = await store.update({ id: data.id, status: '0', user: user.value.id })
  353. if ($checkRes(res, true)) {
  354. key.value = row.name
  355. await search({ skip, limit })
  356. await toClose()
  357. await searchDemand({ demandskip, demandlimit })
  358. width.value = '90%'
  359. dialog.value = { type: '2', show: true, title: '相关需求推荐' }
  360. }
  361. })
  362. .catch(() => {})
  363. }
  364. // 需求列表查询
  365. const searchDemand = async (query = { demandskip, demandlimit }) => {
  366. demandskip = query.demandskip
  367. demandlimit = query.demandlimit
  368. isLoading.value = true
  369. const info = { skip: demandskip, limit: demandlimit, keyword: key.value }
  370. const res = await esStore.demand(info)
  371. if (res.errcode == '0') {
  372. demandList.value = res.data
  373. demandtotal.value = res.total
  374. }
  375. setTimeout(() => {
  376. isLoading.value = false
  377. }, 3000) // 假设3秒后加载完成
  378. }
  379. const dcurrentPage = ref(1)
  380. // 分页
  381. const dchangePage = (page = dcurrentPage.value) => {
  382. searchDemand({ demandskip: (page - 1) * demandlimit, demandlimit: demandlimit })
  383. }
  384. const dsizeChange = (limits) => {
  385. demandlimit = limits
  386. dcurrentPage.value = 1
  387. searchDemand({ demandskip: 0, demandlimit: demandlimit })
  388. }
  389. // 查看详情
  390. const toView = (item) => {
  391. router.push({ path: '/demand/detail', query: { id: item.id || item._id } })
  392. }
  393. // 地区
  394. const getArea = (data) => {
  395. if (data) return data.join('-')
  396. else return '暂无地区'
  397. }
  398. const toClose = () => {
  399. checkedExport.value = []
  400. checkAll.value = false
  401. form.value = { time: [] }
  402. width.value = '50%'
  403. dialog.value = { show: false }
  404. }
  405. // 全选
  406. const handleCheckAllChange = (val) => {
  407. checkedExport.value = val ? formFields.value : []
  408. isIndeterminate.value = false
  409. }
  410. // 选择
  411. const checkedExportChange = (value) => {
  412. let checkedCount = value.length
  413. checkAll.value = checkedCount === formFields.value.length
  414. isIndeterminate.value = checkedCount > 0 && checkedCount < formFields.value.length
  415. }
  416. // 导入数据
  417. const toExpert = () => {
  418. dialog.value = { type: '4', show: true, title: '导入数据' }
  419. }
  420. // 导出数据转到组件
  421. // 下载导入模板
  422. const toTemplate = () => {
  423. importActive.value = importActive.value + 1
  424. window.open('/产学研用供给模板.xlsx')
  425. }
  426. // 上传Excel
  427. const onSuccess = async (response) => {
  428. importActive.value = importActive.value + 1
  429. url.value = response.uri
  430. }
  431. // 确定导出
  432. const onImport = async () => {
  433. const msgbox = ElMessage({ message: '正在导入中,请稍后...', center: true, duration: 0 })
  434. try {
  435. const res = await utilStore.toImport({ url: url.value })
  436. if (res.errcode == '0') {
  437. if (res.data[0].errorList) {
  438. ElMessageBox.alert(res.data[0].errorList, '错误提示', {
  439. confirmButtonText: 'OK'
  440. })
  441. } else {
  442. ElMessage({ message: '导入成功', type: 'success' })
  443. }
  444. await search({ skip, limit })
  445. await toClose()
  446. }
  447. } catch (error) {
  448. console.error(error)
  449. } finally {
  450. msgbox.close()
  451. }
  452. }
  453. // 分页
  454. const changePage = (page = currentPage.value) => {
  455. search({ skip: (page - 1) * limit, limit: limit })
  456. }
  457. const sizeChange = (limits) => {
  458. limit = limits
  459. currentPage.value = 1
  460. search({ skip: 0, limit: limit })
  461. }
  462. </script>
  463. <style scoped lang="scss">
  464. .main {
  465. height: 85vh;
  466. .one {
  467. height: 50px;
  468. display: flex;
  469. justify-content: space-between;
  470. align-items: center;
  471. margin: 0 0 10px 0;
  472. .one_left {
  473. display: flex;
  474. font-size: 16px;
  475. .button {
  476. background: #1875df;
  477. padding: 0 10px;
  478. height: 30px;
  479. color: #fff;
  480. line-height: 30px;
  481. text-align: center;
  482. cursor: default;
  483. margin: 0 10px 0 0;
  484. }
  485. }
  486. }
  487. .thr {
  488. display: flex;
  489. justify-content: center;
  490. margin: 20px 0 0 0;
  491. }
  492. }
  493. .why-enter-from,
  494. .why-leave-to {
  495. opacity: 0;
  496. transform: scale(0.6);
  497. }
  498. .why-enter-to,
  499. .why-leave-from {
  500. opacity: 1;
  501. transform: scale(1);
  502. }
  503. .why-enter-active,
  504. .why-leave-active {
  505. transition: all 2s ease;
  506. }
  507. .dialog {
  508. display: flex;
  509. justify-content: space-between;
  510. flex-wrap: wrap;
  511. margin-top: 20px;
  512. .list {
  513. position: relative;
  514. margin-right: 10px;
  515. margin-bottom: 10px;
  516. width: 380px;
  517. box-shadow: 0 0 13px 0 rgba(5, 88, 219, 0.18);
  518. .name {
  519. padding: 10px 20px;
  520. width: 380px;
  521. height: 71px;
  522. background-color: #dce5ff;
  523. font-size: 16px;
  524. line-height: 24px;
  525. color: #0d0d0d;
  526. display: flex;
  527. align-items: center;
  528. }
  529. .other {
  530. padding: 10px 20px 20px;
  531. .other_1 {
  532. padding: 0 10px;
  533. height: 25px;
  534. background-color: #e6f2fd;
  535. border-radius: 2px;
  536. border: solid 1px #cae0f5;
  537. font-size: $global-font-size-14;
  538. line-height: 25px;
  539. color: #0085f5;
  540. }
  541. .other_2 {
  542. display: flex;
  543. align-items: center;
  544. margin-top: 15px;
  545. font-size: $global-font-size-16;
  546. span {
  547. color: #909090;
  548. }
  549. .state {
  550. position: absolute;
  551. right: 20px;
  552. bottom: 15px;
  553. display: inline-block;
  554. vertical-align: middle;
  555. padding: 0 20px;
  556. height: 30px;
  557. line-height: 30px;
  558. background-image: linear-gradient(90deg, #ff8a00 0, #ff5a00 100%), linear-gradient(#ff7800, #ff7800);
  559. background-blend-mode: normal, normal;
  560. border-radius: 14px;
  561. border: solid 1px #e5e5e5;
  562. color: #fff;
  563. }
  564. }
  565. }
  566. }
  567. .list:hover {
  568. box-shadow: 0 0 5px 0 $global-color-107;
  569. .name {
  570. background-color: $global-color-107;
  571. color: $global-color-fff;
  572. }
  573. }
  574. .page {
  575. margin: 10px 0;
  576. display: flex;
  577. justify-content: center;
  578. }
  579. }
  580. .dialog_thr {
  581. padding: 20px;
  582. .one {
  583. margin: 0 0 10px 0;
  584. }
  585. .two {
  586. margin: 0 0 10px 0;
  587. :deep(.el-checkbox) {
  588. padding: 0 0 20px 0;
  589. }
  590. }
  591. .btn {
  592. text-align: center;
  593. margin: 20px 0 0 0;
  594. }
  595. }
  596. .dialog_four {
  597. padding: 20px;
  598. }
  599. </style>