detail.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. <template>
  2. <view class="detail-container">
  3. <!-- 头部卡片:标题 + 基本信息 -->
  4. <view class="header-card">
  5. <view class="title">{{ rowData.instTitle || '未命名流程' }}</view>
  6. </view>
  7. <!-- 标签页:单据信息 / 审批记录 / 审批意见(审批模式) -->
  8. <view class="tabs-container">
  9. <uv-tabs
  10. :list="tabList"
  11. :current="currentTab"
  12. @click="handleTabClick"
  13. lineColor="#1c9bfd"
  14. activeColor="#1c9bfd"
  15. inactiveColor="#666"
  16. :scrollable="false"
  17. ></uv-tabs>
  18. </view>
  19. <!-- 内容区域 -->
  20. <scroll-view class="content-area" scroll-y>
  21. <view class="component-wrapper">
  22. <!-- 单据信息 -->
  23. <view v-if="currentTab === 0">
  24. <!-- 详细单据内容 (由 defCode 和 taskCode 驱动) -->
  25. <DocumentInfoDisplay
  26. v-if="defCode && taskCode"
  27. :defCode="defCode"
  28. :taskCode="taskCode"
  29. />
  30. </view>
  31. <!-- 审批记录 -->
  32. <view v-else-if="currentTab === 1">
  33. <ApprovalFlowTable
  34. v-if="instanceId"
  35. :id="instanceId"
  36. />
  37. </view>
  38. <!-- 审批意见(仅审批模式显示) -->
  39. <view v-else-if="currentTab === 2 && canApprove">
  40. <view class="section-card">
  41. <view class="section-title">审批处理</view>
  42. <view class="form-item">
  43. <view class="form-label"><text class="required" style="margin-right: 4rpx; margin-left: 0;">*</text>审批意见</view>
  44. <textarea
  45. v-model="approveForm.approveOpinion"
  46. class="form-textarea"
  47. placeholder="请输入审批意见"
  48. maxlength="500"
  49. :show-confirm-bar="false"
  50. />
  51. </view>
  52. <!-- 结算金额(仅报销单据且有权限时显示) -->
  53. <view class="form-item" v-if="showSettleAmount">
  54. <view class="form-label">结算金额(元)</view>
  55. <uv-input
  56. v-model="approveForm.settleAmount"
  57. type="number"
  58. placeholder="请输入结算金额"
  59. :customStyle="{ backgroundColor: '#f5f7fa' }"
  60. ></uv-input>
  61. </view>
  62. <!-- 附件上传(如果流程需要附件) -->
  63. <view class="form-item" v-if="rowData.isFile">
  64. <view class="form-label">附件 <text class="tip" style="font-size: 24rpx; color: #999; font-weight: normal; margin-left: 10rpx;">(仅“通过”时必传)</text></view>
  65. <view class="upload-area">
  66. <uv-upload
  67. :fileList="fileList"
  68. :maxCount="1"
  69. :action="uploadUrl"
  70. :header="uploadHeaders"
  71. @afterRead="afterRead"
  72. @delete="deleteFile"
  73. >
  74. <view class="upload-btn">
  75. <uv-icon name="plus" size="40"></uv-icon>
  76. <text class="upload-text">点击上传</text>
  77. </view>
  78. </uv-upload>
  79. </view>
  80. </view>
  81. <!-- 操作按钮移入此处 -->
  82. <view class="approval-actions">
  83. <uv-button
  84. type="error"
  85. text="拒绝"
  86. plain
  87. @click="submitApprove('rejection')"
  88. ></uv-button>
  89. <uv-button
  90. type="primary"
  91. text="通过"
  92. @click="submitApprove('pass')"
  93. ></uv-button>
  94. </view>
  95. </view>
  96. </view>
  97. </view>
  98. </scroll-view>
  99. </view>
  100. </template>
  101. <script setup lang="ts">
  102. import { onLoad } from '@dcloudio/uni-app';
  103. import { ref, computed } from 'vue';
  104. import { useExecutionApi } from '@/api/execution';
  105. import { formatDate } from '@/utils/date';
  106. import ApprovalFlowTable from '@/pages/todo/components/ApprovalFlowTable.vue';
  107. import DocumentInfoDisplay from '@/pages/todo/components/DocumentInfoDisplay.vue';
  108. import to from 'await-to-js';
  109. const executionApi = useExecutionApi();
  110. // 动态生成标签页列表
  111. const tabList = computed(() => {
  112. const tabs = [
  113. { name: '单据信息', index: 0 },
  114. { name: '审批记录', index: 1 }
  115. ];
  116. // 如果是审批模式,添加"审批意见"标签
  117. if (canApprove.value) {
  118. tabs.push({ name: '审批意见', index: 2 });
  119. }
  120. return tabs;
  121. });
  122. const currentTab = ref(0);
  123. const handleTabClick = (item: any) => {
  124. currentTab.value = item.index;
  125. };
  126. // 列表行数据(从待办页透传)
  127. const rowData = ref<any>({});
  128. // 流程实例 & 流程信息
  129. const instanceId = ref<number | null>(null);
  130. const taskCode = ref('');
  131. const defCode = ref('');
  132. // 审批相关
  133. const taskId = ref<number | null>(null);
  134. const mode = ref<'view' | 'approval'>('view');
  135. // 审批表单
  136. const approveForm = ref({
  137. approveOpinion: '',
  138. settleAmount: null as number | null,
  139. fileUrl: ''
  140. });
  141. // 文件列表
  142. const fileList = ref<any[]>([]);
  143. // 上传地址和请求头
  144. const uploadUrl = ref(import.meta.env.VITE_UPLOAD || '');
  145. const uploadHeaders = ref({
  146. Authorization: uni.getStorageSync('ACCESS_TOKEN') || ''
  147. });
  148. // 是否可以审批
  149. const canApprove = computed(() => {
  150. return (
  151. mode.value === 'approval' &&
  152. !!taskId.value &&
  153. rowData.value &&
  154. rowData.value.isFinish === '20'
  155. );
  156. });
  157. // 是否显示结算金额(报销单据且有权限)
  158. const showSettleAmount = computed(() => {
  159. // 这里可以根据权限 and 单据类型判断
  160. // 对应PC端的 auth('sci_fund_expense_settleAmount') && defCode === 'sci_fund_expense'
  161. return defCode.value === 'sci_fund_expense';
  162. });
  163. onLoad(async (options: any) => {
  164. // 从参数中提取基础信息
  165. taskCode.value = options.taskCode || '';
  166. defCode.value = options.defCode || '';
  167. instanceId.value = options.id ? Number(options.id) : null;
  168. taskId.value = options.taskId ? Number(options.taskId) : null;
  169. mode.value = (options.mode === 'approval' ? 'approval' : 'view');
  170. // 构建默认展示用的 rowData
  171. rowData.value = {
  172. businessCode: taskCode.value,
  173. taskId: taskId.value,
  174. defCode: defCode.value
  175. };
  176. // 如果有实例 ID,则去后台拉取最新的流程实例详情并回填头部元数据
  177. if (instanceId.value) {
  178. try {
  179. const res: any = await executionApi.getInstanceById({ id: instanceId.value });
  180. if (res?.data) {
  181. const d = res.data;
  182. rowData.value = {
  183. ...rowData.value,
  184. instTitle: d.instTitle || '',
  185. defName: d.defName || '',
  186. startUserName: d.startUserName || '',
  187. createdTime: d.createdTime || '',
  188. isFinish: d.isFinish || '',
  189. isFile: d.isFile || '' // 是否需要附件
  190. };
  191. }
  192. } catch (e) {
  193. console.error('Failed to fetch instance detail:', e);
  194. }
  195. }
  196. });
  197. // 文件上传后的回调
  198. const afterRead = (event: any) => {
  199. const { file } = event;
  200. // 这里可以处理上传逻辑
  201. uni.uploadFile({
  202. url: uploadUrl.value,
  203. filePath: file.url,
  204. name: 'file',
  205. header: uploadHeaders.value,
  206. success: (res) => {
  207. const data = JSON.parse(res.data);
  208. if (data.Data) {
  209. approveForm.value.fileUrl = data.Data;
  210. fileList.value = [{
  211. url: data.Data,
  212. name: file.name
  213. }];
  214. uni.showToast({ title: '上传成功', icon: 'success' });
  215. }
  216. },
  217. fail: () => {
  218. uni.showToast({ title: '上传失败', icon: 'none' });
  219. }
  220. });
  221. };
  222. // 删除文件
  223. const deleteFile = () => {
  224. approveForm.value.fileUrl = '';
  225. fileList.value = [];
  226. };
  227. // 提交审批
  228. const submitApprove = async (result: 'pass' | 'rejection') => {
  229. if (!taskId.value) {
  230. uni.showToast({ title: '缺少任务ID', icon: 'none' });
  231. return;
  232. }
  233. if (!approveForm.value.approveOpinion || !approveForm.value.approveOpinion.trim()) {
  234. uni.showToast({ title: '请输入审批意见', icon: 'none' });
  235. return;
  236. }
  237. // 如果需要附件且通过,检查是否上传
  238. if (rowData.value.isFile && result === 'pass' && !approveForm.value.fileUrl) {
  239. uni.showToast({ title: '请上传必要的附件', icon: 'none' });
  240. return;
  241. }
  242. uni.showModal({
  243. title: '提示',
  244. content: `确定要 ${result === 'pass' ? '通过' : '拒绝'} 该申请吗?`,
  245. success: async (res) => {
  246. if (res.confirm) {
  247. uni.showLoading({ title: '提交中...', mask: true });
  248. // 构建提交参数
  249. const params: any = {
  250. taskId: taskId.value,
  251. result: result,
  252. opinion: approveForm.value.approveOpinion || (result === 'pass' ? '同意' : '不同意')
  253. };
  254. // 如果有结算金额
  255. if (showSettleAmount.value && approveForm.value.settleAmount !== null) {
  256. params.settleAmount = approveForm.value.settleAmount;
  257. }
  258. // 如果有附件
  259. if (approveForm.value.fileUrl) {
  260. params.fileUrl = approveForm.value.fileUrl;
  261. }
  262. const [err]: any = await to(executionApi.approve(params));
  263. uni.hideLoading();
  264. if (err) {
  265. uni.showToast({
  266. title: err?.message || '提交失败',
  267. icon: 'none'
  268. });
  269. return;
  270. }
  271. uni.showToast({
  272. title: '提交成功',
  273. icon: 'success'
  274. });
  275. setTimeout(() => {
  276. uni.navigateBack();
  277. }, 800);
  278. }
  279. }
  280. });
  281. };
  282. </script>
  283. <style lang="scss" scoped>
  284. .detail-container {
  285. height: 100vh;
  286. display: flex;
  287. flex-direction: column;
  288. background-color: #f5f7fa;
  289. box-sizing: border-box;
  290. }
  291. .header-card {
  292. flex-shrink: 0;
  293. background: linear-gradient(135deg, #1c9bfd 0%, #15a982 100%);
  294. padding: 40rpx 30rpx 60rpx;
  295. color: #fff;
  296. border-bottom-left-radius: 40rpx;
  297. border-bottom-right-radius: 40rpx;
  298. box-shadow: 0 10rpx 20rpx rgba(28, 155, 253, 0.2);
  299. .title {
  300. font-size: 36rpx;
  301. font-weight: bold;
  302. margin-bottom: 20rpx;
  303. line-height: 1.4;
  304. }
  305. .meta-row {
  306. display: flex;
  307. justify-content: space-between;
  308. align-items: center;
  309. .left {
  310. .meta-text {
  311. font-size: 26rpx;
  312. opacity: 0.9;
  313. line-height: 1.6;
  314. }
  315. }
  316. .right {
  317. .status-tag {
  318. font-size: 24rpx;
  319. padding: 6rpx 18rpx;
  320. border-radius: 30rpx;
  321. border: 2rpx solid rgba(255, 255, 255, 0.7);
  322. &.status-done {
  323. background-color: rgba(82, 196, 26, 0.2);
  324. }
  325. &.status-undo {
  326. background-color: rgba(250, 140, 22, 0.2);
  327. }
  328. }
  329. }
  330. }
  331. }
  332. .tabs-container {
  333. flex-shrink: 0;
  334. margin: -30rpx 30rpx 20rpx;
  335. background-color: #fff;
  336. border-radius: 16rpx;
  337. padding: 10rpx 0;
  338. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  339. position: relative;
  340. z-index: 10;
  341. }
  342. .content-area {
  343. flex: 1;
  344. height: 0;
  345. }
  346. .component-wrapper {
  347. margin: 0 30rpx;
  348. }
  349. .section-card {
  350. background-color: #fff;
  351. border-radius: 16rpx;
  352. padding: 30rpx;
  353. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.03);
  354. margin-bottom: 24rpx;
  355. .section-title {
  356. font-size: 32rpx;
  357. font-weight: 600;
  358. color: #343A3F;
  359. margin-bottom: 24rpx;
  360. padding-left: 20rpx;
  361. position: relative;
  362. &::before {
  363. content: '';
  364. position: absolute;
  365. left: 0;
  366. top: 50%;
  367. transform: translateY(-50%);
  368. width: 8rpx;
  369. height: 32rpx;
  370. background-color: #1c9bfd;
  371. border-radius: 4rpx;
  372. }
  373. }
  374. .info-row {
  375. display: flex;
  376. padding: 16rpx 0;
  377. border-bottom: 2rpx dashed #ececec;
  378. font-size: 26rpx;
  379. &:last-child {
  380. border-bottom: none;
  381. }
  382. .label {
  383. color: #343A3F;
  384. width: 180rpx;
  385. flex-shrink: 0;
  386. }
  387. .value {
  388. color: #585858;
  389. flex: 1;
  390. text-align: right;
  391. word-break: break-all;
  392. }
  393. }
  394. // 表单项样式
  395. .form-item {
  396. margin-bottom: 32rpx;
  397. &:last-child {
  398. margin-bottom: 0;
  399. }
  400. .form-label {
  401. font-size: 28rpx;
  402. color: #343A3F;
  403. margin-bottom: 16rpx;
  404. font-weight: 500;
  405. .required {
  406. color: #f56c6c;
  407. margin-left: 4rpx;
  408. }
  409. }
  410. .form-textarea {
  411. width: 100%;
  412. min-height: 200rpx;
  413. padding: 20rpx;
  414. border-radius: 12rpx;
  415. background-color: #f5f7fa;
  416. font-size: 26rpx;
  417. box-sizing: border-box;
  418. border: 2rpx solid #e4e7ed;
  419. color: #585858;
  420. &:focus {
  421. border-color: #1c9bfd;
  422. background-color: #fff;
  423. }
  424. }
  425. .upload-area {
  426. .upload-btn {
  427. width: 160rpx;
  428. height: 160rpx;
  429. border: 2rpx dashed #dcdfe6;
  430. border-radius: 12rpx;
  431. display: flex;
  432. flex-direction: column;
  433. align-items: center;
  434. justify-content: center;
  435. background-color: #fafafa;
  436. .upload-text {
  437. font-size: 24rpx;
  438. color: #999;
  439. margin-top: 8rpx;
  440. }
  441. }
  442. }
  443. }
  444. }
  445. .mt20 {
  446. margin-top: 20rpx;
  447. }
  448. /* Bottom Actions - Integrated into Form */
  449. .approval-actions {
  450. display: flex;
  451. gap: 24rpx;
  452. margin-top: 60rpx;
  453. padding-top: 30rpx;
  454. border-top: 1rpx solid #f1f5f9;
  455. }
  456. </style>