feat(oa): add permission-controlled shared finance remarks to done center
Share one finance remark per approval instance across Web and mobile. Require the explicit permission and the current tenant user's own done task for every read and write; keep general instance responses free of financial remarks. Add idempotent MySQL migration without automatic role grants, cross-end editors, isolated backend and frontend tests, and rollout requirements. Refs #7; platform validation and target database migration remain pending.
This commit is contained in:
1 parent
86df360f35
commit
0e64b413ce
15 files changed
+1188
-78
No files matched your search
@@ -408,7 +408,7 @@ export function fetchOaTasks(
|
||||
const applyTimeFrom = (params?.applyTimeFrom || '').trim()
|
||||
const applyTimeTo = (params?.applyTimeTo || '').trim()
|
||||
const instanceStatus = (params?.instanceStatus || '').trim()
|
||||
return request.get<{ list: any[]; total: number }>({
|
||||
return request.get<{ list: any[]; total: number; canManageFinanceRemark?: boolean }>({
|
||||
url: `${BASE}/tasks/${kind}`,
|
||||
params: {
|
||||
page: params?.page ?? 1,
|
||||
@@ -664,3 +664,18 @@ export function fetchReplaceOaBankReceipts(
|
||||
data: { items }
|
||||
})
|
||||
}
|
||||
|
||||
export type OaFinanceRemark = { instanceId: string; financeRemark: string }
|
||||
|
||||
export function fetchOaFinanceRemark(instanceId: string) {
|
||||
return request.get<OaFinanceRemark>({
|
||||
url: `${BASE}/instances/${encodeURIComponent(instanceId)}/finance-remark`
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchUpdateOaFinanceRemark(instanceId: string, financeRemark: string) {
|
||||
return request.put<OaFinanceRemark>({
|
||||
url: `${BASE}/instances/${encodeURIComponent(instanceId)}/finance-remark`,
|
||||
data: { financeRemark }
|
||||
})
|
||||
}
|
||||
@@ -6,18 +6,10 @@
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="流程名称">
|
||||
<el-input
|
||||
v-model="searchForm.appName"
|
||||
placeholder="请输入流程名称"
|
||||
clearable
|
||||
/>
|
||||
<el-input v-model="searchForm.appName" placeholder="请输入流程名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="申请人">
|
||||
<el-input
|
||||
v-model="searchForm.initiatorName"
|
||||
placeholder="请输入申请人"
|
||||
clearable
|
||||
/>
|
||||
<el-input v-model="searchForm.initiatorName" placeholder="请输入申请人" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="申请时间">
|
||||
<el-date-picker
|
||||
@@ -76,28 +68,13 @@
|
||||
>
|
||||
<template #default="{ row }">{{ row.appName || row.title || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="initiatorUserName"
|
||||
label="申请人"
|
||||
min-width="120"
|
||||
align="center"
|
||||
>
|
||||
<el-table-column prop="initiatorUserName" label="申请人" min-width="120" align="center">
|
||||
<template #default="{ row }">{{ row.initiatorUserName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="applyTime"
|
||||
label="申请时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
>
|
||||
<el-table-column prop="applyTime" label="申请时间" min-width="170" align="center">
|
||||
<template #default="{ row }">{{ row.applyTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="currentNodeName"
|
||||
label="当前节点"
|
||||
min-width="140"
|
||||
align="center"
|
||||
>
|
||||
<el-table-column prop="currentNodeName" label="当前节点" min-width="140" align="center">
|
||||
<template #default="{ row }">{{ row.currentNodeName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
@@ -116,6 +93,15 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-if="canManageFinanceRemark"
|
||||
prop="financeRemark"
|
||||
label="财务备注"
|
||||
min-width="220"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.financeRemark || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="400" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="openDetail(row)">
|
||||
@@ -130,6 +116,14 @@
|
||||
<el-icon><Printer /></el-icon>
|
||||
打印
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canManageFinanceRemark"
|
||||
class="btn-op-item"
|
||||
@click="openFinanceRemark(row)"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>
|
||||
财务备注
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canUploadBankReceipt"
|
||||
class="btn-op-item"
|
||||
@@ -157,6 +151,49 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
:model-value="financeRemarkVisible"
|
||||
title="财务备注"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
modal-class="protocol-dialog-mask"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!financeRemarkSaving"
|
||||
:show-close="!financeRemarkSaving"
|
||||
@update:model-value="cancelFinanceRemark"
|
||||
>
|
||||
<el-form v-loading="financeRemarkLoading" label-position="top" @submit.prevent>
|
||||
<el-form-item label="财务备注">
|
||||
<el-input
|
||||
v-model="financeRemarkText"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
:maxlength="512"
|
||||
show-word-limit
|
||||
:disabled="financeRemarkLoading || financeRemarkSaving"
|
||||
placeholder="请输入财务备注,留空可清空"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" :disabled="financeRemarkSaving" @click="cancelFinanceRemark">
|
||||
<el-icon><Close /></el-icon>取消
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
:loading="financeRemarkSaving"
|
||||
:disabled="financeRemarkLoading"
|
||||
@click="saveFinanceRemark"
|
||||
>
|
||||
<el-icon><Check /></el-icon>确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<OaBankReceiptDialog
|
||||
v-model="bankReceiptVisible"
|
||||
:instance-id="bankReceiptInstanceId"
|
||||
@@ -166,11 +203,22 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Printer, RefreshLeft, Search, Upload, View } from '@element-plus/icons-vue'
|
||||
import { fetchOaInstanceDetail, fetchOaTasks } from '@/api/oaApproval'
|
||||
import {
|
||||
Check,
|
||||
Close,
|
||||
EditPen,
|
||||
Printer,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
Upload,
|
||||
View
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchOaInstanceDetail,
|
||||
fetchOaTasks,
|
||||
fetchOaFinanceRemark,
|
||||
fetchUpdateOaFinanceRemark
|
||||
} from '@/api/oaApproval'
|
||||
import OaBankReceiptDialog from '@/components/oa-approval/OaBankReceiptDialog.vue'
|
||||
import { useMenuPerm } from '@/hooks/core/useMenuPerm'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
@@ -187,6 +235,82 @@
|
||||
const loading = ref(false)
|
||||
const printingId = ref('')
|
||||
const rows = ref<any[]>([])
|
||||
const canManageFinanceRemark = ref(false)
|
||||
const financeRemarkVisible = ref(false)
|
||||
const financeRemarkLoading = ref(false)
|
||||
const financeRemarkSaving = ref(false)
|
||||
const financeRemarkText = ref('')
|
||||
let financeRemarkInstanceId = ''
|
||||
let financeRemarkVersion = 0
|
||||
let listVersion = 0
|
||||
let pageActive = true
|
||||
|
||||
function cancelFinanceRemark() {
|
||||
financeRemarkVersion++
|
||||
financeRemarkVisible.value = false
|
||||
financeRemarkText.value = ''
|
||||
financeRemarkInstanceId = ''
|
||||
financeRemarkLoading.value = false
|
||||
financeRemarkSaving.value = false
|
||||
}
|
||||
|
||||
function clearFinanceAccess() {
|
||||
canManageFinanceRemark.value = false
|
||||
cancelFinanceRemark()
|
||||
rows.value = rows.value.map((row) => {
|
||||
const sanitized = { ...row }
|
||||
delete sanitized.financeRemark
|
||||
return sanitized
|
||||
})
|
||||
}
|
||||
|
||||
async function openFinanceRemark(row: any) {
|
||||
if (!canManageFinanceRemark.value || financeRemarkSaving.value) return
|
||||
const id = String(row.instanceId || '').trim()
|
||||
if (!id) return
|
||||
cancelFinanceRemark()
|
||||
const version = financeRemarkVersion
|
||||
financeRemarkInstanceId = id
|
||||
financeRemarkVisible.value = true
|
||||
financeRemarkLoading.value = true
|
||||
try {
|
||||
const result = await fetchOaFinanceRemark(id)
|
||||
if (version !== financeRemarkVersion) return
|
||||
financeRemarkText.value = result.financeRemark || ''
|
||||
} catch {
|
||||
if (version === financeRemarkVersion) cancelFinanceRemark()
|
||||
} finally {
|
||||
if (version === financeRemarkVersion) financeRemarkLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFinanceRemark() {
|
||||
if (
|
||||
!canManageFinanceRemark.value ||
|
||||
!financeRemarkInstanceId ||
|
||||
financeRemarkLoading.value ||
|
||||
financeRemarkSaving.value
|
||||
)
|
||||
return
|
||||
if (financeRemarkText.value.length > 512) {
|
||||
ElMessage.warning('财务备注最多 512 个字符')
|
||||
return
|
||||
}
|
||||
const version = financeRemarkVersion
|
||||
financeRemarkSaving.value = true
|
||||
try {
|
||||
await fetchUpdateOaFinanceRemark(financeRemarkInstanceId, financeRemarkText.value)
|
||||
if (version !== financeRemarkVersion) return
|
||||
cancelFinanceRemark()
|
||||
ElMessage.success('财务备注已保存')
|
||||
await load()
|
||||
} catch {
|
||||
// 请求层统一提示错误,保留输入以便重试。
|
||||
} finally {
|
||||
if (version === financeRemarkVersion) financeRemarkSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
@@ -210,9 +334,7 @@
|
||||
|
||||
function currentPrinterName() {
|
||||
const info: any = userStore.info || userStore.getUserInfo || {}
|
||||
return String(
|
||||
info.realName || info.nickName || info.userName || info.username || ''
|
||||
).trim()
|
||||
return String(info.realName || info.nickName || info.userName || info.username || '').trim()
|
||||
}
|
||||
|
||||
function statusType(s: string) {
|
||||
@@ -306,16 +428,24 @@
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!pageActive) return
|
||||
const version = ++listVersion
|
||||
clearFinanceAccess()
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchOaTasks('done', buildFilterParams())
|
||||
if (version !== listVersion) return
|
||||
canManageFinanceRemark.value = res?.canManageFinanceRemark === true
|
||||
rows.value = res?.list || []
|
||||
if (!canManageFinanceRemark.value) clearFinanceAccess()
|
||||
total.value = res?.total || 0
|
||||
} catch {
|
||||
if (version !== listVersion) return
|
||||
clearFinanceAccess()
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (version === listVersion) loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +458,14 @@
|
||||
{ debounce: 300, deep: true }
|
||||
)
|
||||
|
||||
onDeactivated(() => {
|
||||
pageActive = false
|
||||
listVersion++
|
||||
clearFinanceAccess()
|
||||
})
|
||||
onMounted(load)
|
||||
onActivated(load)
|
||||
onActivated(() => {
|
||||
pageActive = true
|
||||
void load()
|
||||
})
|
||||
</script>
|
||||
Reference in new issue
Block a user