提交
This commit is contained in:
1 parent
3d98104455
commit
0d52442902
76 files changed
+21994
-12642
No files matched your search
@@ -386,6 +386,39 @@ export function fetchOaInstanceHistory(params: {
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchOaInstanceHistoryExport(params: {
|
||||
appId: string
|
||||
instanceId?: string
|
||||
instanceIds?: string
|
||||
instanceStatus?: string
|
||||
initiatorName?: string
|
||||
applyTimeFrom?: string
|
||||
applyTimeTo?: string
|
||||
finishTimeFrom?: string
|
||||
finishTimeTo?: string
|
||||
}) {
|
||||
return request.get<{
|
||||
appName?: string
|
||||
businessColumns?: Array<{
|
||||
key: string
|
||||
label: string
|
||||
kind?: string
|
||||
subLabel?: string
|
||||
}>
|
||||
fixedColumns?: Array<{ key: string; label: string }>
|
||||
rows?: Array<{
|
||||
index?: number
|
||||
id?: string
|
||||
business?: Record<string, string>
|
||||
fixed?: Record<string, string>
|
||||
}>
|
||||
total?: number
|
||||
}>({
|
||||
url: `${BASE}/instances/history/export`,
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function fetchOaPendingTaskCount() {
|
||||
return request.get<{ count: number }>({ url: `${BASE}/tasks/pending/count` })
|
||||
}
|
||||
|
||||
@@ -1,388 +0,0 @@
|
||||
<!-- OA 发起审批 · 单流程历史记录弹窗 -->
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
class="dialog-form-layout"
|
||||
:title="`历史记录 · ${appName}`"
|
||||
width="1080px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
align-center
|
||||
modal-class="protocol-dialog-mask"
|
||||
@closed="onClosed"
|
||||
>
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="流程名称">
|
||||
<el-input :model-value="appName" readonly placeholder="流程名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审批状态">
|
||||
<el-select v-model="searchForm.instanceStatus" placeholder="全部" clearable>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="进行中" value="running" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="已拒绝" value="rejected" />
|
||||
<el-option label="已打回" value="returned" />
|
||||
<el-option label="已撤销" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="审批 ID">
|
||||
<el-input
|
||||
v-model="searchForm.instanceId"
|
||||
placeholder="请输入审批 ID"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="发起时间">
|
||||
<el-date-picker
|
||||
v-model="applyTimeRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="完成时间">
|
||||
<el-date-picker
|
||||
v-model="finishTimeRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="canViewAll" label="申请人">
|
||||
<el-input
|
||||
v-model="searchForm.initiatorName"
|
||||
placeholder="请输入申请人"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="rows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
empty-text="暂无历史记录"
|
||||
>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="indexMethod"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="审批 ID"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.id || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="appName"
|
||||
label="流程名称"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.appName || appName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="initiatorUserName"
|
||||
label="申请人"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ row.initiatorUserName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-for="label in previewFieldLabels"
|
||||
:key="label"
|
||||
:label="label"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ previewValue(row, label) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="applyTime"
|
||||
label="发起时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ row.applyTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="finishTime"
|
||||
label="完成时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ row.finishTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="instanceStatus"
|
||||
label="审批状态"
|
||||
min-width="110"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="statusType(row.instanceStatus || row.status)">
|
||||
{{ statusText(row.instanceStatus || row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="currentNodeName"
|
||||
label="当前节点"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.currentNodeName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="260" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="openDetail(row)">
|
||||
<el-icon><View /></el-icon>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:loading="printingId === row.id"
|
||||
@click="printRow(row)"
|
||||
>
|
||||
<el-icon><Printer /></el-icon>
|
||||
打印
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
background
|
||||
@size-change="load"
|
||||
@current-change="load"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Printer, RefreshLeft, Search, View } from '@element-plus/icons-vue'
|
||||
import { fetchOaInstanceDetail, fetchOaInstanceHistory } from '@/api/oaApproval'
|
||||
import { useMenuPerm } from '@/hooks/core/useMenuPerm'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { exportOaApprovalPdf } from '@/utils/oa-approval-print'
|
||||
|
||||
defineOptions({ name: 'OaAppHistoryDialog' })
|
||||
|
||||
const OA_START_HISTORY_ALL = 'oa:start:history:all'
|
||||
const { hasMenuPerm } = useMenuPerm()
|
||||
const canViewAll = computed(() => hasMenuPerm(OA_START_HISTORY_ALL))
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const visible = ref(false)
|
||||
const appId = ref('')
|
||||
const appName = ref('')
|
||||
const loading = ref(false)
|
||||
const printingId = ref('')
|
||||
const rows = ref<any[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const applyTimeRange = ref<string[] | null>(null)
|
||||
const finishTimeRange = ref<string[] | null>(null)
|
||||
const searchForm = reactive({
|
||||
instanceStatus: '',
|
||||
instanceId: '',
|
||||
initiatorName: ''
|
||||
})
|
||||
|
||||
const previewFieldLabels = computed(() => {
|
||||
const labels: string[] = []
|
||||
for (const row of rows.value) {
|
||||
for (const f of row.previewFields || []) {
|
||||
const lb = String(f?.label || '').trim()
|
||||
if (lb && !labels.includes(lb)) labels.push(lb)
|
||||
}
|
||||
}
|
||||
return labels.slice(0, 6)
|
||||
})
|
||||
|
||||
function indexMethod(index: number) {
|
||||
return (page.value - 1) * pageSize.value + index + 1
|
||||
}
|
||||
|
||||
function previewValue(row: any, label: string) {
|
||||
const hit = (row.previewFields || []).find((f: any) => f.label === label)
|
||||
return hit?.value || '-'
|
||||
}
|
||||
|
||||
function currentPrinterName() {
|
||||
const info: any = userStore.info || userStore.getUserInfo || {}
|
||||
return String(
|
||||
info.realName || info.nickName || info.userName || info.username || ''
|
||||
).trim()
|
||||
}
|
||||
|
||||
function statusType(s: string) {
|
||||
if (s === 'pending' || s === 'running') return 'warning'
|
||||
if (s === 'approved' || s === 'completed' || s === 'done') return 'success'
|
||||
if (s === 'rejected' || s === 'cancelled' || s === 'returned') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function statusText(s: string) {
|
||||
if (!s) return '-'
|
||||
const map: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
running: '进行中',
|
||||
approved: '已同意',
|
||||
rejected: '已拒绝',
|
||||
completed: '已完成',
|
||||
cancelled: '已撤销',
|
||||
done: '已结束',
|
||||
returned: '已打回',
|
||||
transferred: '已转交'
|
||||
}
|
||||
return map[s] || s
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
searchForm.instanceStatus = ''
|
||||
searchForm.instanceId = ''
|
||||
searchForm.initiatorName = ''
|
||||
applyTimeRange.value = null
|
||||
finishTimeRange.value = null
|
||||
page.value = 1
|
||||
}
|
||||
|
||||
function open(app: { id: string; appName?: string }) {
|
||||
appId.value = String(app?.id || '').trim()
|
||||
appName.value = String(app?.appName || '').trim()
|
||||
resetFilters()
|
||||
visible.value = true
|
||||
void load()
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
printingId.value = ''
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
void load()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
resetFilters()
|
||||
void load()
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!appId.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
appId: appId.value,
|
||||
instanceId: searchForm.instanceId || undefined,
|
||||
instanceStatus: searchForm.instanceStatus || undefined,
|
||||
applyTimeFrom: applyTimeRange.value?.[0],
|
||||
applyTimeTo: applyTimeRange.value?.[1],
|
||||
finishTimeFrom: finishTimeRange.value?.[0],
|
||||
finishTimeTo: finishTimeRange.value?.[1],
|
||||
page: page.value,
|
||||
pageSize: pageSize.value
|
||||
}
|
||||
if (canViewAll.value && searchForm.initiatorName) {
|
||||
params.initiatorName = searchForm.initiatorName
|
||||
}
|
||||
const data = await fetchOaInstanceHistory(params)
|
||||
rows.value = data?.list || []
|
||||
total.value = Number(data?.total || 0)
|
||||
} catch {
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openDetail(row: any) {
|
||||
const id = row?.id
|
||||
if (!id) return
|
||||
visible.value = false
|
||||
router.push({ path: `/system-manage/oa-approval/instances/${id}` })
|
||||
}
|
||||
|
||||
async function printRow(row: any) {
|
||||
const id = String(row?.id || '').trim()
|
||||
if (!id || printingId.value) return
|
||||
printingId.value = id
|
||||
let detail: any
|
||||
try {
|
||||
detail = await fetchOaInstanceDetail(id)
|
||||
} catch {
|
||||
printingId.value = ''
|
||||
ElMessage.error('加载审批单失败')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await exportOaApprovalPdf(detail, { printerName: currentPrinterName() })
|
||||
ElMessage.success('PDF 已下载')
|
||||
} catch {
|
||||
ElMessage.error('导出 PDF 失败')
|
||||
} finally {
|
||||
printingId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
@@ -1444,6 +1444,19 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
|
||||
menuCode: 'oa:start'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'OA000000000000000000000000000203',
|
||||
name: 'OaApprovalHistory',
|
||||
path: 'history/:appId',
|
||||
component: '/oa-approval/history/index',
|
||||
meta: {
|
||||
title: '历史记录',
|
||||
keepAlive: true,
|
||||
isHide: true,
|
||||
worktabMulti: true,
|
||||
menuCode: 'oa:start'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'OA000000000000000000000000000109',
|
||||
name: 'OaApprovalDrafts',
|
||||
|
||||
@@ -206,9 +206,9 @@ export const useWorktabStore = defineStore(
|
||||
removeKeepAliveExclude(tab.name)
|
||||
}
|
||||
|
||||
// 先根据路由名称查找(应对动态路由参数导致的多开问题),找不到再根据路径查找
|
||||
// 默认:先按 name 复用(动态参数同页);worktabMulti 仅按 path 匹配以支持同 name 多开
|
||||
let existingIndex = -1
|
||||
if (tab.name) {
|
||||
if (!tab.worktabMulti && tab.name) {
|
||||
existingIndex = opened.value.findIndex((t) => t.name === tab.name)
|
||||
}
|
||||
if (existingIndex === -1) {
|
||||
@@ -239,6 +239,7 @@ export const useWorktabStore = defineStore(
|
||||
title: tab.title || existingTab.title,
|
||||
fixedTab: tab.fixedTab ?? existingTab.fixedTab,
|
||||
keepAlive: tab.keepAlive ?? existingTab.keepAlive,
|
||||
worktabMulti: tab.worktabMulti ?? existingTab.worktabMulti,
|
||||
name: tab.name || existingTab.name,
|
||||
icon: tab.icon || existingTab.icon
|
||||
}
|
||||
@@ -430,6 +431,8 @@ export const useWorktabStore = defineStore(
|
||||
*/
|
||||
const addKeepAliveExclude = (tab: WorkTab): void => {
|
||||
if (!tab.keepAlive || !tab.name) return
|
||||
// 同 name 多开:仍有其它同名标签时勿排除,否则会清掉其它实例缓存
|
||||
if (opened.value.some((t) => t.name === tab.name)) return
|
||||
|
||||
if (!keepAliveExclude.value.includes(tab.name)) {
|
||||
keepAliveExclude.value.push(tab.name)
|
||||
@@ -449,9 +452,15 @@ export const useWorktabStore = defineStore(
|
||||
* 将传入的一组选项卡的组件名称标记为排除缓存
|
||||
*/
|
||||
const markTabsToRemove = (tabs: WorkTab[]): void => {
|
||||
const removePaths = new Set(tabs.map((t) => t.path))
|
||||
tabs.forEach((tab) => {
|
||||
if (tab.name) {
|
||||
addKeepAliveExclude(tab)
|
||||
if (!tab.keepAlive || !tab.name) return
|
||||
const stillOpen = opened.value.some(
|
||||
(t) => t.name === tab.name && !removePaths.has(t.path)
|
||||
)
|
||||
if (stillOpen) return
|
||||
if (!keepAliveExclude.value.includes(tab.name)) {
|
||||
keepAliveExclude.value.push(tab.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,6 +60,11 @@ export interface RouteMeta extends Record<string | number | symbol, unknown> {
|
||||
roles?: string[]
|
||||
/** 是否固定标签页 */
|
||||
fixedTab?: boolean
|
||||
/**
|
||||
* 同路由 name 允许多开(按完整 path 区分)
|
||||
* 用于如「历史记录/:appId」等需同时保留多份的页面
|
||||
*/
|
||||
worktabMulti?: boolean
|
||||
/** 激活菜单路径 */
|
||||
activePath?: string
|
||||
/** 是否为全屏页面 */
|
||||
|
||||
@@ -91,6 +91,11 @@ export interface WorkTab {
|
||||
keepAlive: boolean
|
||||
/** 是否固定标签 */
|
||||
fixedTab?: boolean
|
||||
/**
|
||||
* 同路由 name 允许多开(按 path 区分,如历史记录按 appId)
|
||||
* 为 true 时 openTab 不按 name 复用已有标签
|
||||
*/
|
||||
worktabMulti?: boolean
|
||||
/** 路由参数 */
|
||||
params?: object
|
||||
/** 路由查询参数 */
|
||||
|
||||
@@ -63,7 +63,8 @@ export const setWorktab = (to: RouteLocationNormalized): void => {
|
||||
keepAlive: meta.keepAlive as boolean,
|
||||
params,
|
||||
query,
|
||||
fixedTab: meta.fixedTab as boolean
|
||||
fixedTab: meta.fixedTab as boolean,
|
||||
worktabMulti: Boolean(meta.worktabMulti)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
import ExcelJS from 'exceljs'
|
||||
import FileSaver from 'file-saver'
|
||||
|
||||
export type OaHistoryExportColumn = {
|
||||
key: string
|
||||
label: string
|
||||
kind?: 'business' | 'related'
|
||||
subLabel?: string
|
||||
}
|
||||
|
||||
export type OaHistoryExportFixedColumn = {
|
||||
key: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export type OaHistoryExportRow = {
|
||||
index?: number
|
||||
id?: string
|
||||
business?: Record<string, string>
|
||||
fixed?: Record<string, string>
|
||||
}
|
||||
|
||||
export type OaHistoryExportPayload = {
|
||||
appName?: string
|
||||
businessColumns?: OaHistoryExportColumn[]
|
||||
fixedColumns?: OaHistoryExportFixedColumn[]
|
||||
rows?: OaHistoryExportRow[]
|
||||
}
|
||||
|
||||
type SheetColumn = {
|
||||
key: string
|
||||
label: string
|
||||
subLabel?: string
|
||||
width: number
|
||||
}
|
||||
|
||||
const HEADER_FILL: ExcelJS.Fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FFF0FDF4' }
|
||||
}
|
||||
|
||||
const HEADER_FONT: Partial<ExcelJS.Font> = {
|
||||
bold: true,
|
||||
color: { argb: 'FF065F46' },
|
||||
size: 11
|
||||
}
|
||||
|
||||
const BORDER: Partial<ExcelJS.Borders> = {
|
||||
top: { style: 'thin', color: { argb: 'FFE2E8F0' } },
|
||||
left: { style: 'thin', color: { argb: 'FFE2E8F0' } },
|
||||
bottom: { style: 'thin', color: { argb: 'FFE2E8F0' } },
|
||||
right: { style: 'thin', color: { argb: 'FFE2E8F0' } }
|
||||
}
|
||||
|
||||
function pad(n: number) {
|
||||
return String(n).padStart(2, '0')
|
||||
}
|
||||
|
||||
function exportTimestamp() {
|
||||
const d = new Date()
|
||||
return (
|
||||
`${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}` +
|
||||
`${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`
|
||||
)
|
||||
}
|
||||
|
||||
function sanitizeFilename(name: string) {
|
||||
return String(name || '历史记录')
|
||||
.replace(/[\\/:*?"<>|]/g, '_')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function styleHeaderCell(cell: ExcelJS.Cell) {
|
||||
cell.fill = HEADER_FILL
|
||||
cell.font = HEADER_FONT
|
||||
cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true }
|
||||
cell.border = BORDER
|
||||
}
|
||||
|
||||
function styleBodyCell(cell: ExcelJS.Cell) {
|
||||
cell.alignment = { vertical: 'top', horizontal: 'left', wrapText: true }
|
||||
cell.border = BORDER
|
||||
}
|
||||
|
||||
function buildSheetColumns(payload: OaHistoryExportPayload): SheetColumn[] {
|
||||
const cols: SheetColumn[] = [
|
||||
{ key: '__index', label: '序号', width: 8 },
|
||||
{ key: '__id', label: '数据id', width: 28 }
|
||||
]
|
||||
for (const col of payload.businessColumns || []) {
|
||||
if (col.kind === 'related') {
|
||||
cols.push({ key: col.key, label: col.label, width: 18 })
|
||||
cols.push({
|
||||
key: `${col.key}__titles`,
|
||||
label: col.label,
|
||||
subLabel: col.subLabel || '关联标题',
|
||||
width: 24
|
||||
})
|
||||
} else {
|
||||
cols.push({ key: col.key, label: col.label, width: 20 })
|
||||
}
|
||||
}
|
||||
for (const col of payload.fixedColumns || []) {
|
||||
cols.push({
|
||||
key: col.key,
|
||||
label: col.label,
|
||||
width: col.key === 'approvalRecord' ? 48 : 18
|
||||
})
|
||||
}
|
||||
return cols
|
||||
}
|
||||
|
||||
function cellValue(row: OaHistoryExportRow, key: string) {
|
||||
if (key === '__index') return row.index ?? ''
|
||||
if (key === '__id') return row.id ?? ''
|
||||
const fixed = row.fixed || {}
|
||||
const business = row.business || {}
|
||||
if (key in fixed) return fixed[key] ?? ''
|
||||
return business[key] ?? ''
|
||||
}
|
||||
|
||||
function writeHeaderRows(worksheet: ExcelJS.Worksheet, sheetColumns: SheetColumn[]) {
|
||||
const headerRow1 = worksheet.getRow(1)
|
||||
const headerRow2 = worksheet.getRow(2)
|
||||
headerRow1.height = 24
|
||||
headerRow2.height = 20
|
||||
|
||||
let colIdx = 1
|
||||
let i = 0
|
||||
while (i < sheetColumns.length) {
|
||||
const col = sheetColumns[i]
|
||||
const next = sheetColumns[i + 1]
|
||||
const isRelatedPair = Boolean(next?.subLabel && next.label === col.label && !col.subLabel)
|
||||
|
||||
if (isRelatedPair) {
|
||||
const c1 = headerRow1.getCell(colIdx)
|
||||
c1.value = col.label
|
||||
styleHeaderCell(c1)
|
||||
worksheet.mergeCells(1, colIdx, 1, colIdx + 1)
|
||||
|
||||
const c2a = headerRow2.getCell(colIdx)
|
||||
c2a.value = ''
|
||||
styleHeaderCell(c2a)
|
||||
|
||||
const c2b = headerRow2.getCell(colIdx + 1)
|
||||
c2b.value = next.subLabel || '关联标题'
|
||||
styleHeaderCell(c2b)
|
||||
|
||||
colIdx += 2
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
|
||||
const c1 = headerRow1.getCell(colIdx)
|
||||
c1.value = col.label
|
||||
styleHeaderCell(c1)
|
||||
const c2 = headerRow2.getCell(colIdx)
|
||||
c2.value = ''
|
||||
styleHeaderCell(c2)
|
||||
worksheet.mergeCells(1, colIdx, 2, colIdx)
|
||||
colIdx += 1
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadOaHistoryExcel(
|
||||
payload: OaHistoryExportPayload,
|
||||
options?: { filenamePrefix?: string }
|
||||
) {
|
||||
const rows = payload.rows || []
|
||||
if (!rows.length) {
|
||||
throw new Error('暂无可导出数据')
|
||||
}
|
||||
|
||||
const sheetColumns = buildSheetColumns(payload)
|
||||
const workbook = new ExcelJS.Workbook()
|
||||
const sheetName = sanitizeFilename(payload.appName || '历史记录').slice(0, 31)
|
||||
const worksheet = workbook.addWorksheet(sheetName)
|
||||
|
||||
worksheet.columns = sheetColumns.map((c) => ({ width: c.width }))
|
||||
writeHeaderRows(worksheet, sheetColumns)
|
||||
|
||||
rows.forEach((row, rowIndex) => {
|
||||
const excelRow = worksheet.getRow(rowIndex + 3)
|
||||
sheetColumns.forEach((col, i) => {
|
||||
const cell = excelRow.getCell(i + 1)
|
||||
cell.value = cellValue(row, col.key)
|
||||
styleBodyCell(cell)
|
||||
})
|
||||
})
|
||||
|
||||
worksheet.views = [{ state: 'frozen', ySplit: 2 }]
|
||||
|
||||
const buffer = await workbook.xlsx.writeBuffer()
|
||||
const prefix = sanitizeFilename(options?.filenamePrefix || payload.appName || '历史记录')
|
||||
const blob = new Blob([buffer], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
})
|
||||
FileSaver.saveAs(blob, `${prefix}-${exportTimestamp()}.xlsx`)
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export const PRODUCTION_WORK_ORDER_TABS: ProductionWorkOrderTabConfig[] = [
|
||||
},
|
||||
{
|
||||
key: 'temporary',
|
||||
label: '临时生产',
|
||||
label: '扩增/传代生产',
|
||||
group: 'temporary',
|
||||
workOrderType: 'temporary',
|
||||
listLayout: 'standalone'
|
||||
|
||||
@@ -119,6 +119,14 @@ const SOP_SHARED_CONTENT_STYLE = `
|
||||
.sop-body-table tbody {
|
||||
height: 100%;
|
||||
}
|
||||
.sop-header-table.sop-row-hidden,
|
||||
.sop-body-table tr.sop-row-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
.sop-record.sop-a4:has(.sop-row-hidden) {
|
||||
min-height: auto !important;
|
||||
height: auto !important;
|
||||
}
|
||||
.sop-body-table tr.sop-fill-row {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,874 @@
|
||||
<!-- OA 发起审批 · 单流程历史记录(WorkTab 页,按 appId 多开) -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">{{ pageTitle }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="filter-block">
|
||||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||||
<div class="filter-grid">
|
||||
<el-form-item label="流程名称">
|
||||
<el-input :model-value="appName" readonly placeholder="流程名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审批状态">
|
||||
<el-select v-model="searchForm.instanceStatus" placeholder="全部" clearable>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="进行中" value="running" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="已拒绝" value="rejected" />
|
||||
<el-option label="已打回" value="returned" />
|
||||
<el-option label="已撤销" value="cancelled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="审批 ID">
|
||||
<el-input
|
||||
v-model="searchForm.instanceId"
|
||||
placeholder="请输入审批 ID"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="发起时间">
|
||||
<el-date-picker
|
||||
v-model="applyTimeRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="完成时间">
|
||||
<el-date-picker
|
||||
v-model="finishTimeRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="canViewAll" label="申请人">
|
||||
<el-input
|
||||
v-model="searchForm.initiatorName"
|
||||
placeholder="请输入申请人"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div class="filter-actions-row">
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="list-toolbar">
|
||||
<div class="list-toolbar-row">
|
||||
<div class="list-toolbar-selected">
|
||||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||||
</div>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="selectedRows.length === 0 || exporting"
|
||||
:loading="exporting && exportMode === 'selected'"
|
||||
@click="handleExportSelected"
|
||||
>
|
||||
<el-icon><Download /></el-icon>
|
||||
导出勾选
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-ghost"
|
||||
:disabled="total <= 0 || exporting"
|
||||
:loading="exporting && exportMode === 'all'"
|
||||
@click="handleExportAll"
|
||||
>
|
||||
<el-icon><Download /></el-icon>
|
||||
导出全部
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="rows"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
empty-text="暂无历史记录"
|
||||
@selection-change="onSelectionChange"
|
||||
@row-dblclick="openDetail"
|
||||
>
|
||||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
min-width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="indexMethod"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="审批 ID"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.id || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="appName"
|
||||
label="流程名称"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.appName || appName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="initiatorUserName"
|
||||
label="申请人"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ row.initiatorUserName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
v-for="label in previewFieldLabels"
|
||||
:key="label"
|
||||
:label="label"
|
||||
:min-width="previewColumnMinWidth(label)"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:show-overflow-tooltip="!isUploadPreviewLabel(label)"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div
|
||||
v-if="previewFiles(row, label).length"
|
||||
class="hist-attach"
|
||||
:class="{ 'is-multi': previewFiles(row, label).length > 1 }"
|
||||
>
|
||||
<div
|
||||
v-for="(file, idx) in previewFiles(row, label)"
|
||||
:key="`${file.url}-${idx}`"
|
||||
class="hist-attach__card"
|
||||
:title="file.name || '附件'"
|
||||
>
|
||||
<div class="hist-attach__preview">
|
||||
<el-image
|
||||
v-if="isImageFile(file)"
|
||||
class="hist-attach__img"
|
||||
:src="fileFullUrl(file.url)"
|
||||
:preview-src-list="imagePreviewList(row, label)"
|
||||
:initial-index="imagePreviewIndex(row, label, file.url)"
|
||||
fit="cover"
|
||||
preview-teleported
|
||||
>
|
||||
<template #error>
|
||||
<div class="hist-attach__fallback">
|
||||
<el-icon :size="18"><Picture /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="hist-attach__doc"
|
||||
@click="previewFile(file)"
|
||||
>
|
||||
<span class="hist-attach__doc-icon">
|
||||
<el-icon :size="18"><Document /></el-icon>
|
||||
</span>
|
||||
<span class="hist-attach__ext">{{ fileExt(file) }}</span>
|
||||
</button>
|
||||
<div class="hist-attach__hint" aria-hidden="true">
|
||||
<el-icon :size="14">
|
||||
<ZoomIn v-if="isImageFile(file)" />
|
||||
<View v-else />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hist-attach__footer">
|
||||
<span class="hist-attach__name">{{ shortFileLabel(file) }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="hist-attach__dl"
|
||||
title="下载"
|
||||
@click.stop="downloadFile(file)"
|
||||
>
|
||||
<el-icon :size="12"><Download /></el-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else>{{ previewValue(row, label) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="applyTime"
|
||||
label="发起时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ row.applyTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="finishTime"
|
||||
label="完成时间"
|
||||
min-width="170"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">{{ row.finishTime || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="instanceStatus"
|
||||
label="审批状态"
|
||||
min-width="110"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small" :type="statusType(row.instanceStatus || row.status)">
|
||||
{{ statusText(row.instanceStatus || row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="currentNodeName"
|
||||
label="当前节点"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template #default="{ row }">{{ row.currentNodeName || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" min-width="260" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="openDetail(row)">
|
||||
<el-icon><View /></el-icon>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:loading="printingId === row.id"
|
||||
@click="printRow(row)"
|
||||
>
|
||||
<el-icon><Printer /></el-icon>
|
||||
打印
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination list-pager-bar">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
background
|
||||
@size-change="load"
|
||||
@current-change="load"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Document,
|
||||
Download,
|
||||
Picture,
|
||||
Printer,
|
||||
RefreshLeft,
|
||||
Search,
|
||||
View,
|
||||
ZoomIn
|
||||
} from '@element-plus/icons-vue'
|
||||
import { fetchOaInstanceDetail, fetchOaInstanceHistory, fetchOaInstanceHistoryExport } from '@/api/oaApproval'
|
||||
import { attachmentFullUrl } from '@/composables/useAttachmentActions'
|
||||
import { useMenuPerm } from '@/hooks/core/useMenuPerm'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { useWorktabStore } from '@/store/modules/worktab'
|
||||
import { downloadAttachmentFile } from '@/utils/download-attachment'
|
||||
import { downloadOaHistoryExcel } from '@/utils/oa-history-export'
|
||||
import { exportOaApprovalPdf } from '@/utils/oa-approval-print'
|
||||
import { isOaUploadImageFile } from '@/utils/oa-upload-file-type'
|
||||
|
||||
defineOptions({ name: 'OaApprovalHistory' })
|
||||
|
||||
type PreviewFile = { url: string; name: string }
|
||||
|
||||
const OA_START_HISTORY_ALL = 'oa:start:history:all'
|
||||
const { hasMenuPerm } = useMenuPerm()
|
||||
const canViewAll = computed(() => hasMenuPerm(OA_START_HISTORY_ALL))
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const worktabStore = useWorktabStore()
|
||||
|
||||
// KeepAlive 多开:setup 时锁定本实例对应的流程,避免全局 route 切走后串数据/标题
|
||||
const boundAppId = String(route.params.appId || route.query.appId || '').trim()
|
||||
const boundPath = String(route.path || '')
|
||||
const boundAppName = ref(String(route.query.appName || '').trim())
|
||||
|
||||
const loading = ref(false)
|
||||
const exporting = ref(false)
|
||||
const exportMode = ref<'selected' | 'all' | ''>('')
|
||||
const printingId = ref('')
|
||||
const selectedRows = ref<any[]>([])
|
||||
const rows = ref<any[]>([])
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
const applyTimeRange = ref<string[] | null>(null)
|
||||
const finishTimeRange = ref<string[] | null>(null)
|
||||
const searchForm = reactive({
|
||||
instanceStatus: '',
|
||||
instanceId: '',
|
||||
initiatorName: ''
|
||||
})
|
||||
|
||||
const appId = computed(() => boundAppId)
|
||||
const appName = computed(() => boundAppName.value)
|
||||
const pageTitle = computed(() =>
|
||||
appName.value ? `历史记录 · ${appName.value}` : '历史记录'
|
||||
)
|
||||
|
||||
function isActiveHistoryInstance() {
|
||||
return (
|
||||
route.name === 'OaApprovalHistory' &&
|
||||
String(route.params.appId || route.query.appId || '').trim() === boundAppId
|
||||
)
|
||||
}
|
||||
|
||||
const previewFieldLabels = computed(() => {
|
||||
const labels: string[] = []
|
||||
for (const row of rows.value) {
|
||||
for (const f of row.previewFields || []) {
|
||||
const lb = String(f?.label || '').trim()
|
||||
if (lb && !labels.includes(lb)) labels.push(lb)
|
||||
}
|
||||
}
|
||||
return labels.slice(0, 6)
|
||||
})
|
||||
|
||||
const uploadPreviewLabels = computed(() => {
|
||||
const set = new Set<string>()
|
||||
for (const row of rows.value) {
|
||||
for (const f of row.previewFields || []) {
|
||||
const lb = String(f?.label || '').trim()
|
||||
if (!lb) continue
|
||||
const files = Array.isArray(f?.files) ? f.files : []
|
||||
if (files.length || isUploadFieldType(f?.fieldType)) set.add(lb)
|
||||
}
|
||||
}
|
||||
return set
|
||||
})
|
||||
|
||||
function isUploadFieldType(t: unknown) {
|
||||
const s = String(t || '')
|
||||
return s === 'upload' || s === 'fcUpload' || s === 'image' || s === 'oaAttachment'
|
||||
}
|
||||
|
||||
function isUploadPreviewLabel(label: string) {
|
||||
return uploadPreviewLabels.value.has(label)
|
||||
}
|
||||
|
||||
function previewColumnMinWidth(label: string) {
|
||||
return isUploadPreviewLabel(label) ? 168 : 120
|
||||
}
|
||||
|
||||
function syncWorktabTitle() {
|
||||
if (!isActiveHistoryInstance() || !boundPath) return
|
||||
worktabStore.updateTabTitle(boundPath, pageTitle.value)
|
||||
}
|
||||
|
||||
function indexMethod(index: number) {
|
||||
return (page.value - 1) * pageSize.value + index + 1
|
||||
}
|
||||
|
||||
function onSelectionChange(selection: any[]) {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
function buildExportParams(instanceIds?: string[]) {
|
||||
const params: Record<string, string> = { appId: appId.value }
|
||||
if (searchForm.instanceId) params.instanceId = searchForm.instanceId
|
||||
if (searchForm.instanceStatus) params.instanceStatus = searchForm.instanceStatus
|
||||
if (applyTimeRange.value?.[0]) params.applyTimeFrom = applyTimeRange.value[0]
|
||||
if (applyTimeRange.value?.[1]) params.applyTimeTo = applyTimeRange.value[1]
|
||||
if (finishTimeRange.value?.[0]) params.finishTimeFrom = finishTimeRange.value[0]
|
||||
if (finishTimeRange.value?.[1]) params.finishTimeTo = finishTimeRange.value[1]
|
||||
if (canViewAll.value && searchForm.initiatorName) {
|
||||
params.initiatorName = searchForm.initiatorName
|
||||
}
|
||||
if (instanceIds?.length) {
|
||||
params.instanceIds = instanceIds.join(',')
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
async function runExport(instanceIds?: string[]) {
|
||||
if (!appId.value) {
|
||||
ElMessage.warning('缺少流程信息,无法导出')
|
||||
return
|
||||
}
|
||||
exporting.value = true
|
||||
try {
|
||||
const data = await fetchOaInstanceHistoryExport(buildExportParams(instanceIds))
|
||||
if (!data?.rows?.length) {
|
||||
ElMessage.warning('暂无可导出数据')
|
||||
return
|
||||
}
|
||||
await downloadOaHistoryExcel(data, {
|
||||
filenamePrefix: appName.value || data.appName || '历史记录'
|
||||
})
|
||||
ElMessage.success(`已导出 ${data.rows.length} 条`)
|
||||
} catch {
|
||||
ElMessage.error('导出失败')
|
||||
} finally {
|
||||
exporting.value = false
|
||||
exportMode.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function handleExportSelected() {
|
||||
const ids = selectedRows.value.map((r) => String(r?.id || '').trim()).filter(Boolean)
|
||||
if (!ids.length) {
|
||||
ElMessage.warning('请先勾选要导出的记录')
|
||||
return
|
||||
}
|
||||
exportMode.value = 'selected'
|
||||
void runExport(ids)
|
||||
}
|
||||
|
||||
function handleExportAll() {
|
||||
if (total.value <= 0) {
|
||||
ElMessage.warning('暂无可导出数据')
|
||||
return
|
||||
}
|
||||
exportMode.value = 'all'
|
||||
void runExport()
|
||||
}
|
||||
|
||||
function previewField(row: any, label: string) {
|
||||
return (row.previewFields || []).find((f: any) => f.label === label)
|
||||
}
|
||||
|
||||
function previewValue(row: any, label: string) {
|
||||
const hit = previewField(row, label)
|
||||
return hit?.value || '-'
|
||||
}
|
||||
|
||||
function previewFiles(row: any, label: string): PreviewFile[] {
|
||||
const hit = previewField(row, label)
|
||||
const files = Array.isArray(hit?.files) ? hit.files : []
|
||||
return files
|
||||
.map((f: any) => ({
|
||||
url: String(f?.url || '').trim(),
|
||||
name: String(f?.name || '').trim()
|
||||
}))
|
||||
.filter((f: PreviewFile) => !!f.url)
|
||||
}
|
||||
|
||||
function fileFullUrl(url: string) {
|
||||
return attachmentFullUrl(url) || url
|
||||
}
|
||||
|
||||
function isImageFile(file: PreviewFile) {
|
||||
return isOaUploadImageFile({ url: file.url, name: file.name })
|
||||
}
|
||||
|
||||
function fileExt(file: PreviewFile) {
|
||||
const name = String(file.name || file.url || '')
|
||||
const m = name.match(/\.([a-z0-9]+)(?:\?|$)/i)
|
||||
return (m?.[1] || 'FILE').toUpperCase().slice(0, 4)
|
||||
}
|
||||
|
||||
/** 列表内短名:UUID 文件名改为「图片/文件.ext」,避免一长串哈希 */
|
||||
function shortFileLabel(file: PreviewFile) {
|
||||
const raw = String(file.name || '').trim()
|
||||
if (!raw) return isImageFile(file) ? '图片' : '附件'
|
||||
const extMatch = raw.match(/\.([a-z0-9]+)$/i)
|
||||
const ext = extMatch?.[1] || ''
|
||||
const base = ext ? raw.slice(0, -(ext.length + 1)) : raw
|
||||
if (/^[a-f0-9_-]{16,}$/i.test(base)) {
|
||||
if (isImageFile(file)) return ext ? `图片.${ext.toLowerCase()}` : '图片'
|
||||
return ext ? `文件.${ext.toLowerCase()}` : '文件'
|
||||
}
|
||||
if (raw.length <= 14) return raw
|
||||
if (ext) {
|
||||
const keep = Math.max(4, 10 - ext.length)
|
||||
return `${base.slice(0, keep)}….${ext.toLowerCase()}`
|
||||
}
|
||||
return `${raw.slice(0, 12)}…`
|
||||
}
|
||||
|
||||
function imagePreviewList(row: any, label: string) {
|
||||
return previewFiles(row, label)
|
||||
.filter((f) => isImageFile(f))
|
||||
.map((f) => fileFullUrl(f.url))
|
||||
}
|
||||
|
||||
function imagePreviewIndex(row: any, label: string, url: string) {
|
||||
const list = previewFiles(row, label).filter((f) => isImageFile(f))
|
||||
const idx = list.findIndex((f) => f.url === url)
|
||||
return idx >= 0 ? idx : 0
|
||||
}
|
||||
|
||||
function previewFile(file: PreviewFile) {
|
||||
const url = fileFullUrl(file.url)
|
||||
if (url) window.open(url, '_blank')
|
||||
}
|
||||
|
||||
function downloadFile(file: PreviewFile) {
|
||||
void downloadAttachmentFile(
|
||||
{ fileUrl: file.url, fileName: file.name || undefined },
|
||||
{ fullFileUrl: (u) => attachmentFullUrl(u) }
|
||||
)
|
||||
}
|
||||
|
||||
function currentPrinterName() {
|
||||
const info: any = userStore.info || userStore.getUserInfo || {}
|
||||
return String(
|
||||
info.realName || info.nickName || info.userName || info.username || ''
|
||||
).trim()
|
||||
}
|
||||
|
||||
function statusType(s: string) {
|
||||
if (s === 'pending' || s === 'running') return 'warning'
|
||||
if (s === 'approved' || s === 'completed' || s === 'done') return 'success'
|
||||
if (s === 'rejected' || s === 'cancelled' || s === 'returned') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function statusText(s: string) {
|
||||
if (!s) return '-'
|
||||
const map: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
running: '进行中',
|
||||
approved: '已同意',
|
||||
rejected: '已拒绝',
|
||||
completed: '已完成',
|
||||
cancelled: '已撤销',
|
||||
done: '已结束',
|
||||
returned: '已打回',
|
||||
transferred: '已转交'
|
||||
}
|
||||
return map[s] || s
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
searchForm.instanceStatus = ''
|
||||
searchForm.instanceId = ''
|
||||
searchForm.initiatorName = ''
|
||||
applyTimeRange.value = null
|
||||
finishTimeRange.value = null
|
||||
page.value = 1
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
page.value = 1
|
||||
void load()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
resetFilters()
|
||||
void load()
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!appId.value) {
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = {
|
||||
appId: appId.value,
|
||||
instanceId: searchForm.instanceId || undefined,
|
||||
instanceStatus: searchForm.instanceStatus || undefined,
|
||||
applyTimeFrom: applyTimeRange.value?.[0],
|
||||
applyTimeTo: applyTimeRange.value?.[1],
|
||||
finishTimeFrom: finishTimeRange.value?.[0],
|
||||
finishTimeTo: finishTimeRange.value?.[1],
|
||||
page: page.value,
|
||||
pageSize: pageSize.value
|
||||
}
|
||||
if (canViewAll.value && searchForm.initiatorName) {
|
||||
params.initiatorName = searchForm.initiatorName
|
||||
}
|
||||
const data = await fetchOaInstanceHistory(params)
|
||||
rows.value = data?.list || []
|
||||
total.value = Number(data?.total || 0)
|
||||
} catch {
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openDetail(row: any) {
|
||||
const id = row?.id
|
||||
if (!id) return
|
||||
router.push({ path: `/system-manage/oa-approval/instances/${id}` })
|
||||
}
|
||||
|
||||
async function printRow(row: any) {
|
||||
const id = String(row?.id || '').trim()
|
||||
if (!id || printingId.value) return
|
||||
printingId.value = id
|
||||
let detail: any
|
||||
try {
|
||||
detail = await fetchOaInstanceDetail(id)
|
||||
} catch {
|
||||
printingId.value = ''
|
||||
ElMessage.error('加载审批单失败')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await exportOaApprovalPdf(detail, { printerName: currentPrinterName() })
|
||||
ElMessage.success('PDF 已下载')
|
||||
} catch {
|
||||
ElMessage.error('导出 PDF 失败')
|
||||
} finally {
|
||||
printingId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function bootstrapFromRoute() {
|
||||
if (!isActiveHistoryInstance()) return
|
||||
const qName = String(route.query.appName || '').trim()
|
||||
if (qName) boundAppName.value = qName
|
||||
resetFilters()
|
||||
syncWorktabTitle()
|
||||
void load()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [route.name, route.params.appId, route.query.appId, route.query.appName] as const,
|
||||
() => {
|
||||
if (!isActiveHistoryInstance()) return
|
||||
bootstrapFromRoute()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(bootstrapFromRoute)
|
||||
onActivated(() => {
|
||||
if (!isActiveHistoryInstance()) return
|
||||
const qName = String(route.query.appName || '').trim()
|
||||
if (qName) boundAppName.value = qName
|
||||
syncWorktabTitle()
|
||||
void load()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.hist-attach {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
max-width: 100%;
|
||||
padding: 2px 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.hist-attach__card {
|
||||
width: 88px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.15s ease,
|
||||
transform 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #10b981;
|
||||
box-shadow: 0 4px 12px rgb(16 185 129 / 14%);
|
||||
transform: translateY(-1px);
|
||||
|
||||
.hist-attach__hint {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hist-attach__preview {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
}
|
||||
|
||||
.hist-attach__img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
cursor: zoom-in;
|
||||
|
||||
:deep(.el-image__inner) {
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.hist-attach__fallback,
|
||||
.hist-attach__doc {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
padding: 0;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.hist-attach__doc-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: #0284c7;
|
||||
background: #e0f2fe;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.hist-attach__ext {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: #475569;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.hist-attach__hint {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
pointer-events: none;
|
||||
background: rgb(15 23 42 / 38%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.hist-attach__footer {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 5px 6px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.hist-attach__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
color: #334155;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.hist-attach__dl {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
background: #f1f5f9;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background: #10b981;
|
||||
}
|
||||
}
|
||||
|
||||
.hist-attach.is-multi {
|
||||
gap: 6px;
|
||||
|
||||
.hist-attach__card {
|
||||
width: 72px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.hist-attach__preview,
|
||||
.hist-attach__img,
|
||||
.hist-attach__fallback,
|
||||
.hist-attach__doc {
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.hist-attach__img :deep(.el-image__inner) {
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.hist-attach__footer {
|
||||
padding: 4px 5px;
|
||||
}
|
||||
|
||||
.hist-attach__name {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.hist-attach__dl {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -93,33 +93,32 @@
|
||||
@saved="onDialogSaved"
|
||||
@submitted="onDialogSubmitted"
|
||||
/>
|
||||
<OaAppHistoryDialog ref="historyDialogRef" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onActivated, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Clock, Promotion, Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { fetchOaGroupList, fetchOaTemplates } from '@/api/oaApproval'
|
||||
import OaAppIcon from '@/components/oa-approval/OaAppIcon.vue'
|
||||
import OaAppHistoryDialog from '@/components/oa-approval/OaAppHistoryDialog.vue'
|
||||
import OaStartFormDialog from '@/components/oa-approval/OaStartFormDialog.vue'
|
||||
import { useWorktabStore } from '@/store/modules/worktab'
|
||||
import { refreshOaMenuBadges } from '@/utils/oa-menu-badge'
|
||||
|
||||
defineOptions({ name: 'OaApprovalStart' })
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const worktabStore = useWorktabStore()
|
||||
const loading = ref(false)
|
||||
const templates = ref<any[]>([])
|
||||
const groupOptions = ref<Array<{ id: string; groupName: string }>>([])
|
||||
const searchKeyword = ref('')
|
||||
const activeGroupId = ref('')
|
||||
const startDialogRef = ref<InstanceType<typeof OaStartFormDialog> | null>(null)
|
||||
const historyDialogRef = ref<InstanceType<typeof OaAppHistoryDialog> | null>(null)
|
||||
|
||||
const displayGroups = computed(() => {
|
||||
const kw = (searchKeyword.value || '').trim().toLowerCase()
|
||||
@@ -168,7 +167,14 @@
|
||||
}
|
||||
|
||||
function openHistory(row: any) {
|
||||
historyDialogRef.value?.open({ id: row.id, appName: row.appName })
|
||||
const id = String(row?.id || '').trim()
|
||||
if (!id) return
|
||||
router.push({
|
||||
path: `/system-manage/oa-approval/history/${encodeURIComponent(id)}`,
|
||||
query: {
|
||||
appName: String(row?.appName || '').trim()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onDialogSaved() {
|
||||
@@ -182,7 +188,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
function restoreStartTabTitle() {
|
||||
worktabStore.resetTabTitle('/system-manage/oa-approval/start')
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
restoreStartTabTitle()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
restoreStartTabTitle()
|
||||
loading.value = true
|
||||
try {
|
||||
const [tpl, groupsRes] = await Promise.all([fetchOaTemplates(), fetchOaGroupList()])
|
||||
|
||||
@@ -2456,16 +2456,98 @@
|
||||
}
|
||||
}
|
||||
|
||||
function buildBatchStockOutPrefillFromSelection(): {
|
||||
lines: BatchStockOutLine[]
|
||||
options: ProductionMaterialBatchOption[]
|
||||
skippedZero: number
|
||||
} {
|
||||
const sel = selectedRows.value || []
|
||||
const lines: BatchStockOutLine[] = []
|
||||
const options: ProductionMaterialBatchOption[] = []
|
||||
const seen = new Set<string>()
|
||||
let skippedZero = 0
|
||||
for (const row of sel) {
|
||||
const rem = Number(row.remainingQty) || 0
|
||||
if (rem <= 0) {
|
||||
skippedZero += 1
|
||||
continue
|
||||
}
|
||||
const batchId = row.batchId || row.id
|
||||
if (!batchId || seen.has(batchId)) continue
|
||||
seen.add(batchId)
|
||||
const unit = (row.unit || '').trim()
|
||||
const mfr = (row.manufacturer || '').trim()
|
||||
const cat = (row.catalogNo || '').trim()
|
||||
lines.push({
|
||||
batchId,
|
||||
stockInNo: row.stockInNo || '',
|
||||
materialName: row.materialName || '',
|
||||
batchNo: row.batchNo || '',
|
||||
remainingQty: rem,
|
||||
qty: Math.min(1, rem),
|
||||
usageUnit: unit,
|
||||
remark: ''
|
||||
})
|
||||
options.push({
|
||||
id: batchId,
|
||||
batchId,
|
||||
stockInNo: row.stockInNo || '',
|
||||
stockInDate: row.stockInDate,
|
||||
expireDate: row.expireDate,
|
||||
productionDate: row.productionDate,
|
||||
materialId: row.materialId,
|
||||
materialCode: row.materialCode || '',
|
||||
materialName: row.materialName || '',
|
||||
materialType: row.materialType || '',
|
||||
specModel: row.specModel || '',
|
||||
unit,
|
||||
batchNo: row.batchNo || '',
|
||||
catalogNo: row.catalogNo || '',
|
||||
manufacturer: row.manufacturer || '',
|
||||
storageLocation: row.storageLocation,
|
||||
unitPrice: row.unitPrice,
|
||||
amount: row.amount,
|
||||
remainingQty: rem,
|
||||
label: `${row.stockInNo || ''} / ${row.materialName || ''} / 厂家:${mfr || '—'} / 批号:${
|
||||
row.batchNo || '—'
|
||||
} / 货号:${cat || '—'} / 剩:${rem}${unit}`
|
||||
})
|
||||
if (unit) ensureUnitOption(unit)
|
||||
}
|
||||
return { lines, options, skippedZero }
|
||||
}
|
||||
|
||||
function mergeBatchOptions(extra: ProductionMaterialBatchOption[]) {
|
||||
if (!extra.length) return
|
||||
const ids = new Set(batchOptions.value.map((o) => o.id))
|
||||
const missing = extra.filter((o) => o.id && !ids.has(o.id))
|
||||
if (missing.length) {
|
||||
batchOptions.value = [...missing, ...batchOptions.value]
|
||||
}
|
||||
}
|
||||
|
||||
function handleBatchStockOut() {
|
||||
batchStockOutLines.value = [newBatchStockOutLine()]
|
||||
const { lines, options, skippedZero } = buildBatchStockOutPrefillFromSelection()
|
||||
const selectedCount = (selectedRows.value || []).length
|
||||
if (selectedCount > 0 && lines.length === 0) {
|
||||
ElMessage.warning('所选物料剩余数量均为 0,无法出库,请手动选择有库存的批次')
|
||||
} else if (skippedZero > 0) {
|
||||
ElMessage.warning(`已跳过 ${skippedZero} 项剩余数量为 0 的物料`)
|
||||
}
|
||||
batchStockOutLines.value = lines.length ? lines : [newBatchStockOutLine()]
|
||||
batchOptions.value = options
|
||||
batchStockOutForm.stockOutTime = formatNowDatetime()
|
||||
batchStockOutForm.receiver = ''
|
||||
batchStockOutForm.purpose = ''
|
||||
batchStockOutForm.remark = ''
|
||||
batchStockOutVisible.value = true
|
||||
void searchReceiverUsers('')
|
||||
void searchBatchOptions('')
|
||||
void searchUnitOptions('')
|
||||
void searchBatchOptions('').then(() => mergeBatchOptions(options))
|
||||
void searchUnitOptions('').then(() => {
|
||||
for (const line of batchStockOutLines.value) {
|
||||
if (line.usageUnit) ensureUnitOption(line.usageUnit)
|
||||
}
|
||||
})
|
||||
batchStockOutFormRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
|
||||
@@ -2434,16 +2434,95 @@
|
||||
}
|
||||
}
|
||||
|
||||
function buildBatchStockOutPrefillFromSelection(): {
|
||||
lines: BatchStockOutLine[]
|
||||
options: QcMaterialBatchOption[]
|
||||
skippedZero: number
|
||||
} {
|
||||
const sel = selectedRows.value || []
|
||||
const lines: BatchStockOutLine[] = []
|
||||
const options: QcMaterialBatchOption[] = []
|
||||
const seen = new Set<string>()
|
||||
let skippedZero = 0
|
||||
for (const row of sel) {
|
||||
const rem = Number(row.remainingQty) || 0
|
||||
if (rem <= 0) {
|
||||
skippedZero += 1
|
||||
continue
|
||||
}
|
||||
const batchId = row.batchId || row.id
|
||||
if (!batchId || seen.has(batchId)) continue
|
||||
seen.add(batchId)
|
||||
const unit = (row.unit || '').trim()
|
||||
const mfr = (row.manufacturer || '').trim()
|
||||
const cat = (row.catalogNo || '').trim()
|
||||
lines.push({
|
||||
batchId,
|
||||
stockInNo: row.stockInNo || '',
|
||||
materialName: row.materialName || '',
|
||||
batchNo: row.batchNo || '',
|
||||
remainingQty: rem,
|
||||
qty: Math.min(1, rem),
|
||||
usageUnit: unit,
|
||||
remark: ''
|
||||
})
|
||||
options.push({
|
||||
id: batchId,
|
||||
batchId,
|
||||
stockInNo: row.stockInNo || '',
|
||||
stockInDate: row.stockInDate,
|
||||
expireDate: row.expireDate,
|
||||
materialId: row.materialId,
|
||||
materialCode: row.materialCode || '',
|
||||
materialName: row.materialName || '',
|
||||
specModel: row.specModel || '',
|
||||
unit,
|
||||
batchNo: row.batchNo || '',
|
||||
catalogNo: row.catalogNo || '',
|
||||
manufacturer: row.manufacturer || '',
|
||||
unitPrice: row.unitPrice,
|
||||
amount: row.amount,
|
||||
remainingQty: rem,
|
||||
label: `${row.stockInNo || ''} / ${row.materialName || ''} / 厂家:${mfr || '—'} / 批号:${
|
||||
row.batchNo || '—'
|
||||
} / 货号:${cat || '—'} / 剩:${rem}${unit}`
|
||||
})
|
||||
if (unit) ensureUnitOption(unit)
|
||||
}
|
||||
return { lines, options, skippedZero }
|
||||
}
|
||||
|
||||
function mergeBatchOptions(extra: QcMaterialBatchOption[]) {
|
||||
if (!extra.length) return
|
||||
const ids = new Set(batchOptions.value.map((o) => o.id))
|
||||
const missing = extra.filter((o) => o.id && !ids.has(o.id))
|
||||
if (missing.length) {
|
||||
batchOptions.value = [...missing, ...batchOptions.value]
|
||||
}
|
||||
}
|
||||
|
||||
function handleBatchStockOut() {
|
||||
batchStockOutLines.value = [newBatchStockOutLine()]
|
||||
const { lines, options, skippedZero } = buildBatchStockOutPrefillFromSelection()
|
||||
const selectedCount = (selectedRows.value || []).length
|
||||
if (selectedCount > 0 && lines.length === 0) {
|
||||
ElMessage.warning('所选物料剩余数量均为 0,无法出库,请手动选择有库存的批次')
|
||||
} else if (skippedZero > 0) {
|
||||
ElMessage.warning(`已跳过 ${skippedZero} 项剩余数量为 0 的物料`)
|
||||
}
|
||||
batchStockOutLines.value = lines.length ? lines : [newBatchStockOutLine()]
|
||||
batchOptions.value = options
|
||||
batchStockOutForm.stockOutTime = formatNowDatetime()
|
||||
batchStockOutForm.receiver = ''
|
||||
batchStockOutForm.purpose = ''
|
||||
batchStockOutForm.remark = ''
|
||||
batchStockOutVisible.value = true
|
||||
void searchReceiverUsers('')
|
||||
void searchBatchOptions('')
|
||||
void searchUnitOptions('')
|
||||
void searchBatchOptions('').then(() => mergeBatchOptions(options))
|
||||
void searchUnitOptions('').then(() => {
|
||||
for (const line of batchStockOutLines.value) {
|
||||
if (line.usageUnit) ensureUnitOption(line.usageUnit)
|
||||
}
|
||||
})
|
||||
batchStockOutFormRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user