|
|
@@ -19,27 +19,15 @@
|
|
|
<span class="form-badge">{{ isEdit ? '编辑' : '新建' }}</span>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 费用项目 -->
|
|
|
- <div class="field">
|
|
|
- <label class="field-label">费用项目</label>
|
|
|
- <div class="picker-trigger" @click="showExpensePicker = true">
|
|
|
- <div class="left">
|
|
|
- <div class="value" :class="{ ph: !expenseItemDisplay }">
|
|
|
- {{ expenseItemDisplay || '请选择费用项目' }}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <van-icon name="arrow" class="arrow" />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
<!-- 申请原因 -->
|
|
|
<div class="field">
|
|
|
<label class="field-label">申请原因<span class="req">*</span></label>
|
|
|
<textarea
|
|
|
- class="form-textarea"
|
|
|
+ :class="['form-textarea', { error: fieldErr.reason }]"
|
|
|
rows="3"
|
|
|
placeholder="请输入申请原因"
|
|
|
v-model="form.reason"
|
|
|
+ @input="fieldErr.reason = false"
|
|
|
></textarea>
|
|
|
</div>
|
|
|
|
|
|
@@ -48,10 +36,11 @@
|
|
|
<label class="field-label">申请金额<span class="req">*</span></label>
|
|
|
<div class="form-input-prefix">
|
|
|
<input
|
|
|
- class="form-input"
|
|
|
+ :class="['form-input', { error: fieldErr.amount }]"
|
|
|
type="number"
|
|
|
- placeholder="0.00"
|
|
|
+ placeholder="请输入金额"
|
|
|
v-model="form.amount"
|
|
|
+ @input="fieldErr.amount = false"
|
|
|
/>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -126,80 +115,15 @@
|
|
|
保存
|
|
|
</button>
|
|
|
</BottomActionBar>
|
|
|
-
|
|
|
- <!-- 费用项目:左一级 / 右二级 双栏联动 -->
|
|
|
- <van-popup
|
|
|
- v-model:show="showExpensePicker"
|
|
|
- position="bottom"
|
|
|
- round
|
|
|
- :style="{ height: '62vh', maxHeight: '520px' }"
|
|
|
- class="expense-picker-popup"
|
|
|
- >
|
|
|
- <div class="expense-two-col-sheet">
|
|
|
- <div class="expense-two-col-sheet__head">
|
|
|
- <span class="expense-two-col-sheet__title">选择费用项目</span>
|
|
|
- <button type="button" class="expense-two-col-sheet__close" aria-label="关闭" @click="showExpensePicker = false">
|
|
|
- <van-icon name="cross" size="20" color="var(--text-muted)" />
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- <div class="expense-two-col-sheet__cols">
|
|
|
- <div class="expense-col expense-col--left">
|
|
|
- <div class="expense-col__caption">一级</div>
|
|
|
- <div class="expense-col__scroll">
|
|
|
- <button
|
|
|
- v-for="item in leftItems"
|
|
|
- :key="item.id"
|
|
|
- type="button"
|
|
|
- :class="['expense-col__row', { 'is-active': item.id === selectedLeftId }]"
|
|
|
- @click="onPickLeft(item)"
|
|
|
- >
|
|
|
- {{ item.itemName }}
|
|
|
- </button>
|
|
|
- <div v-if="!leftItems.length" class="expense-col__empty">暂无费用大类</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="expense-col expense-col--right">
|
|
|
- <div class="expense-col__caption">二级</div>
|
|
|
- <div class="expense-col__scroll">
|
|
|
- <template v-if="selectedLeft">
|
|
|
- <template v-if="rightItems.length">
|
|
|
- <button
|
|
|
- v-for="child in rightItems"
|
|
|
- :key="child.id"
|
|
|
- type="button"
|
|
|
- :class="['expense-col__row', { 'is-active': child.id === highlightRightId }]"
|
|
|
- @click="confirmExpensePick(child.id)"
|
|
|
- >
|
|
|
- {{ selectedLeft ? `${selectedLeft.itemName}/${child.itemName}` : child.itemName }}
|
|
|
- </button>
|
|
|
- </template>
|
|
|
- <div v-else class="expense-col__leaf-hint">
|
|
|
- <p class="expense-col__leaf-text">该大类下暂无明细项,可直接选用本级。</p>
|
|
|
- <button type="button" class="btn btn-primary btn-block btn-sm" @click="confirmExpensePick(selectedLeft.id)">
|
|
|
- 选用「{{ selectedLeft.itemName }}」
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- </template>
|
|
|
- <div v-else class="expense-col__empty">请先选择左侧大类</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </van-popup>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, computed, watch, onMounted } from 'vue'
|
|
|
+import { ref, reactive, computed, watch, onMounted } from 'vue'
|
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
|
import { showToast, showDialog } from 'vant'
|
|
|
import { advanceApi } from '@/api'
|
|
|
import { BizError } from '@/api/client'
|
|
|
-import {
|
|
|
- useExpenseItemTree,
|
|
|
- formatExpenseName,
|
|
|
- type ExpenseItem,
|
|
|
-} from '@/hooks/useExpenseItemTree'
|
|
|
import { useAttachment } from '@/hooks/useAttachment'
|
|
|
import { SUBMIT_APPROVE_BUTTON_COLOR } from '@/constants/dialog'
|
|
|
import BottomActionBar from '@/components/mobile/BottomActionBar.vue'
|
|
|
@@ -245,115 +169,39 @@ const attachmentAccept =
|
|
|
'.7z',
|
|
|
].join(',')
|
|
|
|
|
|
-const { expenseTree, flatList, load: loadExpenseItems } = useExpenseItemTree()
|
|
|
const attachment = useAttachment()
|
|
|
|
|
|
const isEdit = computed(() => !!route.query.id)
|
|
|
const savedId = ref<number | null>(null)
|
|
|
const submitting = ref(false)
|
|
|
-const showExpensePicker = ref(false)
|
|
|
-/** 左栏当前选中的一级 id */
|
|
|
-const selectedLeftId = ref<number | null>(null)
|
|
|
|
|
|
const form = ref({
|
|
|
reason: '',
|
|
|
amount: '',
|
|
|
remark: '',
|
|
|
- expenseItemId: null as number | null,
|
|
|
-})
|
|
|
-
|
|
|
-const expenseItemDisplay = computed(() => {
|
|
|
- if (!form.value.expenseItemId) return ''
|
|
|
- return formatExpenseName(form.value.expenseItemId, flatList.value)
|
|
|
-})
|
|
|
-
|
|
|
-function isExpenseEnabled(item: ExpenseItem): boolean {
|
|
|
- return item.status === 1 || item.status === '1'
|
|
|
-}
|
|
|
-
|
|
|
-const leftItems = computed(() =>
|
|
|
- [...expenseTree.value].filter(isExpenseEnabled).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)),
|
|
|
-)
|
|
|
-
|
|
|
-const selectedLeft = computed(() => leftItems.value.find((i) => i.id === selectedLeftId.value) ?? null)
|
|
|
-
|
|
|
-const rightItems = computed(() => {
|
|
|
- const raw = selectedLeft.value?.children
|
|
|
- if (!Array.isArray(raw) || !raw.length) return []
|
|
|
- return [...raw].filter(isExpenseEnabled).sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
|
|
|
-})
|
|
|
-
|
|
|
-/** 右栏高亮:当前表单已选项若在右侧列表中则高亮 */
|
|
|
-const highlightRightId = computed(() => {
|
|
|
- const id = form.value.expenseItemId
|
|
|
- if (id == null || !rightItems.value.some((c) => c.id === id)) return null
|
|
|
- return id
|
|
|
-})
|
|
|
-
|
|
|
-/** 从 expenseItemId 推导所属一级(沿 parent 链回到根) */
|
|
|
-function findRootForItem(itemId: number): ExpenseItem | null {
|
|
|
- const node = flatList.value.find((i) => i.id === itemId)
|
|
|
- if (!node) return null
|
|
|
- let cur: ExpenseItem | null = node
|
|
|
- const guard = new Set<number>()
|
|
|
- while (cur !== null && !guard.has(cur.id)) {
|
|
|
- guard.add(cur.id)
|
|
|
- if (!cur.parentId || cur.parentId === 0) return cur
|
|
|
- const pid: number = cur.parentId
|
|
|
- cur = flatList.value.find((i) => i.id === pid) ?? null
|
|
|
- }
|
|
|
- return node
|
|
|
-}
|
|
|
-
|
|
|
-function syncExpensePickerSelection() {
|
|
|
- const id = form.value.expenseItemId
|
|
|
- if (!leftItems.value.length) {
|
|
|
- selectedLeftId.value = null
|
|
|
- return
|
|
|
- }
|
|
|
- if (id == null) {
|
|
|
- selectedLeftId.value = leftItems.value[0].id
|
|
|
- return
|
|
|
- }
|
|
|
- const root = findRootForItem(id)
|
|
|
- const match = root && leftItems.value.some((x) => x.id === root.id) ? root : null
|
|
|
- selectedLeftId.value = match?.id ?? leftItems.value[0].id
|
|
|
-}
|
|
|
-
|
|
|
-watch(showExpensePicker, (open) => {
|
|
|
- if (open) syncExpensePickerSelection()
|
|
|
})
|
|
|
|
|
|
-function onPickLeft(item: ExpenseItem) {
|
|
|
- selectedLeftId.value = item.id
|
|
|
-}
|
|
|
-
|
|
|
-function confirmExpensePick(id: number) {
|
|
|
- form.value.expenseItemId = id
|
|
|
- showExpensePicker.value = false
|
|
|
-}
|
|
|
-
|
|
|
-function validateAmount(val: string): boolean {
|
|
|
- return Number(val) > 0
|
|
|
-}
|
|
|
+/** 与校验提示联动:未填「申请原因 / 申请金额」时红框提示 */
|
|
|
+const fieldErr = reactive({ reason: false, amount: false })
|
|
|
|
|
|
/** 保存草稿前校验(与表单项 * 一致) */
|
|
|
function validateAdvanceApplyForm(): boolean {
|
|
|
+ fieldErr.reason = false
|
|
|
+ fieldErr.amount = false
|
|
|
if (!form.value.reason?.trim()) {
|
|
|
+ fieldErr.reason = true
|
|
|
showToast('请输入申请原因')
|
|
|
return false
|
|
|
}
|
|
|
- if (form.value.expenseItemId == null || form.value.expenseItemId === 0) {
|
|
|
- showToast('请选择费用项目')
|
|
|
- return false
|
|
|
- }
|
|
|
const raw = String(form.value.amount ?? '').trim()
|
|
|
if (!raw) {
|
|
|
+ fieldErr.amount = true
|
|
|
showToast('请输入申请金额')
|
|
|
return false
|
|
|
}
|
|
|
const amt = Number(raw)
|
|
|
if (Number.isNaN(amt) || amt <= 0) {
|
|
|
+ fieldErr.amount = true
|
|
|
showToast('请输入大于 0 的申请金额')
|
|
|
return false
|
|
|
}
|
|
|
@@ -393,9 +241,6 @@ async function onSubmit() {
|
|
|
amount: Number(form.value.amount),
|
|
|
remark: form.value.remark,
|
|
|
}
|
|
|
- if (form.value.expenseItemId) {
|
|
|
- data.expenseItemId = form.value.expenseItemId
|
|
|
- }
|
|
|
|
|
|
let id: number
|
|
|
if (isEdit.value || savedId.value) {
|
|
|
@@ -442,13 +287,6 @@ async function onSubmit() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function pickExpenseItemId(data: Record<string, unknown>): number | null {
|
|
|
- const raw = data.expenseItemId ?? data.expense_item_id
|
|
|
- if (raw == null || raw === '') return null
|
|
|
- const n = Number(raw)
|
|
|
- return Number.isFinite(n) && n > 0 ? n : null
|
|
|
-}
|
|
|
-
|
|
|
function pickRemark(data: Record<string, unknown>): string {
|
|
|
const v = data.remark
|
|
|
if (v == null) return ''
|
|
|
@@ -466,7 +304,6 @@ async function loadEditData() {
|
|
|
reason: (data.reason as string) || '',
|
|
|
amount: data.amount != null ? String(data.amount) : '',
|
|
|
remark: pickRemark(data),
|
|
|
- expenseItemId: pickExpenseItemId(data),
|
|
|
}
|
|
|
await attachment.loadList('advance', id)
|
|
|
}
|
|
|
@@ -479,7 +316,6 @@ onMounted(async () => {
|
|
|
// 从详情「重新提交」会写入 bx_edit_resubmit;已进入编辑页即视为完成跳转,
|
|
|
// 若保留标记,用户点顶栏返回详情时仍会误触发「是否立即提交」弹窗。
|
|
|
sessionStorage.removeItem('bx_edit_resubmit')
|
|
|
- await loadExpenseItems()
|
|
|
await loadEditData()
|
|
|
})
|
|
|
|
|
|
@@ -581,140 +417,4 @@ watch(
|
|
|
word-break: break-word;
|
|
|
}
|
|
|
|
|
|
-.expense-picker-popup.van-popup--bottom.van-popup--round {
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-
|
|
|
-.expense-two-col-sheet {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- height: 100%;
|
|
|
- min-height: 280px;
|
|
|
- background: var(--surface);
|
|
|
-}
|
|
|
-
|
|
|
-.expense-two-col-sheet__head {
|
|
|
- flex-shrink: 0;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- padding: 16px 16px 10px;
|
|
|
- border-bottom: 1px solid var(--border);
|
|
|
-}
|
|
|
-
|
|
|
-.expense-two-col-sheet__title {
|
|
|
- font-size: 16px;
|
|
|
- font-weight: 700;
|
|
|
- color: var(--text);
|
|
|
-}
|
|
|
-
|
|
|
-.expense-two-col-sheet__close {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- width: 36px;
|
|
|
- height: 36px;
|
|
|
- margin: -8px -8px -8px 0;
|
|
|
- padding: 0;
|
|
|
- border: none;
|
|
|
- background: transparent;
|
|
|
- cursor: pointer;
|
|
|
- border-radius: 8px;
|
|
|
-}
|
|
|
-
|
|
|
-.expense-two-col-sheet__close:active {
|
|
|
- background: var(--bg);
|
|
|
-}
|
|
|
-
|
|
|
-.expense-two-col-sheet__cols {
|
|
|
- flex: 1;
|
|
|
- display: flex;
|
|
|
- min-height: 0;
|
|
|
-}
|
|
|
-
|
|
|
-.expense-col {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- min-width: 0;
|
|
|
- border-right: 1px solid var(--border);
|
|
|
-}
|
|
|
-
|
|
|
-.expense-col:last-child {
|
|
|
- border-right: none;
|
|
|
-}
|
|
|
-
|
|
|
-.expense-col--left {
|
|
|
- flex: 0 0 42%;
|
|
|
- max-width: 46%;
|
|
|
- background: var(--bg);
|
|
|
-}
|
|
|
-
|
|
|
-.expense-col--right {
|
|
|
- flex: 1;
|
|
|
- background: var(--surface);
|
|
|
-}
|
|
|
-
|
|
|
-.expense-col__caption {
|
|
|
- flex-shrink: 0;
|
|
|
- padding: 8px 12px;
|
|
|
- font-size: 11px;
|
|
|
- font-weight: 600;
|
|
|
- color: var(--text-muted);
|
|
|
- letter-spacing: 0.06em;
|
|
|
- border-bottom: 1px solid var(--border);
|
|
|
-}
|
|
|
-
|
|
|
-.expense-col__scroll {
|
|
|
- flex: 1;
|
|
|
- overflow-y: auto;
|
|
|
- -webkit-overflow-scrolling: touch;
|
|
|
- padding: 6px 0 12px;
|
|
|
-}
|
|
|
-
|
|
|
-.expense-col__row {
|
|
|
- display: block;
|
|
|
- width: 100%;
|
|
|
- text-align: left;
|
|
|
- padding: 12px 14px;
|
|
|
- font-size: 14px;
|
|
|
- line-height: 1.35;
|
|
|
- color: var(--text);
|
|
|
- background: transparent;
|
|
|
- border: none;
|
|
|
- cursor: pointer;
|
|
|
- transition: background 0.12s;
|
|
|
-}
|
|
|
-
|
|
|
-.expense-col__row:active {
|
|
|
- background: rgba(16, 185, 129, 0.08);
|
|
|
-}
|
|
|
-
|
|
|
-.expense-col__row.is-active {
|
|
|
- background: var(--primary-bg);
|
|
|
- color: var(--primary-dark);
|
|
|
- font-weight: 600;
|
|
|
-}
|
|
|
-
|
|
|
-.expense-col__empty {
|
|
|
- padding: 24px 14px;
|
|
|
- font-size: 13px;
|
|
|
- color: var(--text-muted);
|
|
|
- text-align: center;
|
|
|
-}
|
|
|
-
|
|
|
-.expense-col__leaf-hint {
|
|
|
- padding: 16px 14px;
|
|
|
-}
|
|
|
-
|
|
|
-.expense-col__leaf-text {
|
|
|
- font-size: 13px;
|
|
|
- color: var(--text-sec);
|
|
|
- line-height: 1.5;
|
|
|
- margin: 0 0 12px;
|
|
|
-}
|
|
|
-
|
|
|
-.btn-sm {
|
|
|
- padding: 10px 14px;
|
|
|
- font-size: 14px;
|
|
|
-}
|
|
|
</style>
|