综合办公系统
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

declare.vue 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. <template>
  2. <view class="form-container">
  3. <!-- 表单标题 -->
  4. <view class="form-title">
  5. <text class="title-text">工作填报</text>
  6. <view class="title-line"></view>
  7. </view>
  8. <!-- 表单内容 -->
  9. <uni-forms ref="form" :modelValue="formData" :rules="rules" label-position="top" label-width="150"
  10. class="custom-form">
  11. <flow-note :taskForm="taskForm"></flow-note>
  12. <!-- 当前节点 -->
  13. <uni-forms-item label="当前节点" class="form-item" v-if="taskName">
  14. <uni-tag :inverted="true" type="primary" :text="taskName"></uni-tag>
  15. </uni-forms-item>
  16. <!-- 流程发起人 -->
  17. <uni-forms-item label="填报人" class="form-item">
  18. <b style="font-size:30rpx;">{{ applierUserName }}</b>
  19. </uni-forms-item>
  20. <!-- 填报日期 -->
  21. <uni-forms-item label="填报日期" class="form-item">
  22. <text>{{ formData.submitTime }}</text>
  23. </uni-forms-item>
  24. <!-- 是否零星项目 -->
  25. <uni-forms-item label="是否零星项目" class="form-item" name="isScattered">
  26. <uni-data-checkbox v-model="isScattered" :localdata="isScatteredOptions"></uni-data-checkbox>
  27. </uni-forms-item>
  28. <!-- 选择项目 -->
  29. <uni-forms-item label="选择项目" required class="form-item" v-if="!isScattered" name="projectId">
  30. <u-button type="primary" @click="openProject = true" v-if="taskName == '工作填报'">选择项目</u-button>
  31. <ProjectPicker :visible.sync="openProject" :selected.sync="selectedProject" @confirm="handleConfirm" />
  32. <ProjectInfo :project="projectObj"></ProjectInfo>
  33. </uni-forms-item>
  34. <!-- 工作类别 -->
  35. <uni-forms-item label="工作类别" required class="form-item" name="workType">
  36. <uni-data-select :disabled="taskName != '工作填报'" :clear="taskName == '工作填报'" v-model="formData.workType"
  37. :localdata="workTypeColumns" @change="bindWorkTypeChange"></uni-data-select>
  38. </uni-forms-item>
  39. <!-- 工作细项 -->
  40. <uni-forms-item label="工作细项" required class="form-item" name="workItem">
  41. <picker @change="bindWorkItemChange" :value="formData.workItem" :range="workItemList"
  42. :disabled="taskName != '工作填报'">
  43. <view class="picker-selector" :class="{ 'picker-disabled': taskName != '工作填报' }">
  44. <text class="picker-text" :class="{ 'picker-placeholder': !formData.workItem }">
  45. {{ formData.workItem || '请选择' }}
  46. </text>
  47. <uni-icons type="arrowdown" size="16" color="#c0c4cc"></uni-icons>
  48. </view>
  49. </picker>
  50. </uni-forms-item>
  51. <!-- 工作日期 -->
  52. <uni-forms-item label="工作日期" required class="form-item" name="beginDate">
  53. <uni-datetime-picker v-if="taskName == '工作填报'" type="daterange" v-model="dateRange" start-placeholder="开始日期"
  54. end-placeholder="结束日期" range-separator="至" return-type="string" @change="handleDateRangeChange" :border="true"
  55. :clear-icon="true" />
  56. <view v-else class="date-display">
  57. <text class="date-text">{{ formatDateRange() }}</text>
  58. </view>
  59. </uni-forms-item>
  60. <!-- 具体内容 -->
  61. <uni-forms-item label="具体内容" required class="form-item" name="workContent">
  62. <uni-easyinput :disabled="taskName != '工作填报'" type="textarea" v-model="formData.workContent"
  63. placeholder="请输入具体工作内容" :styles="textareaStyle" />
  64. </uni-forms-item>
  65. <!-- 工天 -->
  66. <uni-forms-item label="工天" required class="form-item" name="workLoad">
  67. <uni-easyinput :disabled="taskName != '工作填报'" type="number" v-model="formData.workLoad" placeholder="请输入工天"
  68. :styles="inputStyle">
  69. <text slot="right" class="unit">天</text>
  70. </uni-easyinput>
  71. </uni-forms-item>
  72. <uni-forms-item label="系数" required class="form-item" name="coefficient" v-if="taskName != '工作填报'">
  73. <uni-easyinput type="number" v-model="formData.coefficient" placeholder="请输入系数" :styles="inputStyle"
  74. @blur="countMoney" @clear="countMoney" :disabled="!taskName || taskName == '填报人确认'">
  75. </uni-easyinput>
  76. </uni-forms-item>
  77. <uni-forms-item label="工天单价" class="form-item" v-if="taskName != '工作填报'">
  78. <u-tag :text="formData.price + '/人天'" type="success" plain></u-tag>
  79. </uni-forms-item>
  80. <uni-forms-item label="预估绩效" class="form-item" v-if="taskName != '工作填报'">
  81. <u-tag :text="'¥' + money" type="primary" plain></u-tag>
  82. </uni-forms-item>
  83. <!-- 提交按钮 -->
  84. <view v-if="taskName">
  85. <button class="save-btn" @click="save">保存</button>
  86. <button class="submit-btn margin-top-xs" type="primary" @click="submitForm"
  87. v-if="taskName == '工作填报'">提交</button>
  88. <button class="submit-btn margin-top-xs" type="primary" @click="submitForm" v-else>确认审核</button>
  89. </view>
  90. </uni-forms>
  91. <uv-modal ref="popModal" title="提示" content='是否提交表单?' :showCancelButton="true" @confirm="confirmSubmit"></uv-modal>
  92. </view>
  93. </template>
  94. <script>
  95. import ProjectPicker from '@/pages/components/ProjectPicker.vue';
  96. import ProjectInfo from '@/pages/components/ProjectInfo.vue';
  97. import { listPrice, getWorkTypeList, getWorkItemList } from '@/api/oa/price/price'
  98. import { listProject, submitProject, modifyProject, delProject } from "@/api/oa/project/project";
  99. import { listDeclare, getDeclare, addDeclare, updateDeclare } from '@/api/oa/declare/declare';
  100. import { parseTime } from "@/utils/common.js"
  101. import { complete, getNextFlowNode } from "@/api/flowable/todo";
  102. import { getUsersDeptLeader, getUsersDeptLeaderByDept, getUsersManageLeaderByDept } from "@/api/system/post.js";
  103. import FlowNote from '@/pages/components/flowNote.vue';
  104. export default {
  105. components: {
  106. ProjectPicker,
  107. ProjectInfo,
  108. FlowNote
  109. },
  110. props: {
  111. taskForm: Object,
  112. taskName: String, // 当前节点
  113. startUserName: String, // 流程发起人
  114. },
  115. created() {
  116. this.applierUserName = this.startUserName;
  117. this.getProjectList();
  118. this.initForm();
  119. if (this.taskName != '工作填报') {
  120. this.isScatteredOptions.map(item => item.disable = true)
  121. }
  122. uni.setNavigationBarTitle({
  123. title: '工作填报'
  124. });
  125. },
  126. data() {
  127. return {
  128. applierUserName: '',
  129. openProject: false,
  130. selectedProject: null,
  131. isScattered: 0,
  132. formData: {
  133. userId: null,
  134. projectId: '',
  135. workType: '',
  136. workItem: '',
  137. beginDate: '',
  138. endDate: '',
  139. workContent: '',
  140. workLoad: '',
  141. price: 200,
  142. submitTime: '',
  143. coefficient: undefined,
  144. },
  145. money: '',
  146. rules: {
  147. projectId: {
  148. rules: [{
  149. required: true,
  150. errorMessage: '请选择项目',
  151. },]
  152. },
  153. workType: {
  154. rules: [{
  155. required: true,
  156. errorMessage: '请选择工作类别',
  157. },]
  158. },
  159. workItem: {
  160. rules: [{
  161. required: true,
  162. errorMessage: '请选择工作细项',
  163. },]
  164. },
  165. beginDate: {
  166. rules: [{
  167. required: true,
  168. errorMessage: '请选择开始日期',
  169. },]
  170. },
  171. endDate: {
  172. rules: [{
  173. required: true,
  174. errorMessage: '请选择结束日期',
  175. },]
  176. },
  177. workContent: {
  178. rules: [{
  179. required: true,
  180. errorMessage: '请输入具体内容',
  181. },]
  182. },
  183. workLoad: {
  184. rules: [{
  185. required: true,
  186. errorMessage: '请输入工天',
  187. },]
  188. },
  189. coefficient: {
  190. rules: [{
  191. required: true,
  192. errorMessage: '请输入系数',
  193. },]
  194. }
  195. },
  196. projectObj: {},
  197. isScatteredOptions: [{
  198. text: '否',
  199. value: 0,
  200. disable: false
  201. }, {
  202. text: '是',
  203. value: 1,
  204. disable: false
  205. }],
  206. projects: [],
  207. projectIndex: -1,
  208. inputStyle: {
  209. borderColor: '#e5e5e5',
  210. borderRadius: '8px'
  211. },
  212. textareaStyle: {
  213. borderColor: '#e5e5e5',
  214. borderRadius: '8px',
  215. height: '100px'
  216. },
  217. workTypeColumns: [{
  218. text: '外业',
  219. value: '外业',
  220. },
  221. {
  222. text: '内业',
  223. value: '内业'
  224. }
  225. ],
  226. workItemList: [],
  227. hasForm: false,
  228. show: false,
  229. isSubmitting: false, // 提交状态
  230. };
  231. },
  232. computed: {
  233. dateRange: {
  234. get() {
  235. if (this.formData.beginDate && this.formData.endDate) {
  236. return [this.formData.beginDate, this.formData.endDate];
  237. }
  238. return [];
  239. },
  240. set(value) {
  241. if (value && value.length === 2) {
  242. this.formData.beginDate = value[0];
  243. this.formData.endDate = value[1];
  244. } else {
  245. this.formData.beginDate = '';
  246. this.formData.endDate = '';
  247. }
  248. }
  249. }
  250. },
  251. methods: {
  252. handleConfirm(project) {
  253. this.selectedProject = project;
  254. this.projectObj = project;
  255. // 同时设置formData中的projectId,避免表单验证失败
  256. this.formData.projectId = project.projectId;
  257. },
  258. initForm() {
  259. if (this.taskName == '工作填报') {
  260. this.formData.submitTime = parseTime(new Date(), "{y}-{m}-{d}");
  261. this.formData.userId = this.$store.getters.userId;
  262. }
  263. getDeclare(this.taskForm.formId).then(res => {
  264. if (res.data) {
  265. this.hasForm = true;
  266. this.formData = res.data;
  267. if (!this.applierUserName) {
  268. this.applierUserName = this.formData.user.nickName;
  269. }
  270. if (res.data.projectId) {
  271. this.isScattered = 0;
  272. } else {
  273. this.isScattered = 1;
  274. }
  275. res.data.project.projectLeader = res.data.projectLeader
  276. this.projectObj = res.data.project;
  277. this.selectedProject = res.data.project;
  278. this.bindWorkTypeChange(this.formData.workType);
  279. // 处理日期范围和工天的关系
  280. if (!this.formData.beginDate) {
  281. this.formData.beginDate = '';
  282. }
  283. if (!this.formData.endDate) {
  284. this.formData.endDate = '';
  285. }
  286. if (this.formData.beginDate && this.formData.endDate) {
  287. // 如果有日期范围,计算建议工天数但不覆盖现有值
  288. this.calculateWorkDays();
  289. }
  290. this.countMoney();
  291. } else {
  292. this.hasForm = false;
  293. }
  294. })
  295. },
  296. bindProjectChange(e) {
  297. this.projectIndex = e.detail.value;
  298. this.formData.project = this.projects[this.projectIndex];
  299. },
  300. bindWorkTypeChange(e) {
  301. listPrice({
  302. workType: e,
  303. pageNum: 1,
  304. pageSize: 9999
  305. }).then(res => {
  306. if (res.code == 200) {
  307. let data = res.rows;
  308. let list = [];
  309. for (let d of data) {
  310. list.push(d.workItem)
  311. }
  312. if (e == '内业') {
  313. list.push('其他')
  314. }
  315. this.workItemList = [...new Set(list)];
  316. }
  317. })
  318. },
  319. bindWorkItemChange(e) {
  320. this.formData.workItem = this.workItemList[e.detail.value];
  321. },
  322. handleRadioChange(e) {
  323. this.formData.isScattered = e.detail.value;
  324. },
  325. handleProjectChange(row) {
  326. },
  327. getProjectList() {
  328. listProject({
  329. pageNum: 1,
  330. pageSize: 10
  331. }).then(res => {
  332. this.projects = res.rows
  333. })
  334. },
  335. countMoney() {
  336. let result = Number(this.formData.price) * Number(this.formData.workLoad) * Number(this.formData.coefficient)
  337. this.money = result.toFixed(2)
  338. },
  339. handleDateRangeChange(range) {
  340. this.dateRange = range;
  341. this.calculateWorkDays();
  342. },
  343. calculateWorkDays() {
  344. if (!this.formData.beginDate || !this.formData.endDate) {
  345. return;
  346. }
  347. const startDate = new Date(this.formData.beginDate);
  348. const endDate = new Date(this.formData.endDate);
  349. if (startDate && endDate && startDate <= endDate) {
  350. // 计算日期差(包含开始和结束日期)
  351. const timeDiff = endDate.getTime() - startDate.getTime();
  352. const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24)) + 1;
  353. // 直接设置工天
  354. this.formData.workLoad = daysDiff;
  355. }
  356. },
  357. formatDateRange() {
  358. if (this.formData.beginDate && this.formData.endDate) {
  359. const beginDate = this.formData.beginDate.split(' ')[0]; // 只取日期部分,去掉时间
  360. const endDate = this.formData.endDate.split(' ')[0];
  361. return `${beginDate} 到 ${endDate}`;
  362. }
  363. return '未选择日期';
  364. },
  365. async save(showToast = true) {
  366. try {
  367. if (!this.isScattered) {
  368. this.formData.projectId = this.projectObj.projectId;
  369. } else {
  370. this.formData.projectId = ''
  371. }
  372. let result;
  373. if (this.hasForm) {
  374. result = await updateDeclare(this.formData);
  375. } else {
  376. this.formData.formId = this.taskForm.formId;
  377. result = await addDeclare(this.formData);
  378. if (result.code == 200) {
  379. this.hasForm = true;
  380. } else {
  381. this.hasForm = false;
  382. }
  383. }
  384. if (result && result.code === 200) {
  385. if (showToast) {
  386. uni.showToast({
  387. title: '保存成功',
  388. icon: 'success'
  389. });
  390. }
  391. return Promise.resolve(result);
  392. } else {
  393. throw new Error(result?.msg || '保存失败');
  394. }
  395. } catch (error) {
  396. console.error('保存失败:', error);
  397. if (showToast) {
  398. uni.showToast({
  399. title: error.message || '保存失败',
  400. icon: 'error'
  401. });
  402. }
  403. return Promise.reject(error);
  404. }
  405. },
  406. submitForm() {
  407. // 在表单验证前确保projectId被正确设置
  408. if (!this.isScattered && this.projectObj && this.projectObj.projectId) {
  409. this.formData.projectId = this.projectObj.projectId;
  410. } else if (this.isScattered) {
  411. this.formData.projectId = '';
  412. }
  413. this.$refs.form.validate().then(res => {
  414. this.taskForm.variables.skip = false;
  415. if (!this.projectObj.projectLeader) {
  416. if (this.isScattered == 0) {
  417. this.$message.error('该项目未指定项目负责人,无法提交。')
  418. return
  419. }
  420. }
  421. this.$refs.popModal.open();
  422. }).catch(err => {
  423. console.log('表单错误信息:', err);
  424. })
  425. },
  426. async confirmSubmit() {
  427. if (this.isSubmitting) return; // 防止重复提交
  428. try {
  429. this.isSubmitting = true;
  430. // 显示保存中提示
  431. uni.showLoading({
  432. title: '正在保存...'
  433. });
  434. // 先保存表单,不显示保存成功提示
  435. await this.save(false);
  436. uni.hideLoading();
  437. if (this.taskName != '填报人确认') {
  438. // 显示提交中提示
  439. uni.showLoading({
  440. title: '正在提交...'
  441. });
  442. }
  443. // 保存成功后执行相应的提交流程
  444. if (this.taskName == '工作填报') {
  445. await this.declareSubmit();
  446. } else if (this.taskName == '项目负责人审核' || this.taskName == '部门负责人审核') {
  447. await this.approveForm();
  448. } else {
  449. await this.confirmForm();
  450. }
  451. } catch (error) {
  452. console.error('保存失败,无法提交:', error);
  453. uni.hideLoading();
  454. uni.showToast({
  455. title: '保存失败,请重试',
  456. icon: 'error'
  457. });
  458. } finally {
  459. this.isSubmitting = false;
  460. }
  461. },
  462. async declareSubmit() {
  463. if (this.isScattered == 0) {
  464. let approval = this.projectObj.projectLeader;
  465. this.taskForm.variables.approval = approval
  466. } else {
  467. let resData = await getUsersDeptLeader({
  468. userId: this.formData.userId
  469. });
  470. if (resData.data) {
  471. this.taskForm.variables.approval = resData.data.userId
  472. this.taskForm.variables.skip = true;
  473. }
  474. }
  475. // 数据已在confirmSubmit中保存,直接进行流程提交
  476. this.handleComplete(this.taskForm);
  477. },
  478. async approveForm() {
  479. if (this.taskName == '项目负责人审核') {
  480. this.formData.checkStatus = '1';
  481. // 更新状态后需要再次保存
  482. await updateDeclare(this.formData);
  483. let resData = await getUsersDeptLeader({
  484. userId: this.formData.userId
  485. });
  486. if (resData.data) {
  487. this.taskForm.variables.approval = resData.data.userId;
  488. this.handleComplete(this.taskForm);
  489. }
  490. } else if (this.taskName == '部门负责人审核') {
  491. this.formData.checkStatus = '1';
  492. this.formData.auditStatus = '1';
  493. // 更新状态后需要再次保存
  494. await updateDeclare(this.formData);
  495. this.taskForm.variables.approval = this.formData.userId;
  496. this.handleComplete(this.taskForm);
  497. }
  498. },
  499. confirmForm() {
  500. return this.$modal.confirm('最后一个节点,提交将结束流程,是否提交?').then(async () => {
  501. this.formData.confirmStatus = '1';
  502. // 更新确认状态后需要再次保存
  503. await updateDeclare(this.formData);
  504. this.handleComplete(this.taskForm);
  505. })
  506. },
  507. handleComplete(taskForm) {
  508. const params = {
  509. taskId: this.taskForm.taskId
  510. };
  511. getNextFlowNode(params).then(() => {
  512. complete(taskForm).then(response => {
  513. uni.hideLoading(); // 隐藏loading
  514. uni.showToast({
  515. title: response.msg,
  516. icon: 'success'
  517. });
  518. setTimeout(() => {
  519. uni.switchTab({
  520. url: '/pages/message/index'
  521. })
  522. }, 500);
  523. }).catch(error => {
  524. uni.hideLoading(); // 隐藏loading
  525. console.error('提交失败:', error);
  526. uni.showToast({
  527. title: '提交失败,请重试',
  528. icon: 'error'
  529. });
  530. })
  531. }).catch(error => {
  532. uni.hideLoading(); // 隐藏loading
  533. console.error('获取下一节点失败:', error);
  534. uni.showToast({
  535. title: '提交失败,请重试',
  536. icon: 'error'
  537. });
  538. })
  539. },
  540. }
  541. };
  542. </script>
  543. <style lang="scss" scoped>
  544. .form-container {
  545. padding: 20px;
  546. background-color: #f8f8f8;
  547. }
  548. .form-title {
  549. margin-bottom: 15px;
  550. text-align: center;
  551. .title-text {
  552. font-size: 20px;
  553. font-weight: bold;
  554. color: #333;
  555. }
  556. .title-line {
  557. width: 50px;
  558. height: 2px;
  559. background-color: #007AFF;
  560. margin: 8px auto;
  561. }
  562. }
  563. .custom-form {
  564. background-color: #fff;
  565. padding: 15px;
  566. border-radius: 12px;
  567. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
  568. }
  569. .form-item {
  570. margin-bottom: 20px;
  571. ::v-deep .uni-forms-item__label {
  572. font-weight: 500;
  573. color: #666;
  574. padding-bottom: 8px;
  575. }
  576. }
  577. .picker {
  578. width: 100%;
  579. padding: 10px;
  580. border: 1px solid #e5e5e5;
  581. border-radius: 8px;
  582. display: flex;
  583. align-items: center;
  584. justify-content: space-between;
  585. color: #333;
  586. }
  587. .radio-label {
  588. margin-right: 25px;
  589. display: inline-flex;
  590. align-items: center;
  591. }
  592. .unit {
  593. color: #999;
  594. padding: 0 10px;
  595. }
  596. .date-display {
  597. padding: 10px;
  598. border: 1px solid #e5e5e5;
  599. border-radius: 8px;
  600. background-color: #f7f6f6;
  601. min-height: 44px;
  602. display: flex;
  603. align-items: center;
  604. }
  605. .date-text {
  606. font-size: 14px;
  607. color: #333333;
  608. line-height: 1.4;
  609. }
  610. .picker-selector {
  611. display: flex;
  612. align-items: center;
  613. justify-content: space-between;
  614. padding: 10px;
  615. border: 1px solid #e5e5e5;
  616. border-radius: 8px;
  617. background-color: #fff;
  618. min-height: 44px;
  619. box-sizing: border-box;
  620. }
  621. .picker-selector.picker-disabled {
  622. background-color: #f7f6f6;
  623. color: #333333;
  624. }
  625. .picker-text {
  626. flex: 1;
  627. font-size: 14px;
  628. color: #333;
  629. line-height: 1.4;
  630. }
  631. .picker-text.picker-placeholder {
  632. color: #c0c4cc;
  633. }
  634. </style>