mes-vue/src/views/biz/concessionApply/index.vue
shaoleiliu-netizen123 f67a24d8b3 让步接收触发审批推送钉钉
让步接收申请单前端页面
2026-06-06 16:44:34 +08:00

609 lines
21 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page-container">
<n-card>
<!-- 搜索表单 -->
<div class="search-form">
<n-form inline :model="searchForm" label-placement="left">
<n-form-item label="主键id">
<n-input v-model:value="searchForm.id" placeholder="请输入主键id" clearable />
</n-form-item>
<n-form-item>
<n-space>
<n-button type="primary" @click="handleSearch">
<template #icon><n-icon><SearchOutline /></n-icon></template>
搜索
</n-button>
<n-button @click="handleReset">
<template #icon><n-icon><RefreshOutline /></n-icon></template>
重置
</n-button>
</n-space>
</n-form-item>
</n-form>
</div>
<!-- 工具栏 -->
<div class="table-toolbar">
<n-space>
<n-button type="primary" @click="handleAdd">
<template #icon><n-icon><AddOutline /></n-icon></template>
新增
</n-button>
<n-button @click="importModalVisible = true">
<template #icon><n-icon><CloudUploadOutline /></n-icon></template>
导入
</n-button>
<n-button @click="handleExport">
<template #icon><n-icon><DownloadOutline /></n-icon></template>
导出{{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }}
</n-button>
<n-button type="error" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
<template #icon><n-icon><TrashOutline /></n-icon></template>
删除
</n-button>
</n-space>
</div>
<!-- 表格 -->
<n-data-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
:row-key="(row) => row.id"
:scroll-x="1200"
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
@update:checked-row-keys="handleCheck"
/>
</n-card>
<!-- 新增/编辑弹窗 -->
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 600px">
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="100px">
<n-form-item label="质检编号" path="qcId">
<n-input v-model:value="formData.qcId" placeholder="请输入质检编号" />
</n-form-item>
<n-form-item label="让步接收数量" path="concessionQty">
<n-input v-model:value="formData.concessionQty" placeholder="请输入让步接收数量" />
</n-form-item>
<n-form-item label="申请原因" path="applyRemark">
<n-input v-model:value="formData.applyRemark" type="textarea" placeholder="请输入申请原因" />
</n-form-item>
<n-form-item label="申请内容" path="applyContent">
<n-input v-model:value="formData.applyContent" type="textarea" placeholder="请输入申请内容" />
</n-form-item>
<n-form-item label="申请人id(发去人id)" path="applyUserId">
<n-input v-model:value="formData.applyUserId" placeholder="请输入申请人id(发去人id)" />
</n-form-item>
<n-form-item label="部门id" path="deptId">
<n-input v-model:value="formData.deptId" placeholder="请输入部门id" />
</n-form-item>
<n-form-item label="钉钉单据id" path="instanceId">
<n-input v-model:value="formData.instanceId" placeholder="请输入钉钉单据id" />
</n-form-item>
<n-form-item label="审批状态 1待审批 2审批通过 3 审批驳回" path="approveStatus">
<n-select v-model:value="formData.approveStatus" placeholder="请选择审批状态 1待审批 2审批通过 3 审批驳回" :options="approveStatusOptions" />
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="modalVisible = false">取消</n-button>
<n-button type="primary" @click="handleSubmit">确定</n-button>
</n-space>
</template>
</n-modal>
<!-- 导入弹窗 -->
<n-modal v-model:show="importModalVisible" preset="card" title="导入让步接收申请表" style="width: 500px">
<n-space vertical>
<n-alert type="info">
<template #header>导入说明</template>
<ul style="margin: 0; padding-left: 16px; line-height: 1.8">
<li>请先下载导入模板,按模板格式填写数据</li>
<li>支持 .xlsx 或 .xls 格式</li>
</ul>
</n-alert>
<n-space>
<n-button type="primary" @click="handleDownloadTemplate">
<template #icon><n-icon><DownloadOutline /></n-icon></template>
下载模板
</n-button>
</n-space>
<n-upload :max="1" accept=".xlsx,.xls" :show-file-list="true" :custom-request="handleImportUpload">
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" :depth="3"><CloudUploadOutline /></n-icon>
</div>
<n-text style="font-size: 16px">点击或拖拽文件到此处上传</n-text>
<n-p depth="3" style="margin: 8px 0 0 0">支持 .xlsx 或 .xls 格式</n-p>
</n-upload-dragger>
</n-upload>
</n-space>
<template #footer>
<n-button @click="importModalVisible = false">关闭</n-button>
</template>
</n-modal>
<!-- 审批纪律弹窗 -->
<n-modal v-model:show="recordModalVisible" preset="card" title="审批记录" style="width: 800px">
<n-card bordered style="width:100%;max-width:800px;padding:24px;position:relative;">
<div class="header-bar" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<span style="font-size:20sp;color:#666">审批编号: {{ detailModel?.businessId }}</span>
<div>
<n-icon style="font-size:22px;margin:0 6px;cursor:pointer" ><DownloadOutline /></n-icon>
</div>
</div>
<div style="padding:10px 0;">
<div style="font-size:30px;font-weight:500;margin-bottom:12px;">{{ detailModel?.title }}</div>
<div style="font-size:36sp;color:#777;margin-bottom:12px;">{{ detailModel?.deptName }}</div>
<div :style="{fontSize:'30sp', color:getApproveResultColor(detailModel?.approveResult)}">{{ detailModel?.approveResult }}</div>
</div>
<div class="seal-wrap" style="position:absolute;right:40px;top:50%;transform:translateY(-50%);">
<div v-if="detailModel?.approveResult==='已通过'" class="seal" :class="{'pass-seal':detailModel?.approveResult==='已通过'}">已通过</div>
<div v-if="detailModel?.approveResult==='已驳回'" class="seal" :class="{'pass-seal_reject':detailModel?.approveResult==='已驳回'}">已驳回</div>
<div v-if="detailModel?.approveResult==='已撤销'" class="seal" :class="{'pass-seal_cancel':detailModel?.approveResult==='已撤销'}">已撤销</div>
</div>
</n-card>
<n-card bordered style="width:100%;max-width:800px;padding:24px;margin-top: 10px;position:relative;">
<div style="padding:10px;">
<!-- 循环审批记录 -->
<div
v-for="(item,index) in detailModel?.approveRecord"
:key="index"
style="display:flex;gap:12px;position:relative;padding-bottom:24px;"
>
<!-- 左侧:圆点 + 竖连接线 -->
<div style="display:flex;flex-direction:column;align-items:center;">
<!-- 圆点图标 -->
<div style="width:20px;height:20px;border-radius:50%;background:#36d399;display:flex;align-items:center;justify-content:center;">
<!-- 可替换CashIcon图标 -->
<span style="color:#fff;font-size:12px;">$</span>
</div>
<!-- 中间竖线:最后一条去掉竖线 -->
<div v-if="index !== detailModel.approveRecord.length -1" style="width:2px;flex:1;background:#d0d5dd;margin-top:4px;"></div>
</div>
<!-- 右侧:标题 + 时间 + 备注 -->
<div style="flex:1;">
<!-- 用户名+操作结果 -->
<div style="font-size:18px;margin-bottom:4px;">
{{ item.username ?? '未知人员' }}
<span :style="{
color: item.operationResult === '已同意' ? '#00b42a' : item.operationResult === '已拒绝' ? '#f53f3f' : item.operationResult === '已撤销' ? '#ff7d00' : '#666',
marginLeft:'8px'
}">
{{ item.operationResult }}
</span>
</div>
<!-- 时间 -->
<div style="color:#888;font-size:14px;margin-bottom:6px;">{{ item.date }}</div>
<!-- 审批备注 -->
<div v-if="item.remark" style="padding:6px 10px;background:#f7f8fa;border-radius:6px;color:#333;">
{{ item.remark }}
</div>
<div v-if="item.attachments && item.attachments.length" style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px;">
<!-- 图片类附件png/jpg/jpeg/gif -->
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px;">
<div
v-for="file in item.attachments.filter(f=>['png','jpg','jpeg','gif'].includes(f.file_type.toLowerCase()))"
:key="file.file_id"
@click="previewImg(file)"
style="width:80px;height:80px;border-radius:6px;overflow:hidden;border:1px solid #eee;cursor:pointer;"
>
</div>
</div>
</div>
</div>
</div>
</div>
</n-card>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, h, onMounted } from 'vue'
import { NButton, NSpace, NIcon, NUpload, useMessage,NTag, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline,DocumentTextOutline,
CashOutline as CashIcon
} from '@vicons/ionicons5'
import { concessionApplyApi, DetailModel, type ConcessionApply } from '@/api/concessionApply'
import { dictDataApi } from '@/api/org'
const message = useMessage()
const dialog = useDialog()
// 搜索表单
const searchForm = reactive({
id: null as number | null,
})
// 表格数据
const tableData = ref<ConcessionApply[]>([])
const loading = ref(false)
const selectedIds = ref<number[]>([])
const pagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0,
showSizePicker: true,
pageSizes: [10, 20, 50]
})
// 弹窗
const modalVisible = ref(false)
const modalTitle = ref('')
const importModalVisible = ref(false)
const formRef = ref()
const defaultFormData: ConcessionApply = {
qcId: undefined,
concessionQty: undefined,
applyRemark: '',
applyContent: '',
applyFile: '',
applyUserId: undefined,
deptId: undefined,
instanceId: '',
approveStatus: undefined,
}
const formData = reactive<ConcessionApply>({ ...defaultFormData })
//审批记录弹窗
const recordModalVisible = ref(false)
const detailModel = ref<DetailModel>()
// 字典选项(下拉框/单选框/复选框关联字典时使用)
const approveStatusOptions = ref<{ label: string; value: any }[]>([])
// 表单校验规则
const formRules = {
}
// 表格列
const columns: DataTableColumns<ConcessionApply> = [
{ type: 'selection' },
{ title: '物料编号', key: 'materialCode',align:"center",minWidth:"150"},
{ title: '物料名称', key: 'materialName',align:"center",minWidth:"150"},
{ title: '让步接收数量', key: 'concessionQty',align:"center",minWidth:"150" },
{ title: '申请原因', key: 'applyRemark',align:"center",minWidth:"150" },
{ title: '申请内容', key: 'applyContent',align:"center",minWidth:"200" },
{ title: '申请附件', key: 'applyFile',align:"center",minWidth:"150" },
{ title: '申请人', key: 'applyUserName',align:"center",minWidth:"150" },
{ title: '部门名称', key: 'deptName',align:"center",minWidth:"150" },
// { title: '钉钉单据编号', key: 'instanceId',align:"center",minWidth:"150" },
{ title: '审批完成时间', key: 'approveTime',align:"center",minWidth:"250" },
{ title: '审批状态', key: 'approveStatus',align:"center",minWidth:"150" },
{ title: '审批结果', key: 'approveResult',align:"center",minWidth:"150",
render(row:any){
console.log(row.approveResult);
if(row.approveResult == '待审批') return h(NTag, { type: 'default', size: 'small' }, { default: () => row.approveResult })
if(row.approveResult == '已驳回') return h(NTag, { type: 'error', size: 'small' }, { default: () => row.approveResult })
if(row.approveResult == '已通过') return h(NTag, { type: 'success', size: 'small' }, { default: () => row.approveResult })
return row.approveResult
}
},
{ title: '审批人', key: 'approveUserName',align:"center",minWidth:"150" },
{ title: '创建时间', key: 'createTime', width: 180 },
{ title: '更新时间', key: 'updateTime', width: 180 },
{
title: '操作',
key: 'actions',
width: 140,
fixed: 'right',
render(row) {
return h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'nowrap' } }, [
h(NButton, { size: 'small', quaternary: true, onClick: () => handleEdit(row) }, {
default: () => [h(NIcon, null, { default: () => h(CreateOutline) }), ' 编辑']
}),
h(NButton, { size: 'small', quaternary: true, type: 'error', onClick: () => handleDelete(row) }, {
default: () => [h(NIcon, null, { default: () => h(TrashOutline) }), ' 删除']
}),
h(NButton, { size: 'small', quaternary: true, type: 'warring', onClick: () => handleViewApproveRecord(row) }, {
default: () => [h(NIcon, null, { default: () => h(DocumentTextOutline) }), ' 审批记录']
})
])
}
}
]
// 加载数据
async function loadData() {
loading.value = true
try {
const res = await concessionApplyApi.page({
page: pagination.page,
pageSize: pagination.pageSize,
id: searchForm.id || undefined,
})
tableData.value = res.list
pagination.itemCount = res.total
} finally {
loading.value = false
}
}
// 搜索
function handleSearch() {
pagination.page = 1
loadData()
}
// 重置
function handleReset() {
searchForm.id = null
handleSearch()
}
// 分页
function handlePageChange(page: number) {
pagination.page = page
loadData()
}
function handlePageSizeChange(pageSize: number) {
pagination.pageSize = pageSize
pagination.page = 1
loadData()
}
// 选择
function handleCheck(keys: Array<string | number>) {
selectedIds.value = keys as number[]
}
// 新增
function handleAdd() {
modalTitle.value = '新增让步接收申请表'
Object.assign(formData, defaultFormData)
modalVisible.value = true
}
// 编辑
function handleEdit(row: ConcessionApply) {
modalTitle.value = '编辑让步接收申请表'
Object.assign(formData, row)
if (formData.createTime && typeof formData.createTime === 'string') {
formData.createTime = new Date(formData.createTime.replace(' ', 'T')).getTime()
}
if (formData.updateTime && typeof formData.updateTime === 'string') {
formData.updateTime = new Date(formData.updateTime.replace(' ', 'T')).getTime()
}
modalVisible.value = true
}
// 提交
async function handleSubmit() {
await formRef.value?.validate()
try {
const submitData = { ...formData } as ConcessionApply
if (typeof submitData.createTime === 'number') {
submitData.createTime = new Date(submitData.createTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (typeof submitData.updateTime === 'number') {
submitData.updateTime = new Date(submitData.updateTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (submitData.id) {
await concessionApplyApi.update(submitData)
message.success('修改成功')
} else {
await concessionApplyApi.create(submitData)
message.success('新增成功')
}
modalVisible.value = false
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
// 删除
function handleDelete(row: ConcessionApply) {
dialog.warning({
title: '提示',
content: '确定要删除该记录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await concessionApplyApi.delete([row.id!])
message.success('删除成功')
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
})
}
//查看审批记录
function handleViewApproveRecord(row:any) {
recordModalVisible.value = true
detailModel.value = {...row}
console.log(detailModel.value);
}
// 批量删除
function handleBatchDelete() {
dialog.warning({
title: '提示',
content: `确定要删除选中的 ${selectedIds.value.length} 条记录吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await concessionApplyApi.delete(selectedIds.value)
message.success('删除成功')
selectedIds.value = []
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
})
}
// 导出
async function handleExport() {
try {
const params: Record<string, any> = {}
if (selectedIds.value.length > 0) params.ids = selectedIds.value
if (searchForm.id != null) params.id = searchForm.id
const blob = await concessionApplyApi.export(params)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '让步接收申请表数据.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
// 错误已在拦截器处理
}
}
// 下载导入模板
async function handleDownloadTemplate() {
try {
const blob = await concessionApplyApi.downloadTemplate()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '让步接收申请表导入模板.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
// 错误已在拦截器处理
}
}
// 导入上传
async function handleImportUpload({ file }: UploadCustomRequestOptions) {
if (!file.file) return
try {
const result = await concessionApplyApi.importData(file.file)
if (result.fail > 0) {
dialog.warning({
title: '导入结果',
content: `成功: ${result.success} 条,失败: ${result.fail} 条\n错误信息: ${(result.errors || []).join('\n') || '无'}`,
positiveText: '确定'
})
} else {
message.success(`导入成功,共 ${result.success} 条数据`)
importModalVisible.value = false
}
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
// 加载字典选项
async function loadDictOptions() {
try {
const data = await dictDataApi.listByType('assets_state')
approveStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class: d.listClass }))
} catch {}
}
const getTimelineType = (type) => {
console.log(type);
const map = {
'START_PROCESS_INSTANCE': 'info', //发起-蓝色
'AGREE': 'success',//通过-绿色
'REFUSE': 'error',//驳回-红色
'TERMINATE_PROCESS_INSTANCE':'warning',//撤回-橙黄
'grey':'default', //兼容grey
'info':'default'
}
return map[type] || 'default'
}
const getTitleAttribute = (item) =>{
return item.username +" "+item.operationResult
}
function getApproveResultColor(status) {
switch(status){
case '已通过':
return "green"
case '已驳回':
return "red"
case '已撤销':
return "gray"
}
}
const getRemark=(item) =>{
console.log(item.remark);
}
onMounted(() => {
loadData()
loadDictOptions()
})
</script>
<style scoped>
.search-form {
margin-bottom: 16px;
}
.table-toolbar {
margin-bottom: 16px;
}
.pass-seal{
width:150px;
height:150px;
border:4px solid #36c972;
border-radius:50%;
color:#36c972;
font-size:40px;
font-weight:bold;
display:flex;
align-items:center;
justify-content:center;
transform:rotate(25deg);
}
.pass-seal_reject{
width:150px;
height:150px;
border:4px solid #da0d0d;
border-radius:50%;
color: #da0d0d;
font-size:40px;
font-weight:bold;
display:flex;
align-items:center;
justify-content:center;
transform:rotate(25deg);
}
.pass-seal_cancel{
width:150px;
height:150px;
border:4px solid #3a3a3a;
border-radius:50%;
color:#3a3a3a;
font-size:40px;
font-weight:bold;
display:flex;
align-items:center;
justify-content:center;
transform:rotate(25deg);
}
</style>