form.vue 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. <script setup>
  2. import { reactive, ref, onMounted } from 'vue'
  3. import {
  4. getExpenseItemTree,
  5. flattenExpenseLeaves,
  6. createReimburse,
  7. submitReimburse,
  8. generateReimburseTitle,
  9. bindAttachments
  10. } from '@/api/bx'
  11. import { chooseAndUploadBxAttachments } from '@/utils/bxUpload'
  12. import { todayStr } from '@/utils/bxLabels'
  13. import { PAGE_PATHS } from '@/config/api'
  14. const submitting = ref(false)
  15. const leaves = ref([])
  16. const photos = ref([])
  17. const form = reactive({
  18. title: '',
  19. reimburseDate: todayStr(),
  20. remark: '',
  21. expenseItemId: '',
  22. expenseLabel: '',
  23. amount: '',
  24. detailRemark: ''
  25. })
  26. const goBack = () => {
  27. uni.navigateBack({
  28. fail: () => uni.navigateTo({ url: PAGE_PATHS.bxReimburseList })
  29. })
  30. }
  31. const loadTree = async () => {
  32. try {
  33. const tree = await getExpenseItemTree()
  34. leaves.value = flattenExpenseLeaves(tree)
  35. } catch (e) {
  36. uni.showToast({ title: e.message || '费用项目加载失败', icon: 'none' })
  37. }
  38. }
  39. const refreshTitle = async () => {
  40. try {
  41. const t = await generateReimburseTitle(form.reimburseDate)
  42. if (typeof t === 'string') form.title = t
  43. else if (t) form.title = String(t)
  44. } catch {
  45. /* optional */
  46. }
  47. }
  48. const pickExpense = () => {
  49. if (!leaves.value.length) {
  50. uni.showToast({ title: '暂无费用项目,请先在 Web 配置', icon: 'none' })
  51. return
  52. }
  53. const names = leaves.value.map((i) => i.pathLabel).slice(0, 20)
  54. uni.showActionSheet({
  55. itemList: names,
  56. success: (res) => {
  57. const item = leaves.value[res.tapIndex]
  58. if (!item) return
  59. form.expenseItemId = item.id
  60. form.expenseLabel = item.pathLabel
  61. }
  62. })
  63. }
  64. const addPhotos = async () => {
  65. const uploaded = await chooseAndUploadBxAttachments({
  66. bizType: 'reimburse_apply',
  67. bizId: '0',
  68. maxCount: 6,
  69. currentCount: photos.value.length
  70. })
  71. photos.value = photos.value.concat(uploaded)
  72. }
  73. const removePhoto = (idx) => {
  74. photos.value.splice(idx, 1)
  75. }
  76. const submit = async (andSubmit) => {
  77. if (!form.expenseItemId) {
  78. uni.showToast({ title: '请选择费用项目', icon: 'none' })
  79. return
  80. }
  81. if (!form.amount || Number(form.amount) <= 0) {
  82. uni.showToast({ title: '请填写金额', icon: 'none' })
  83. return
  84. }
  85. if (!String(form.detailRemark || '').trim()) {
  86. uni.showToast({ title: '请填写明细说明', icon: 'none' })
  87. return
  88. }
  89. submitting.value = true
  90. try {
  91. const created = await createReimburse({
  92. title: form.title || undefined,
  93. reimburseDate: form.reimburseDate,
  94. remark: form.remark,
  95. details: [
  96. {
  97. expenseItemId: form.expenseItemId,
  98. amount: Number(form.amount),
  99. remark: String(form.detailRemark).trim()
  100. }
  101. ]
  102. })
  103. const id = created?.id
  104. if (id && photos.value.length) {
  105. await bindAttachments(
  106. photos.value.map((p) => p.id),
  107. 'reimburse_apply',
  108. id
  109. )
  110. }
  111. if (andSubmit && id) {
  112. await submitReimburse(id)
  113. uni.showToast({ title: '已提交审批', icon: 'success' })
  114. } else {
  115. uni.showToast({ title: '已保存草稿', icon: 'success' })
  116. }
  117. setTimeout(() => goBack(), 500)
  118. } catch (e) {
  119. uni.showToast({ title: e.message || '保存失败', icon: 'none' })
  120. } finally {
  121. submitting.value = false
  122. }
  123. }
  124. onMounted(async () => {
  125. await loadTree()
  126. await refreshTitle()
  127. })
  128. </script>
  129. <template>
  130. <view class="mp-page mp-page--sub mp-page--with-bottom">
  131. <view class="mp-header mp-header--bx">
  132. <text class="mp-subpage-back" @click="goBack">← 返回</text>
  133. <text class="mp-header__title" style="margin-top: 16rpx">新建报销</text>
  134. </view>
  135. <view class="mp-form-panel">
  136. <view class="mp-form-row">
  137. <text class="mp-form-label">标题</text>
  138. <input v-model="form.title" class="mp-form-input" placeholder="自动生成,可修改" />
  139. </view>
  140. <view class="mp-form-row">
  141. <text class="mp-form-label">报销日期</text>
  142. <picker mode="date" :value="form.reimburseDate" @change="(e) => { form.reimburseDate = e.detail.value; refreshTitle() }">
  143. <view class="mp-form-input">{{ form.reimburseDate }}</view>
  144. </picker>
  145. </view>
  146. <view class="mp-form-row" @click="pickExpense">
  147. <text class="mp-form-label mp-form-label--req">费用项目</text>
  148. <view class="mp-form-input">{{ form.expenseLabel || '点击选择' }}</view>
  149. </view>
  150. <view class="mp-form-row">
  151. <text class="mp-form-label mp-form-label--req">金额(元)</text>
  152. <input v-model="form.amount" class="mp-form-input" type="digit" placeholder="0.00" />
  153. </view>
  154. <view class="mp-form-row">
  155. <text class="mp-form-label mp-form-label--req">明细说明</text>
  156. <textarea v-model="form.detailRemark" class="mp-form-textarea" placeholder="费用事由" />
  157. </view>
  158. <view class="mp-form-row">
  159. <text class="mp-form-label">备注</text>
  160. <textarea v-model="form.remark" class="mp-form-textarea" placeholder="可选" />
  161. </view>
  162. <view class="mp-form-row">
  163. <text class="mp-form-label">票据照片</text>
  164. <view class="mp-photo-row">
  165. <view v-for="(p, idx) in photos" :key="p.id" class="mp-photo-item">
  166. <image :src="p.localPath" mode="aspectFill" />
  167. <view class="mp-photo-del" @click.stop="removePhoto(idx)">×</view>
  168. </view>
  169. <view v-if="photos.length < 6" class="mp-photo-add" @click="addPhotos">+ 拍照</view>
  170. </view>
  171. </view>
  172. </view>
  173. <view class="mp-bottom-bar">
  174. <view class="mp-btn mp-btn--ghost" @click="submit(false)">{{ submitting ? '…' : '存草稿' }}</view>
  175. <view class="mp-btn mp-btn--primary" @click="submit(true)">{{ submitting ? '…' : '提交审批' }}</view>
  176. </view>
  177. </view>
  178. </template>