综合办公系统
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. <template>
  2. <view class="container">
  3. <!-- 搜索栏 -->
  4. <view class="search-box">
  5. <view class="search-header" @tap="toggleSearch">
  6. <text class="search-title">搜索条件</text>
  7. <text class="search-icon">{{ isSearchExpanded ? '收起' : '展开' }}</text>
  8. </view>
  9. <view class="search-content" v-if="isSearchExpanded">
  10. <view class="search-item">
  11. <text>项目编号</text>
  12. <input v-model="queryParams.projectNumber" placeholder="请输入项目编号" @confirm="handleQuery" />
  13. </view>
  14. <view class="search-item">
  15. <text>项目名称</text>
  16. <input v-model="queryParams.projectName" placeholder="请输入项目名称" @confirm="handleQuery" />
  17. </view>
  18. <view class="search-item">
  19. <text>项目负责人</text>
  20. <picker mode="selector" :range="userList" range-key="text" @change="handleLeaderChange">
  21. <view class="picker">
  22. <text>{{ selectedLeader || '请选择项目负责人' }}</text>
  23. </view>
  24. </picker>
  25. </view>
  26. <view class="search-item">
  27. <text>承担部门</text>
  28. <picker mode="selector" :range="deptList" range-key="text" @change="handleDeptChange">
  29. <view class="picker">
  30. <text>{{ selectedDept || '请选择承担部门' }}</text>
  31. </view>
  32. </picker>
  33. </view>
  34. <view class="search-item">
  35. <text>其他关键字</text>
  36. <input v-model="queryParams.queryString" placeholder="请输入关键字" @confirm="handleQuery" />
  37. </view>
  38. <view class="search-buttons">
  39. <button type="primary" size="mini" @tap="handleQuery">搜索</button>
  40. <button type="default" size="mini" @tap="resetQuery">重置</button>
  41. </view>
  42. </view>
  43. </view>
  44. <!-- 项目列表 -->
  45. <view class="list-box">
  46. <view class="list-header">
  47. <text class="title">测绘项目列表</text>
  48. <!-- <view class="header-btns">
  49. <button type="primary" size="mini" @tap="handleRegister">登记项目</button>
  50. <button type="default" size="mini" @tap="handleExport">导出项目</button>
  51. </view> -->
  52. </view>
  53. <scroll-view scroll-y class="list-scroll" id="projectList" :scroll-into-view="scrollToId"
  54. :scroll-with-animation="true" @scrolltolower="loadMore" :refresher-enabled="true"
  55. :refresher-triggered="isRefreshing" @refresherrefresh="onRefresh" @scroll="onScroll">
  56. <view class="list-content">
  57. <view id="top"></view>
  58. <view class="project-card" v-for="(item, index) in projectList" :key="index">
  59. <!-- 卡片头部 -->
  60. <view class="card-header">
  61. <view class="header-left">
  62. <text class="project-name">{{ item.projectName }}</text>
  63. <text class="status-tag" :class="item.isFinished === '0' ? 'success' : 'warning'">
  64. {{ item.isFinished === '0' ? '进行中' : '已结束' }}
  65. </text>
  66. </view>
  67. <text class="level-tag" :class="item.projectLevel === '0' ? 'info' : 'error'">
  68. {{ item.projectLevel === '0' ? '一般项目' : '重大项目' }}
  69. </text>
  70. </view>
  71. <!-- 卡片内容 -->
  72. <view class="card-content">
  73. <view class="info-row">
  74. <text class="label">项目编号:</text>
  75. <text class="value">{{ item.projectNumber }}</text>
  76. </view>
  77. <view class="info-row">
  78. <text class="label">合同编码:</text>
  79. <text class="value">{{ item.contract ? item.contract.contractCode : '未关联' }}</text>
  80. </view>
  81. <view class="info-row">
  82. <text class="label">承担部门:</text>
  83. <text class="value">{{ item.undertakingDeptName }}</text>
  84. </view>
  85. <view class="info-row">
  86. <text class="label">项目负责人:</text>
  87. <text class="value">{{ item.projectLeaderUser ? item.projectLeaderUser.nickName : '' }}</text>
  88. </view>
  89. <view class="info-row">
  90. <text class="label">项目类型:</text>
  91. <text class="value">{{ item.projectType }}</text>
  92. </view>
  93. <view class="info-row">
  94. <text class="label">项目进度:</text>
  95. <view class="progress-box">
  96. <progress :percent="item.percentage" :activeColor="getProgressColor(item.percentage)" />
  97. <text class="progress-text">{{ item.percentage }}%</text>
  98. </view>
  99. </view>
  100. </view>
  101. <!-- 卡片底部 -->
  102. <view class="card-footer">
  103. <button type="primary" size="mini" @tap="handleView(item)">查看</button>
  104. </view>
  105. </view>
  106. </view>
  107. </scroll-view>
  108. <!-- 加载更多 -->
  109. <view class="load-more" v-if="projectList.length > 0">
  110. <text v-if="hasMore">上拉加载更多</text>
  111. <text v-else>没有更多数据了</text>
  112. </view>
  113. <!-- 回到顶部按钮 -->
  114. <view class="back-to-top" v-if="showBackToTop" @tap="scrollToTop">
  115. <text class="iconfont">
  116. <uv-icon size="28" name="arrow-upward" color="#fcfcfc"></uv-icon>
  117. </text>
  118. </view>
  119. </view>
  120. </view>
  121. </template>
  122. <script>
  123. import { listProject, listProjectFuzzy, delProject } from "@/api/oa/project/project";
  124. import { getProjectProgress } from "@/api/oa/project/projectProgress";
  125. import { listDept } from '@/api/system/dept';
  126. import { listUser } from '@/api/system/user';
  127. import { checkPermi } from '@/utils/permission';
  128. export default {
  129. data() {
  130. return {
  131. isSearchExpanded: false,
  132. queryParams: {
  133. pageNum: 1,
  134. pageSize: 10,
  135. projectNumber: '',
  136. projectLeader: '',
  137. projectName: '',
  138. queryString: '',
  139. undertakingDept: ''
  140. },
  141. projectList: [],
  142. total: 0,
  143. loading: false,
  144. userList: [],
  145. deptList: [],
  146. selectedLeader: '',
  147. selectedDept: '',
  148. hasMore: true,
  149. isRefreshing: false,
  150. showBackToTop: false,
  151. scrollTop: 0,
  152. scrollToId: ''
  153. }
  154. },
  155. onLoad() {
  156. this.getList();
  157. this.getDeptList();
  158. this.getUserList();
  159. },
  160. methods: {
  161. toggleSearch() {
  162. this.isSearchExpanded = !this.isSearchExpanded;
  163. },
  164. resetQuery() {
  165. this.queryParams = {
  166. pageNum: 1,
  167. pageSize: 10,
  168. projectNumber: '',
  169. projectLeader: '',
  170. projectName: '',
  171. queryString: '',
  172. undertakingDept: ''
  173. };
  174. this.selectedLeader = '';
  175. this.selectedDept = '';
  176. this.handleQuery();
  177. },
  178. async getList() {
  179. this.loading = true;
  180. try {
  181. const deptId = this.$store.getters.deptId;
  182. if (deptId > 106 && deptId != 109) {
  183. let response = await checkPermi(['oa:allproject:query'])
  184. if (response == false) {
  185. this.queryParams.undertakingDept = deptId
  186. }
  187. }
  188. const response = this.queryParams.queryString ?
  189. await listProjectFuzzy(this.queryParams) :
  190. await listProject(this.queryParams);
  191. if (this.queryParams.pageNum === 1) {
  192. this.projectList = response.rows;
  193. } else {
  194. this.projectList = [...this.projectList, ...response.rows];
  195. }
  196. this.total = response.total;
  197. this.hasMore = this.projectList.length < this.total;
  198. for (let project of this.projectList) {
  199. const res = await getProjectProgress(project.projectId);
  200. if (res.data && res.data.length > 0) {
  201. this.$set(project, 'percentage', Number(res.data[res.data.length - 1].percentage));
  202. } else {
  203. this.$set(project, 'percentage', 0);
  204. }
  205. if (project.isFinished === '1') {
  206. this.$set(project, 'percentage', 100);
  207. }
  208. }
  209. } catch (error) {
  210. uni.showToast({
  211. title: '获取数据失败',
  212. icon: 'none'
  213. });
  214. } finally {
  215. this.loading = false;
  216. this.isRefreshing = false;
  217. }
  218. },
  219. async getDeptList() {
  220. try {
  221. const res = await listDept({});
  222. this.deptList = res.data.map(item => ({
  223. value: item.deptId,
  224. text: item.deptName
  225. }));
  226. } catch (error) {
  227. uni.showToast({
  228. title: '获取部门列表失败',
  229. icon: 'none'
  230. });
  231. }
  232. },
  233. async getUserList() {
  234. try {
  235. const res = await listUser({ pageNum: 1, pageSize: 9999 });
  236. this.userList = res.rows.map(item => ({
  237. value: item.userId,
  238. text: item.nickName
  239. }));
  240. } catch (error) {
  241. uni.showToast({
  242. title: '获取用户列表失败',
  243. icon: 'none'
  244. });
  245. }
  246. },
  247. handleQuery() {
  248. this.queryParams.pageNum = 1;
  249. this.getList();
  250. },
  251. handleLeaderChange(e) {
  252. const index = e.detail.value;
  253. this.queryParams.projectLeader = this.userList[index].value;
  254. this.selectedLeader = this.userList[index].text;
  255. this.handleQuery();
  256. },
  257. handleDeptChange(e) {
  258. const index = e.detail.value;
  259. this.queryParams.undertakingDept = this.deptList[index].value;
  260. this.selectedDept = this.deptList[index].text;
  261. this.handleQuery();
  262. },
  263. handleView(row) {
  264. uni.navigateTo({
  265. url: `/pages/project/projectInfo?projectId=${row.projectId}`
  266. });
  267. },
  268. handleRegister() {
  269. uni.navigateTo({
  270. url: '/pages/project/register'
  271. });
  272. },
  273. handleExport() {
  274. uni.showToast({
  275. title: '导出功能开发中',
  276. icon: 'none'
  277. });
  278. },
  279. loadMore() {
  280. if (this.hasMore) {
  281. this.queryParams.pageNum++;
  282. this.getList();
  283. }
  284. },
  285. onRefresh() {
  286. this.isRefreshing = true;
  287. this.queryParams.pageNum = 1;
  288. this.getList();
  289. },
  290. getProgressColor(percentage) {
  291. if (!percentage) return '#ff4d4f';
  292. if (percentage <= 20) return '#ff4d4f';
  293. if (percentage <= 50) return '#faad14';
  294. if (percentage <= 80) return '#1890ff';
  295. return '#52c41a';
  296. },
  297. onScroll(e) {
  298. this.scrollTop = e.detail.scrollTop;
  299. this.showBackToTop = this.scrollTop > 300;
  300. },
  301. scrollToTop() {
  302. this.scrollToId = 'top';
  303. setTimeout(() => {
  304. this.scrollToId = '';
  305. }, 300);
  306. }
  307. }
  308. }
  309. </script>
  310. <style>
  311. .container {
  312. padding: 20rpx;
  313. background-color: #f5f5f5;
  314. height: 100vh;
  315. display: flex;
  316. flex-direction: column;
  317. }
  318. .search-box {
  319. background-color: #fff;
  320. border-radius: 8rpx;
  321. margin-bottom: 20rpx;
  322. overflow: hidden;
  323. }
  324. .search-header {
  325. padding: 20rpx;
  326. display: flex;
  327. justify-content: space-between;
  328. align-items: center;
  329. border-bottom: 1px solid #ebeef5;
  330. }
  331. .search-title {
  332. font-size: 32rpx;
  333. font-weight: bold;
  334. color: #333;
  335. }
  336. .search-icon {
  337. font-size: 28rpx;
  338. color: #909399;
  339. }
  340. .search-content {
  341. padding: 20rpx;
  342. }
  343. .search-item {
  344. margin-bottom: 20rpx;
  345. }
  346. .search-item text {
  347. display: block;
  348. margin-bottom: 10rpx;
  349. font-size: 28rpx;
  350. color: #333;
  351. }
  352. .search-item input,
  353. .search-item .picker {
  354. width: 100%;
  355. height: 80rpx;
  356. padding: 0 20rpx;
  357. border: 1px solid #dcdfe6;
  358. border-radius: 4rpx;
  359. box-sizing: border-box;
  360. }
  361. .search-item .picker {
  362. line-height: 80rpx;
  363. color: #606266;
  364. }
  365. .search-buttons {
  366. display: flex;
  367. gap: 20rpx;
  368. margin-top: 20rpx;
  369. }
  370. .search-buttons button {
  371. flex: 1;
  372. }
  373. .list-box {
  374. flex: 1;
  375. background-color: #fff;
  376. border-radius: 8rpx;
  377. display: flex;
  378. flex-direction: column;
  379. overflow: hidden;
  380. }
  381. .list-header {
  382. padding: 20rpx;
  383. display: flex;
  384. justify-content: space-between;
  385. align-items: center;
  386. border-bottom: 1px solid #ebeef5;
  387. }
  388. .title {
  389. font-size: 32rpx;
  390. font-weight: bold;
  391. }
  392. .header-btns {
  393. display: flex;
  394. gap: 20rpx;
  395. }
  396. .list-scroll {
  397. flex: 1;
  398. height: 0;
  399. }
  400. .list-content {
  401. padding: 20rpx;
  402. }
  403. .project-card {
  404. background-color: #fff;
  405. border-radius: 8rpx;
  406. margin-bottom: 20rpx;
  407. box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
  408. overflow: hidden;
  409. }
  410. .card-header {
  411. padding: 20rpx;
  412. display: flex;
  413. justify-content: space-between;
  414. align-items: center;
  415. border-bottom: 1px solid #ebeef5;
  416. background-color: #fafafa;
  417. }
  418. .header-left {
  419. display: flex;
  420. align-items: center;
  421. gap: 20rpx;
  422. }
  423. .project-name {
  424. font-size: 32rpx;
  425. font-weight: bold;
  426. color: #333;
  427. }
  428. .status-tag,
  429. .level-tag {
  430. padding: 4rpx 12rpx;
  431. border-radius: 4rpx;
  432. font-size: 24rpx;
  433. }
  434. .success {
  435. background-color: #f0f9eb;
  436. color: #67c23a;
  437. }
  438. .warning {
  439. background-color: #fdf6ec;
  440. color: #e6a23c;
  441. }
  442. .info {
  443. background-color: #f4f4f5;
  444. color: #909399;
  445. }
  446. .error {
  447. background-color: #fef0f0;
  448. color: #f56c6c;
  449. }
  450. .card-content {
  451. padding: 20rpx;
  452. }
  453. .info-row {
  454. display: flex;
  455. align-items: center;
  456. margin-bottom: 16rpx;
  457. }
  458. .info-row:last-child {
  459. margin-bottom: 0;
  460. }
  461. .info-row .label {
  462. width: 170rpx;
  463. color: #909399;
  464. font-size: 28rpx;
  465. }
  466. .info-row .value {
  467. flex: 1;
  468. color: #333;
  469. font-size: 28rpx;
  470. }
  471. .progress-box {
  472. flex: 1;
  473. display: flex;
  474. align-items: center;
  475. gap: 20rpx;
  476. }
  477. .progress-box progress {
  478. flex: 1;
  479. }
  480. .progress-text {
  481. width: 80rpx;
  482. text-align: right;
  483. color: #333;
  484. font-size: 28rpx;
  485. }
  486. .card-footer {
  487. display: flex;
  488. justify-content: flex-end;
  489. gap: 20rpx;
  490. padding: 20rpx;
  491. border-top: 1px solid #ebeef5;
  492. background-color: #fafafa;
  493. }
  494. .card-footer button {
  495. margin: 0;
  496. padding: 0 30rpx;
  497. }
  498. .load-more {
  499. text-align: center;
  500. padding: 20rpx;
  501. color: #909399;
  502. font-size: 28rpx;
  503. }
  504. .back-to-top {
  505. position: fixed;
  506. right: 30rpx;
  507. bottom: 100rpx;
  508. width: 80rpx;
  509. height: 80rpx;
  510. background-color: rgba(0, 0, 0, 0.5);
  511. border-radius: 50%;
  512. display: flex;
  513. align-items: center;
  514. justify-content: center;
  515. z-index: 999;
  516. }
  517. .back-to-top .iconfont {
  518. color: #fff;
  519. font-size: 40rpx;
  520. }
  521. .icon-top:before {
  522. content: "↑";
  523. }
  524. </style>