Merge branch 'main' of http://49.232.154.205:10100/DeXun/fad_oa
This commit is contained in:
43
ruoyi-ui/src/api/oa/approval.js
Normal file
43
ruoyi-ui/src/api/oa/approval.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// ===== 审批配置 =====
|
||||
export function listApprovalConfig() {
|
||||
return request({ url: '/oa/approval/config/list', method: 'get' })
|
||||
}
|
||||
export function getApprovalConfig(businessType) {
|
||||
return request({ url: '/oa/approval/config/' + businessType, method: 'get' })
|
||||
}
|
||||
export function saveApprovalConfig(data) {
|
||||
return request({ url: '/oa/approval/config', method: 'post', data })
|
||||
}
|
||||
export function delApprovalConfig(id) {
|
||||
return request({ url: '/oa/approval/config/' + id, method: 'delete' })
|
||||
}
|
||||
|
||||
// ===== 我的审批 =====
|
||||
export function listMyPending(query) {
|
||||
return request({ url: '/oa/approval/mine/pending', method: 'get', params: query })
|
||||
}
|
||||
export function listMyDone(query) {
|
||||
return request({ url: '/oa/approval/mine/done', method: 'get', params: query })
|
||||
}
|
||||
export function listMySubmitted(query) {
|
||||
return request({ url: '/oa/approval/mine/submitted', method: 'get', params: query })
|
||||
}
|
||||
export function listAllApproval(query) {
|
||||
return request({ url: '/oa/approval/list', method: 'get', params: query })
|
||||
}
|
||||
|
||||
// ===== 操作 =====
|
||||
export function actApproval(data) {
|
||||
return request({ url: '/oa/approval/act', method: 'post', data })
|
||||
}
|
||||
export function withdrawApproval(instanceId) {
|
||||
return request({ url: '/oa/approval/withdraw/' + instanceId, method: 'post' })
|
||||
}
|
||||
export function getApprovalDetail(instanceId) {
|
||||
return request({ url: '/oa/approval/detail/' + instanceId, method: 'get' })
|
||||
}
|
||||
export function getLatestApproval(businessType, businessId) {
|
||||
return request({ url: '/oa/approval/latest', method: 'get', params: { businessType, businessId } })
|
||||
}
|
||||
@@ -3,8 +3,8 @@
|
||||
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container"
|
||||
@toggleClick="toggleSideBar" />
|
||||
|
||||
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav" />
|
||||
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav" />
|
||||
<sub-menu-bar v-if="!topNav" />
|
||||
|
||||
<div class="right-menu">
|
||||
<template v-if="device !== 'mobile'">
|
||||
@@ -51,6 +51,7 @@ import RuoYiGit from "@/components/RuoYi/Git";
|
||||
import Screenfull from "@/components/Screenfull";
|
||||
import SizeSelect from "@/components/SizeSelect";
|
||||
import TopNav from "@/components/TopNav";
|
||||
import SubMenuBar from "@/layout/components/SubMenuBar/index.vue";
|
||||
import AIChat from "@/layout/components/AIChat/index.vue";
|
||||
import FeedbackEntry from "@/layout/components/FeedbackEntry.vue";
|
||||
import { parseTime } from "@/utils/ruoyi";
|
||||
@@ -65,6 +66,7 @@ export default {
|
||||
// ChatComponent,
|
||||
Breadcrumb,
|
||||
TopNav,
|
||||
SubMenuBar,
|
||||
Hamburger,
|
||||
Screenfull,
|
||||
SizeSelect,
|
||||
|
||||
95
ruoyi-ui/src/layout/components/SubMenuBar/index.vue
Normal file
95
ruoyi-ui/src/layout/components/SubMenuBar/index.vue
Normal file
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="sub-menu-bar" v-if="items.length">
|
||||
<router-link
|
||||
v-for="item in items"
|
||||
:key="item.path"
|
||||
:to="item.path"
|
||||
class="sub-menu-item"
|
||||
:class="{ active: isActive(item) }"
|
||||
>{{ item.title }}</router-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
|
||||
export default {
|
||||
name: "SubMenuBar",
|
||||
computed: {
|
||||
...mapGetters(["sidebarRouters"]),
|
||||
activeTopPath () {
|
||||
const path = this.$route.path || "";
|
||||
if (!path || path === "/") return "";
|
||||
const seg = path.split("/").filter(Boolean)[0];
|
||||
return seg ? "/" + seg : "";
|
||||
},
|
||||
items () {
|
||||
const top = this.findTop(this.sidebarRouters, this.activeTopPath);
|
||||
if (!top) return [];
|
||||
const children = (top.children || []).filter(c => !c.hidden);
|
||||
return children.map(c => ({
|
||||
path: this.resolvePath(top.path, c.path),
|
||||
title: c.meta?.title
|
||||
})).filter(it => it.title);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
findTop (routes, activePath) {
|
||||
if (!routes || !activePath) return null;
|
||||
for (const r of routes) {
|
||||
if (r.hidden) continue;
|
||||
const rp = r.path?.startsWith("/") ? r.path : "/" + r.path;
|
||||
if (rp === activePath) return r;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
resolvePath (parent, child) {
|
||||
if (!child) return parent;
|
||||
if (/^https?:\/\//.test(child)) return child;
|
||||
if (child.startsWith("/")) return child;
|
||||
return (parent || "").replace(/\/+$/, "") + "/" + child;
|
||||
},
|
||||
isActive (item) {
|
||||
return this.$route.path === item.path || this.$route.path.startsWith(item.path + "/");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sub-menu-bar {
|
||||
float: left;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding-left: 12px;
|
||||
overflow-x: auto;
|
||||
max-width: calc(100% - 360px);
|
||||
}
|
||||
|
||||
.sub-menu-bar::-webkit-scrollbar {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.sub-menu-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
color: #5a5e66;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
.sub-menu-item:hover {
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.sub-menu-item.active {
|
||||
color: #409eff;
|
||||
border-bottom-color: #409eff;
|
||||
}
|
||||
</style>
|
||||
@@ -4,13 +4,13 @@
|
||||
<div slot="header" class="card-header">
|
||||
<span>{{ bizTitle }}</span>
|
||||
<div class="actions">
|
||||
<el-button v-if="!preview" size="mini" icon="el-icon-arrow-left" @click="$router.back()">返回</el-button>
|
||||
<el-button v-if="!isPreview" size="mini" icon="el-icon-arrow-left" @click="$router.back()">返回</el-button>
|
||||
<el-button size="mini" icon="el-icon-refresh" @click="loadDetail">刷新</el-button>
|
||||
|
||||
<el-button v-if="!preview && canApprove" type="success" size="mini" :loading="actionLoading" @click="handleApprove">
|
||||
<el-button v-if="!isPreview && canApprove" type="success" size="mini" :loading="actionLoading" @click="handleApprove">
|
||||
通过
|
||||
</el-button>
|
||||
<el-button v-if="!preview && canApprove" type="danger" size="mini" :loading="actionLoading" @click="handleReject">
|
||||
<el-button v-if="!isPreview && canApprove" type="danger" size="mini" :loading="actionLoading" @click="handleReject">
|
||||
驳回
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
<ProjectInfo :info="detail" />
|
||||
</el-card>
|
||||
|
||||
<div v-if="!preview">
|
||||
<div v-if="!isPreview">
|
||||
<div class="block-title">审批操作</div>
|
||||
<el-card class="inner-card" shadow="never">
|
||||
<div v-if="currentTask" class="btn-row">
|
||||
@@ -75,7 +75,7 @@
|
||||
<span>操作汇报</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!preview" class="comment-form">
|
||||
<div v-if="!isPreview" class="comment-form">
|
||||
<editor v-model="commentForm.commentContent" placeholder="填写操作汇报(可选)" />
|
||||
<file-upload v-model="commentForm.attachments" />
|
||||
<div class="form-actions">
|
||||
@@ -89,7 +89,7 @@
|
||||
<div class="comment-meta">
|
||||
<span class="comment-operator">{{ item.createByName }}</span>
|
||||
<span class="comment-time">{{ item.createTime }}</span>
|
||||
<el-button v-if="!preview && isSelf(item)" type="danger" size="mini" @click="handleDeleteComment(item.commentId)"
|
||||
<el-button v-if="!isPreview && isSelf(item)" type="danger" size="mini" @click="handleDeleteComment(item.commentId)"
|
||||
:loading="buttonLoading">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,6 +160,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isPreview () {
|
||||
return this.preview || !!this.$route?.query?.preview
|
||||
},
|
||||
currentBizId () {
|
||||
return this.bizId
|
||||
},
|
||||
|
||||
@@ -46,7 +46,7 @@ export default {
|
||||
if (routePath) {
|
||||
this.$router.push({
|
||||
path: routePath,
|
||||
query: { bizId: bizId }
|
||||
query: { bizId: bizId, preview: 1 }
|
||||
})
|
||||
} else {
|
||||
this.$message.warning('无法确定申请类型对应的详情页面')
|
||||
|
||||
@@ -138,7 +138,28 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
getRowTitle (row) {
|
||||
return row.title || row.remark || '-'
|
||||
const t = (row.title || row.remark || '').trim()
|
||||
if (t) return t
|
||||
return this.summarizeBiz(row) || '-'
|
||||
},
|
||||
summarizeBiz (row) {
|
||||
const b = row.bizData || {}
|
||||
switch (row.bizType) {
|
||||
case 'leave': {
|
||||
const dur = b.hours ? `${b.hours}h` : ''
|
||||
return [b.leaveType || '请假', dur, b.reason].filter(Boolean).join(' · ')
|
||||
}
|
||||
case 'travel':
|
||||
return [b.travelType || '出差', b.destination, b.reason].filter(Boolean).join(' · ')
|
||||
case 'seal':
|
||||
return [b.sealType || '用印', b.purpose].filter(Boolean).join(' · ')
|
||||
case 'reimburse':
|
||||
return [b.reimburseType || '报销', b.totalAmount != null ? '¥' + b.totalAmount : null, b.reason].filter(Boolean).join(' · ')
|
||||
case 'appropriation':
|
||||
return [b.appropriationType || '拨款', b.amount != null ? '¥' + b.amount : null, b.reason].filter(Boolean).join(' · ')
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
},
|
||||
isTravelCompleted (row) {
|
||||
if (!row || row.bizType !== 'travel') return false
|
||||
|
||||
@@ -50,7 +50,6 @@
|
||||
stripe
|
||||
border
|
||||
@row-dblclick="handleRowClick">
|
||||
<el-table-column label="编号" prop="instId" width="80" />
|
||||
<el-table-column label="类型" width="70">
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="getTypeTagType(scope.row.bizType)" size="mini">{{ getTypeText(scope.row.bizType) }}</el-tag>
|
||||
@@ -80,6 +79,9 @@
|
||||
<el-table-column label="申请时间" prop="createTime" width="140">
|
||||
<template slot-scope="scope">{{ formatDate(scope.row.createTime) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结束时间" width="140">
|
||||
<template slot-scope="scope">{{ formatFinishTime(scope.row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="耗时" width="80">
|
||||
<template slot-scope="scope">{{ formatElapsed(scope.row) }}</template>
|
||||
</el-table-column>
|
||||
@@ -174,7 +176,7 @@ export default {
|
||||
case 'travel':
|
||||
return `${b.travelType || '出差'} · ${b.destination || '未填目的地'}${b.startTime ? ' · ' + this.formatDate(b.startTime) : ''}`
|
||||
case 'seal':
|
||||
return `${b.sealType || '用印'}${b.fileName ? ' · ' + b.fileName : ''}${b.useReason ? ' · ' + b.useReason : ''}`
|
||||
return `${b.sealType || '用印'}${b.purpose ? ' · ' + b.purpose : ''}`
|
||||
case 'reimburse':
|
||||
return `${b.reimburseType || '报销'} · ¥${b.totalAmount != null ? b.totalAmount : 0}${b.reason ? ' · ' + b.reason : ''}`
|
||||
case 'appropriation':
|
||||
@@ -192,9 +194,19 @@ export default {
|
||||
}
|
||||
return '-'
|
||||
},
|
||||
flowEndTime (row) {
|
||||
const finalStatus = ['approved', 'rejected', 'revoked', 'finished', 'complete']
|
||||
if (row.bizType === 'travel' && row.actualEndTime) return row.actualEndTime
|
||||
if (finalStatus.includes(row.status)) return row.updateTime || null
|
||||
return null
|
||||
},
|
||||
formatFinishTime (row) {
|
||||
const end = this.flowEndTime(row)
|
||||
return end ? this.formatDate(end) : '-'
|
||||
},
|
||||
formatElapsed (row) {
|
||||
if (!row.createTime) return '-'
|
||||
const end = row.finishTime || row.endTime || Date.now()
|
||||
const end = this.flowEndTime(row) || Date.now()
|
||||
const ms = new Date(end).getTime() - new Date(row.createTime).getTime()
|
||||
if (ms <= 0) return '-'
|
||||
const h = ms / 3600000
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
</el-card>
|
||||
|
||||
<!-- 盖章操作(审批通过后显示) -->
|
||||
<div v-if="canStamp" class="block-title">盖章操作</div>
|
||||
<el-card v-if="canStamp" class="inner-card" shadow="never">
|
||||
<div v-if="canStamp && !$route.query.preview" class="block-title">盖章操作</div>
|
||||
<el-card v-if="canStamp && !$route.query.preview" class="inner-card" shadow="never">
|
||||
<div class="stamp-section">
|
||||
<div class="stamp-config">
|
||||
<el-form :model="stampForm" label-width="120px" size="small">
|
||||
|
||||
@@ -30,6 +30,23 @@
|
||||
: '评估候选人,分析与目标岗位的匹配度、优势、短板与面试建议。' }}
|
||||
支持 PDF / Word(.doc/.docx),≤ 20MB。
|
||||
</div>
|
||||
|
||||
<!-- 审核重点 / 附加要求 -->
|
||||
<div class="req-area">
|
||||
<div class="req-line">
|
||||
<span class="req-label">审核重点</span>
|
||||
<el-checkbox-group v-model="checkedItems" size="mini" :disabled="streaming" class="req-items">
|
||||
<el-checkbox v-for="it in itemOptions" :key="it.value" :label="it.label" border>{{ it.label }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<span class="req-tip">可在「系统管理→字典管理」增删审核项</span>
|
||||
</div>
|
||||
<div class="req-line">
|
||||
<span class="req-label">附加要求</span>
|
||||
<el-input v-model="extraText" type="textarea" :rows="2" :disabled="streaming"
|
||||
class="req-extra" maxlength="500" show-word-limit
|
||||
placeholder="可补充本次审核的特殊关注点,例如:重点核查付款比例是否对我方有利、是否有自动续约陷阱…" />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="12" class="body">
|
||||
@@ -96,6 +113,7 @@
|
||||
|
||||
<script>
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { getDicts } from '@/api/system/dict/data'
|
||||
const marked = require('marked')
|
||||
|
||||
export default {
|
||||
@@ -107,6 +125,11 @@ export default {
|
||||
rawFile: null,
|
||||
fileName: '',
|
||||
|
||||
// 审核重点(字典) + 附加要求(自由文本)
|
||||
itemDicts: { contract: [], resume: [] },
|
||||
checkedItems: [],
|
||||
extraText: '',
|
||||
|
||||
streaming: false,
|
||||
done: false,
|
||||
reasoning: '',
|
||||
@@ -124,15 +147,37 @@ export default {
|
||||
computed: {
|
||||
renderedMd () {
|
||||
try { return marked(this.content) } catch (e) { return this.content }
|
||||
},
|
||||
itemOptions () {
|
||||
return this.itemDicts[this.reviewType] || []
|
||||
},
|
||||
requirements () {
|
||||
const parts = []
|
||||
if (this.checkedItems.length) parts.push('重点审核项:' + this.checkedItems.join('、'))
|
||||
if (this.extraText && this.extraText.trim()) parts.push('其他要求:' + this.extraText.trim())
|
||||
return parts.join('\n')
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 切换类型时清空已选审核项(两套字典不同)
|
||||
reviewType () { this.checkedItems = [] }
|
||||
},
|
||||
created () {
|
||||
marked.setOptions({ breaks: true })
|
||||
this.loadItemDicts()
|
||||
},
|
||||
beforeDestroy () {
|
||||
if (this.previewUrl) URL.revokeObjectURL(this.previewUrl)
|
||||
},
|
||||
methods: {
|
||||
loadItemDicts () {
|
||||
getDicts('oa_ai_review_item_contract').then(res => {
|
||||
this.itemDicts = { ...this.itemDicts, contract: (res.data || []).map(d => ({ label: d.dictLabel, value: d.dictValue })) }
|
||||
})
|
||||
getDicts('oa_ai_review_item_resume').then(res => {
|
||||
this.itemDicts = { ...this.itemDicts, resume: (res.data || []).map(d => ({ label: d.dictLabel, value: d.dictValue })) }
|
||||
})
|
||||
},
|
||||
goBack () { this.$router.push('/hint/aiReview') },
|
||||
goDetail () { if (this.savedId) this.$router.push('/hint/aiReview/detail/' + this.savedId) },
|
||||
riskTagType (r) { return r === '高' ? 'danger' : (r === '中' ? 'warning' : 'success') },
|
||||
@@ -167,6 +212,7 @@ export default {
|
||||
fd.append('file', this.rawFile)
|
||||
fd.append('reviewType', this.reviewType)
|
||||
if (this.reviewType === 'resume' && this.position) fd.append('position', this.position)
|
||||
if (this.requirements) fd.append('requirements', this.requirements)
|
||||
|
||||
try {
|
||||
const resp = await fetch(process.env.VUE_APP_BASE_API + '/oa/aiReview/analyzeStream', {
|
||||
@@ -264,6 +310,18 @@ export default {
|
||||
}
|
||||
.bar-hint { font-size: 12px; color: #909399; margin-top: 8px; }
|
||||
|
||||
.req-area { margin-top: 10px; border-top: 1px dashed #ebeef5; padding-top: 8px; }
|
||||
.req-line { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 8px;
|
||||
&:last-child { margin-bottom: 0; }
|
||||
}
|
||||
.req-label { flex: 0 0 56px; font-size: 12px; color: #606266; padding-top: 5px; font-weight: 600; }
|
||||
.req-items { flex: 1; display: flex; flex-wrap: wrap; gap: 6px 0;
|
||||
::v-deep .el-checkbox { margin-right: 8px; margin-left: 0; }
|
||||
::v-deep .el-checkbox.is-bordered { padding: 4px 10px 4px 8px; height: auto; }
|
||||
}
|
||||
.req-tip { flex: 0 0 auto; font-size: 11px; color: #c0c4cc; padding-top: 5px; }
|
||||
.req-extra { flex: 1; }
|
||||
|
||||
.body { margin-top: 0; }
|
||||
.panel { ::v-deep .el-card__header { padding: 9px 14px; } }
|
||||
.hd { display: flex; justify-content: space-between; align-items: center;
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
<el-descriptions-item label="模型">{{ info.model || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审核时间">{{ info.createTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="审核人">{{ info.createBy || '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="info.requirements" label="审核重点 / 附加要求" :span="3">
|
||||
<span style="white-space: pre-wrap">{{ info.requirements }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div v-if="info" class="md-body" v-html="renderedMd" />
|
||||
|
||||
169
ruoyi-ui/src/views/oa/approval/config/index.vue
Normal file
169
ruoyi-ui/src/views/oa/approval/config/index.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-alert
|
||||
title="配置每个业务的审批人和或签/会签规则。审批人 user_id 多选;保存后立即生效,但只影响新提交的审批单。"
|
||||
type="info" show-icon :closable="false" style="margin-bottom: 12px;" />
|
||||
|
||||
<div style="margin-bottom: 10px;">
|
||||
<el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd">新增业务配置</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="getList">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="业务类型 key" prop="businessType" width="180" />
|
||||
<el-table-column label="业务名称" prop="businessName" width="180" />
|
||||
<el-table-column label="审批人" prop="approverNames" min-width="200" show-overflow-tooltip>
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag v-for="(name, i) in (row.approverNames || '').split(',').filter(Boolean)"
|
||||
:key="i" size="mini" style="margin-right: 4px;">{{ name }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="规则" prop="signType" width="100" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag v-if="row.signType === 1" type="success" size="mini">或签</el-tag>
|
||||
<el-tag v-else type="warning" size="mini">会签</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" prop="enabled" width="80" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag v-if="row.enabled === 1" size="mini" type="success">启用</el-tag>
|
||||
<el-tag v-else size="mini" type="info">停用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(row)">改审批人</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-delete" style="color:#f56c6c" @click="handleDel(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog :title="form.id ? '修改审批配置' : '新增审批配置'" :visible.sync="open" width="640px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="120px" size="small">
|
||||
<el-form-item label="业务类型 key" prop="businessType">
|
||||
<el-input v-model="form.businessType" :disabled="!!form.id" placeholder="如 purchase_req / contract" />
|
||||
<div class="form-tip">代码内调用 approvalService.submit 时使用的 key,建表后不要修改。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务名称" prop="businessName">
|
||||
<el-input v-model="form.businessName" placeholder="如 采购需求" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审批人" prop="approverIdList">
|
||||
<el-select v-model="form.approverIdList" multiple filterable
|
||||
placeholder="选择一个或多个审批人" style="width: 100%">
|
||||
<el-option v-for="u in userOptions" :key="u.userId"
|
||||
:label="u.nickName + (u.userName ? ('('+u.userName+')') : '')" :value="u.userId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="规则" prop="signType">
|
||||
<el-radio-group v-model="form.signType">
|
||||
<el-radio :label="1">或签(任一人通过即通过)</el-radio>
|
||||
<el-radio :label="2">会签(全部通过才通过)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用" prop="enabled">
|
||||
<el-switch v-model="form.enabled" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button @click="open = false">取消</el-button>
|
||||
<el-button type="primary" @click="submit">保 存</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listApprovalConfig, saveApprovalConfig, delApprovalConfig } from '@/api/oa/approval'
|
||||
import { listUser } from '@/api/system/user'
|
||||
|
||||
export default {
|
||||
name: 'OaApprovalConfig',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
list: [],
|
||||
userOptions: [],
|
||||
open: false,
|
||||
form: this.emptyForm(),
|
||||
rules: {
|
||||
businessType: [{ required: true, message: '业务类型不能为空', trigger: 'blur' }],
|
||||
businessName: [{ required: true, message: '业务名称不能为空', trigger: 'blur' }],
|
||||
approverIdList: [{ required: true, type: 'array', min: 1, message: '至少选择一个审批人', trigger: 'change' }],
|
||||
signType: [{ required: true, message: '请选择规则', trigger: 'change' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
this.loadUsers()
|
||||
},
|
||||
methods: {
|
||||
emptyForm() {
|
||||
return { id: null, businessType: '', businessName: '', approverIdList: [], signType: 1, enabled: 1, remark: '' }
|
||||
},
|
||||
getList() {
|
||||
this.loading = true
|
||||
listApprovalConfig().then(res => {
|
||||
this.list = res.data || []
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
loadUsers() {
|
||||
listUser({ pageNum: 1, pageSize: 1000, status: '0' }).then(res => {
|
||||
this.userOptions = res.rows || []
|
||||
})
|
||||
},
|
||||
handleAdd() {
|
||||
this.form = this.emptyForm()
|
||||
this.open = true
|
||||
},
|
||||
handleEdit(row) {
|
||||
const ids = (row.approverIds || '').split(',').filter(Boolean).map(s => Number(s))
|
||||
this.form = {
|
||||
id: row.id,
|
||||
businessType: row.businessType,
|
||||
businessName: row.businessName,
|
||||
approverIdList: ids,
|
||||
signType: row.signType,
|
||||
enabled: row.enabled,
|
||||
remark: row.remark
|
||||
}
|
||||
this.open = true
|
||||
},
|
||||
submit() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (!valid) return
|
||||
const payload = {
|
||||
id: this.form.id,
|
||||
businessType: this.form.businessType,
|
||||
businessName: this.form.businessName,
|
||||
approverIds: (this.form.approverIdList || []).join(','),
|
||||
signType: this.form.signType,
|
||||
enabled: this.form.enabled,
|
||||
remark: this.form.remark
|
||||
}
|
||||
saveApprovalConfig(payload).then(() => {
|
||||
this.$modal.msgSuccess('保存成功')
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
})
|
||||
},
|
||||
handleDel(row) {
|
||||
this.$modal.confirm('确认删除业务【' + row.businessName + '】的审批配置?').then(() =>
|
||||
delApprovalConfig(row.id)
|
||||
).then(() => {
|
||||
this.$modal.msgSuccess('删除成功')
|
||||
this.getList()
|
||||
}).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-tip { color: #909399; font-size: 12px; line-height: 1.4; margin-top: 4px; }
|
||||
</style>
|
||||
168
ruoyi-ui/src/views/oa/approval/pending/index.vue
Normal file
168
ruoyi-ui/src/views/oa/approval/pending/index.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-tabs v-model="tab" @tab-click="onTabClick" class="compact-tabs">
|
||||
<el-tab-pane name="pending" label="待我审批" />
|
||||
<el-tab-pane name="done" label="我已审批" />
|
||||
</el-tabs>
|
||||
|
||||
<el-form :model="queryParams" size="mini" :inline="true" class="compact-search">
|
||||
<el-form-item label="业务类型">
|
||||
<el-select v-model="queryParams.businessType" clearable placeholder="全部" style="width: 200px;">
|
||||
<el-option v-for="c in configs" :key="c.businessType"
|
||||
:label="c.businessName + '(' + c.businessType + ')'" :value="c.businessType" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="业务" width="160">
|
||||
<template slot-scope="{ row }">
|
||||
{{ row.businessName || row.businessType }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标题" prop="businessTitle" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="申请人" prop="applyUserName" width="100" />
|
||||
<el-table-column label="申请时间" prop="applyTime" width="160" />
|
||||
<el-table-column label="规则" width="80" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag size="mini" :type="row.signType === 1 ? 'success' : 'warning'">
|
||||
{{ row.signType === 1 ? '或签' : '会签' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审批人" prop="approverNames" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag size="mini" :type="statusTag(row.status)">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-button v-if="tab === 'pending'" type="success" size="mini" @click="openAct(row, 1)">通过</el-button>
|
||||
<el-button v-if="tab === 'pending'" type="danger" size="mini" @click="openAct(row, 2)">驳回</el-button>
|
||||
<el-button type="text" size="mini" icon="el-icon-document" @click="openDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total"
|
||||
:page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
|
||||
<!-- 审批操作弹窗 -->
|
||||
<el-dialog :title="actForm.action === 1 ? '审批通过' : '审批驳回'" :visible.sync="actOpen" width="500px" append-to-body>
|
||||
<div style="margin-bottom: 10px;">
|
||||
<b>{{ actForm.businessName }} - {{ actForm.businessTitle }}</b>
|
||||
</div>
|
||||
<el-input v-model="actForm.comment" type="textarea" :rows="3" placeholder="意见(可选)" />
|
||||
<div slot="footer">
|
||||
<el-button @click="actOpen = false">取消</el-button>
|
||||
<el-button :type="actForm.action === 1 ? 'success' : 'danger'" @click="doAct">
|
||||
{{ actForm.action === 1 ? '确认通过' : '确认驳回' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog title="审批详情" :visible.sync="detailOpen" width="640px" append-to-body>
|
||||
<div v-if="detail">
|
||||
<p><b>业务:</b>{{ detail.businessName || detail.businessType }} #{{ detail.businessId }}</p>
|
||||
<p><b>标题:</b>{{ detail.businessTitle }}</p>
|
||||
<p><b>申请人:</b>{{ detail.applyUserName }}({{ detail.applyTime }})</p>
|
||||
<p><b>审批人:</b>{{ detail.approverNames }}({{ detail.signType === 1 ? '或签' : '会签' }})</p>
|
||||
<p><b>状态:</b>
|
||||
<el-tag size="mini" :type="statusTag(detail.status)">{{ statusText(detail.status) }}</el-tag>
|
||||
</p>
|
||||
<el-divider>审批流水</el-divider>
|
||||
<el-timeline>
|
||||
<el-timeline-item v-for="r in (detail.records || [])" :key="r.id"
|
||||
:type="r.action === 1 ? 'success' : 'danger'"
|
||||
:timestamp="r.opTime">
|
||||
<b>{{ r.approverName }}</b> {{ r.action === 1 ? '通过' : '驳回' }}
|
||||
<div v-if="r.comment" style="color:#666;">{{ r.comment }}</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
listMyPending, listMyDone, listApprovalConfig,
|
||||
actApproval, getApprovalDetail
|
||||
} from '@/api/oa/approval'
|
||||
|
||||
export default {
|
||||
name: 'OaApprovalPending',
|
||||
data() {
|
||||
return {
|
||||
tab: 'pending',
|
||||
loading: false,
|
||||
total: 0,
|
||||
list: [],
|
||||
configs: [],
|
||||
queryParams: { pageNum: 1, pageSize: 10, businessType: undefined },
|
||||
actOpen: false,
|
||||
actForm: { instanceId: null, action: 1, comment: '', businessName: '', businessTitle: '' },
|
||||
detailOpen: false,
|
||||
detail: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
listApprovalConfig().then(res => { this.configs = res.data || [] })
|
||||
},
|
||||
methods: {
|
||||
statusText(s) {
|
||||
return ({ 0: '待审', 1: '通过', 2: '驳回', 3: '撤回' })[s] || '-'
|
||||
},
|
||||
statusTag(s) {
|
||||
return ({ 0: 'warning', 1: 'success', 2: 'danger', 3: 'info' })[s] || ''
|
||||
},
|
||||
onTabClick() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
|
||||
reset() { this.queryParams = { pageNum: 1, pageSize: 10, businessType: undefined }; this.getList() },
|
||||
getList() {
|
||||
this.loading = true
|
||||
const fn = this.tab === 'pending' ? listMyPending : listMyDone
|
||||
fn(this.queryParams).then(res => {
|
||||
this.list = res.rows || []
|
||||
this.total = res.total || 0
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
openAct(row, action) {
|
||||
this.actForm = {
|
||||
instanceId: row.id, action, comment: '',
|
||||
businessName: row.businessName || row.businessType,
|
||||
businessTitle: row.businessTitle
|
||||
}
|
||||
this.actOpen = true
|
||||
},
|
||||
doAct() {
|
||||
actApproval({
|
||||
instanceId: this.actForm.instanceId,
|
||||
action: this.actForm.action,
|
||||
comment: this.actForm.comment
|
||||
}).then(() => {
|
||||
this.$modal.msgSuccess('已提交')
|
||||
this.actOpen = false
|
||||
this.getList()
|
||||
})
|
||||
},
|
||||
openDetail(row) {
|
||||
getApprovalDetail(row.id).then(res => {
|
||||
this.detail = res.data
|
||||
this.detailOpen = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
112
ruoyi-ui/src/views/oa/approval/submitted/index.vue
Normal file
112
ruoyi-ui/src/views/oa/approval/submitted/index.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<el-form :model="queryParams" size="mini" :inline="true" class="compact-search">
|
||||
<el-form-item label="业务类型">
|
||||
<el-select v-model="queryParams.businessType" clearable placeholder="全部" style="width: 200px;">
|
||||
<el-option v-for="c in configs" :key="c.businessType"
|
||||
:label="c.businessName + '(' + c.businessType + ')'" :value="c.businessType" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="reset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column label="业务" width="160">
|
||||
<template slot-scope="{ row }">{{ row.businessName || row.businessType }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="标题" prop="businessTitle" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="申请时间" prop="applyTime" width="160" />
|
||||
<el-table-column label="审批人" prop="approverNames" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="状态" width="80" align="center">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag size="mini" :type="statusTag(row.status)">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="完成时间" prop="finishTime" width="160" />
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template slot-scope="{ row }">
|
||||
<el-button v-if="row.status === 0" type="text" size="mini" style="color:#f56c6c"
|
||||
@click="withdraw(row)">撤回</el-button>
|
||||
<el-button type="text" size="mini" icon="el-icon-document" @click="openDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total"
|
||||
:page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize"
|
||||
@pagination="getList" />
|
||||
|
||||
<el-dialog title="审批详情" :visible.sync="detailOpen" width="640px" append-to-body>
|
||||
<div v-if="detail">
|
||||
<p><b>业务:</b>{{ detail.businessName || detail.businessType }} #{{ detail.businessId }}</p>
|
||||
<p><b>标题:</b>{{ detail.businessTitle }}</p>
|
||||
<p><b>申请人:</b>{{ detail.applyUserName }}({{ detail.applyTime }})</p>
|
||||
<p><b>审批人:</b>{{ detail.approverNames }}({{ detail.signType === 1 ? '或签' : '会签' }})</p>
|
||||
<p><b>状态:</b>
|
||||
<el-tag size="mini" :type="statusTag(detail.status)">{{ statusText(detail.status) }}</el-tag>
|
||||
</p>
|
||||
<el-divider>审批流水</el-divider>
|
||||
<el-timeline>
|
||||
<el-timeline-item v-for="r in (detail.records || [])" :key="r.id"
|
||||
:type="r.action === 1 ? 'success' : 'danger'"
|
||||
:timestamp="r.opTime">
|
||||
<b>{{ r.approverName }}</b> {{ r.action === 1 ? '通过' : '驳回' }}
|
||||
<div v-if="r.comment" style="color:#666;">{{ r.comment }}</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
listMySubmitted, listApprovalConfig,
|
||||
withdrawApproval, getApprovalDetail
|
||||
} from '@/api/oa/approval'
|
||||
|
||||
export default {
|
||||
name: 'OaApprovalSubmitted',
|
||||
data() {
|
||||
return {
|
||||
loading: false, total: 0, list: [], configs: [],
|
||||
queryParams: { pageNum: 1, pageSize: 10, businessType: undefined },
|
||||
detailOpen: false, detail: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
listApprovalConfig().then(res => { this.configs = res.data || [] })
|
||||
},
|
||||
methods: {
|
||||
statusText(s) { return ({ 0: '待审', 1: '通过', 2: '驳回', 3: '撤回' })[s] || '-' },
|
||||
statusTag(s) { return ({ 0: 'warning', 1: 'success', 2: 'danger', 3: 'info' })[s] || '' },
|
||||
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
|
||||
reset() { this.queryParams = { pageNum: 1, pageSize: 10, businessType: undefined }; this.getList() },
|
||||
getList() {
|
||||
this.loading = true
|
||||
listMySubmitted(this.queryParams).then(res => {
|
||||
this.list = res.rows || []
|
||||
this.total = res.total || 0
|
||||
}).finally(() => { this.loading = false })
|
||||
},
|
||||
withdraw(row) {
|
||||
this.$modal.confirm('确认撤回审批单?').then(() =>
|
||||
withdrawApproval(row.id)
|
||||
).then(() => {
|
||||
this.$modal.msgSuccess('已撤回')
|
||||
this.getList()
|
||||
}).catch(() => {})
|
||||
},
|
||||
openDetail(row) {
|
||||
getApprovalDetail(row.id).then(res => {
|
||||
this.detail = res.data
|
||||
this.detailOpen = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
278
ruoyi-ui/src/views/oa/docs/contract/index.vue
Normal file
278
ruoyi-ui/src/views/oa/docs/contract/index.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<div class="app-container docs-page">
|
||||
<h1>合同流 · 使用说明</h1>
|
||||
<el-alert type="info" :closable="false" show-icon
|
||||
title="合同从「谁起草」到「谁批」到「正式生效」的全过程。本页让业务员、法务、领导都清楚自己在哪一步。"
|
||||
style="margin-bottom: 20px;" />
|
||||
|
||||
<!-- ============ 泳道图 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>一、流程全景(泳道图)</b></div>
|
||||
<div class="swimlane-wrap">
|
||||
<svg viewBox="0 0 1180 560" class="swimlane-svg" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#606266" /></marker>
|
||||
<marker id="arrow-red" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#f56c6c" /></marker>
|
||||
<marker id="arrow-green" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#67c23a" /></marker>
|
||||
<marker id="arrow-dash" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#909399" /></marker>
|
||||
</defs>
|
||||
|
||||
<!-- 泳道背景 -->
|
||||
<g class="lanes">
|
||||
<rect x="120" y="20" width="1040" height="100" fill="#f9f6ff" stroke="#d3adf7" />
|
||||
<text x="20" y="76" class="lane-label">管理员</text>
|
||||
<text x="20" y="92" class="lane-sub">配置审批人</text>
|
||||
|
||||
<rect x="120" y="125" width="1040" height="120" fill="#eef9ff" stroke="#91d5ff" />
|
||||
<text x="20" y="185" class="lane-label">起草人</text>
|
||||
<text x="20" y="201" class="lane-sub">业务员 / 法务</text>
|
||||
|
||||
<rect x="120" y="250" width="1040" height="150" fill="#fff7e6" stroke="#ffd591" />
|
||||
<text x="20" y="325" class="lane-label">系统</text>
|
||||
<text x="20" y="341" class="lane-sub">自动派单 / 提醒</text>
|
||||
|
||||
<rect x="120" y="405" width="1040" height="135" fill="#f6ffed" stroke="#b7eb8f" />
|
||||
<text x="20" y="475" class="lane-label">审批人</text>
|
||||
<text x="20" y="491" class="lane-sub">领导 / 法务 / 财务</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 管理员 ===== -->
|
||||
<g class="node node-admin">
|
||||
<rect x="140" y="40" width="180" height="60" rx="6" />
|
||||
<text x="230" y="64" class="t1">① 配置「谁来批合同」</text>
|
||||
<text x="230" y="82" class="t2">选定审批人</text>
|
||||
<text x="230" y="96" class="t2">设置或签 / 会签</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 起草人 ===== -->
|
||||
<g class="node node-user">
|
||||
<rect x="140" y="150" width="180" height="70" rx="6" />
|
||||
<text x="230" y="178" class="t1">② 起草合同</text>
|
||||
<text x="230" y="196" class="t2">填合同名/甲乙方/金额</text>
|
||||
<text x="230" y="210" class="t2">/合同管理/新增</text>
|
||||
</g>
|
||||
|
||||
<g class="node node-user">
|
||||
<rect x="900" y="150" width="210" height="70" rx="6" />
|
||||
<text x="1005" y="178" class="t1">⑦ 合同生效执行</text>
|
||||
<text x="1005" y="196" class="t2">签订 / 履约 / 归档</text>
|
||||
<text x="1005" y="210" class="t2">(审批通过后才发出)</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 系统 ===== -->
|
||||
<g class="node node-sys">
|
||||
<rect x="350" y="270" width="170" height="70" rx="6" />
|
||||
<text x="435" y="296" class="t1">③ 自动生成审批单</text>
|
||||
<text x="435" y="314" class="t2">把当前审批人 +</text>
|
||||
<text x="435" y="328" class="t2">或签/会签规则定下来</text>
|
||||
</g>
|
||||
<g class="node node-sys">
|
||||
<rect x="560" y="270" width="160" height="70" rx="6" />
|
||||
<text x="640" y="296" class="t1">④ 派单给审批人</text>
|
||||
<text x="640" y="314" class="t2">在「我的审批」</text>
|
||||
<text x="640" y="328" class="t2">出现待办</text>
|
||||
</g>
|
||||
<g class="node node-sys">
|
||||
<rect x="900" y="350" width="210" height="40" rx="6" />
|
||||
<text x="1005" y="376" class="t1">⑥' 判定整单结果(或/会签)</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 审批人 ===== -->
|
||||
<g class="node node-app">
|
||||
<rect x="560" y="430" width="180" height="80" rx="6" />
|
||||
<text x="650" y="456" class="t1">⑤ 审批人处理</text>
|
||||
<text x="650" y="474" class="t2">通过 ✓ 或 驳回 ✗</text>
|
||||
<text x="650" y="490" class="t2">可写意见</text>
|
||||
<text x="650" y="506" class="t2">流水自动留痕</text>
|
||||
</g>
|
||||
|
||||
<!-- 配置 → 生成(虚线) -->
|
||||
<path d="M 320 80 C 400 80, 400 240, 435 270"
|
||||
fill="none" stroke="#909399" stroke-width="1.4" stroke-dasharray="5 4" marker-end="url(#arrow-dash)" />
|
||||
<text x="345" y="160" class="arrow-label dim">仅影响后续新单</text>
|
||||
|
||||
<!-- ② → ③ -->
|
||||
<path d="M 320 200 C 360 200, 360 250, 435 270"
|
||||
fill="none" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
|
||||
<!-- ③ → ④ -->
|
||||
<line x1="520" y1="305" x2="560" y2="305" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
|
||||
<!-- ④ → ⑤ -->
|
||||
<path d="M 640 340 C 640 380, 650 405, 650 430"
|
||||
fill="none" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
<text x="660" y="395" class="arrow-label">收到待办</text>
|
||||
|
||||
<!-- ⑤ → ⑥' -->
|
||||
<path d="M 740 460 C 850 460, 880 410, 950 390"
|
||||
fill="none" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
|
||||
<!-- ⑥' → ⑦ 通过 -->
|
||||
<path d="M 1005 350 L 1005 220"
|
||||
fill="none" stroke="#67c23a" stroke-width="2" marker-end="url(#arrow-green)" />
|
||||
<text x="1018" y="290" class="arrow-label ok">通过</text>
|
||||
|
||||
<!-- ⑥' → ② 驳回(闭环) -->
|
||||
<path d="M 900 370 C 700 370, 400 380, 230 240"
|
||||
fill="none" stroke="#f56c6c" stroke-width="2" stroke-dasharray="6 3" marker-end="url(#arrow-red)" />
|
||||
<text x="540" y="408" class="arrow-label bad">驳回 → 改完再提,自动开新单</text>
|
||||
|
||||
<!-- ⑦ → 完成 -->
|
||||
<g class="node node-end">
|
||||
<circle cx="1130" cy="185" r="22" />
|
||||
<text x="1130" y="190" class="t-end">归档</text>
|
||||
</g>
|
||||
<line x1="1110" y1="185" x2="1108" y2="185" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<span><i class="dot dot-admin"></i>管理员动作</span>
|
||||
<span><i class="dot dot-user"></i>起草人动作</span>
|
||||
<span><i class="dot dot-sys"></i>系统自动</span>
|
||||
<span><i class="dot dot-app"></i>审批人动作</span>
|
||||
<span><svg width="34" height="10"><line x1="0" y1="5" x2="30" y2="5" stroke="#67c23a" stroke-width="2"/></svg> 通过</span>
|
||||
<span><svg width="34" height="10"><line x1="0" y1="5" x2="30" y2="5" stroke="#f56c6c" stroke-width="2" stroke-dasharray="6 3"/></svg> 驳回退回</span>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 审批状态 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>二、合同列表里「审批」列的含义</b></div>
|
||||
<p class="hint">合同管理列表会显示一列「审批」,告诉你这份合同走到流程哪一步了。</p>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<div class="state-card">
|
||||
<h4>「审批」列<span class="state-owner">(合同到底批没批)</span></h4>
|
||||
<div class="state-row"><el-tag size="small" type="info">未提交</el-tag><span>合同审批未启用 / 未配置,合同直接生效</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="warning">待审</el-tag><span>已派给审批人,等他们处理</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="success">通过</el-tag><span>合同可以正式签订 / 发出</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="danger">驳回</el-tag><span>修改合同内容后再保存,系统会自动新开一张审批单</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="info">撤回</el-tag><span>起草人主动撤回,本单作废</span></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="state-card warn">
|
||||
<h4>⚠️ 卡点:未审批通过不能标记「完结」</h4>
|
||||
<p>合同的字段(甲乙方/金额/条款等)<b>不受审批拦截</b>,可以照常编辑;但要把合同状态推进到 <el-tag size="mini" type="success">完结</el-tag> 时,<b>最新审批必须是「通过」</b>,否则系统会拒绝并提示「该合同尚未审批通过」。</p>
|
||||
<p class="hint">如果整个合同审批没启用 / 未配置(列表显示"未提交"),则不会有任何拦截,完结照常推进。</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 角色操作速查 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>三、不同角色怎么用</b></div>
|
||||
<el-table :data="roleSteps" border size="small">
|
||||
<el-table-column label="你是" prop="role" width="100" />
|
||||
<el-table-column label="去哪儿" prop="menu" width="240" />
|
||||
<el-table-column label="做什么" prop="action" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 或签 vs 会签 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>四、或签 vs 会签(在合同场景下怎么选)</b></div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<div class="state-card">
|
||||
<h4>或签 <el-tag size="mini" type="success">任一人即可</el-tag></h4>
|
||||
<p>审批人列表里 <b>任意一人</b> 通过 → 整张合同审批通过。</p>
|
||||
<p class="hint">适用:日常采购合同、小额合同、找最快有空的领导批即可的场景。</p>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="state-card">
|
||||
<h4>会签 <el-tag size="mini" type="warning">必须全员通过</el-tag></h4>
|
||||
<p>审批人列表里 <b>所有人</b> 都通过 → 整张合同审批才通过。</p>
|
||||
<p class="hint">适用:大额合同 / 跨部门合同 / 重大法务事项 — 法务 + 财务 + 主管领导都要背书。</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ FAQ ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>五、常见问题</b></div>
|
||||
<el-collapse>
|
||||
<el-collapse-item title="我新建了合同,但不想走审批,怎么办?">
|
||||
找管理员在「审批配置」里把「合同」这一行的「启用」开关关掉,新建合同就不再产生审批单,可以直接执行。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="合同已经审批通过了,但条款要改怎么办?">
|
||||
目前实现不会自动重新发起审批。如果改动比较大、有原则性变化,建议起草人在「我的发起」撤回旧单(如果还能撤回),或者重新建一份合同记录走新流程,留好痕迹。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="审批被驳回了,合同记录还在吗?">
|
||||
在。合同本身不会消失,改完字段重新保存时系统会自动新开一张审批单(流程图里那条红色虚线)。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="谁审过、什么意见去哪看?">
|
||||
起草人去「审批中心 → 我的发起」找到这张合同的审批单点「详情」,里面有完整的审批流水(谁、什么时间、通过/驳回、意见)。审批人本人在「我的审批 → 我已审批」也能看。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="我换了合同审批人,老合同的审批单上人没变?">
|
||||
单子生成时已经把审批人和或签/会签规则定下来了 —— <b>不会回溯</b>。修改只对之后新提交的合同生效。
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'OaDocContractFlow',
|
||||
data() {
|
||||
return {
|
||||
roleSteps: [
|
||||
{ role: '管理员', menu: '审批中心 → 审批配置', action: '选定合同的审批人;切换或签 / 会签;启用或停用合同审批。修改不影响已发起的旧单。' },
|
||||
{ role: '起草人', menu: '合同管理 → 新增', action: '填合同信息,保存后系统自动派单给审批人;列表的「审批」列会出现「待审」。' },
|
||||
{ role: '起草人', menu: '审批中心 → 我的发起', action: '查自己提交过的所有合同审批单 + 流水 + 意见;待审中可撤回。' },
|
||||
{ role: '审批人', menu: '审批中心 → 我的审批', action: '「待我审批」tab 里点通过 / 驳回并写意见;「我已审批」可回看历史。' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.docs-page { max-width: 1240px; }
|
||||
.docs-page h1 { font-size: 22px; margin: 0 0 16px; }
|
||||
.doc-card { margin-bottom: 16px; }
|
||||
|
||||
.swimlane-wrap { width: 100%; overflow-x: auto; }
|
||||
.swimlane-svg { width: 100%; min-width: 1100px; height: auto; display: block; }
|
||||
.swimlane-svg .lane-label { font-size: 15px; font-weight: 600; fill: #303133; }
|
||||
.swimlane-svg .lane-sub { font-size: 11px; fill: #909399; }
|
||||
.swimlane-svg .node rect { stroke-width: 1.3; }
|
||||
.swimlane-svg .node .t1 { font-size: 13px; font-weight: 600; text-anchor: middle; fill: #303133; }
|
||||
.swimlane-svg .node .t2 { font-size: 11px; text-anchor: middle; fill: #606266; }
|
||||
.swimlane-svg .node-admin rect { fill: #fff; stroke: #b37feb; }
|
||||
.swimlane-svg .node-user rect { fill: #fff; stroke: #40a9ff; }
|
||||
.swimlane-svg .node-sys rect { fill: #fff; stroke: #ffa940; }
|
||||
.swimlane-svg .node-app rect { fill: #fff; stroke: #73d13d; }
|
||||
.swimlane-svg .node-end circle { fill: #67c23a; stroke: #67c23a; }
|
||||
.swimlane-svg .node-end .t-end { font-size: 12px; fill: #fff; font-weight: 600; text-anchor: middle; }
|
||||
.swimlane-svg .arrow-label { font-size: 11px; fill: #606266; }
|
||||
.swimlane-svg .arrow-label.dim { fill: #909399; }
|
||||
.swimlane-svg .arrow-label.ok { fill: #67c23a; font-weight: 600; }
|
||||
.swimlane-svg .arrow-label.bad { fill: #f56c6c; font-weight: 600; }
|
||||
|
||||
.legend { margin-top: 12px; padding: 8px 12px; background: #fafafa; border-radius: 4px;
|
||||
display: flex; flex-wrap: wrap; gap: 18px; font-size: 12px; color: #606266; align-items: center; }
|
||||
.legend .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 6px; vertical-align: middle; }
|
||||
.legend .dot-sys { background: #ffa940; }
|
||||
.legend .dot-user { background: #40a9ff; }
|
||||
.legend .dot-app { background: #73d13d; }
|
||||
.legend .dot-admin { background: #b37feb; }
|
||||
|
||||
.state-card { background: #fafafa; border-radius: 6px; padding: 14px 16px; height: 100%; }
|
||||
.state-card.warn { background: #fff8e6; }
|
||||
.state-card h4 { margin: 0 0 10px; font-size: 14px; }
|
||||
.state-card .state-owner { font-weight: normal; color: #909399; margin-left: 6px; font-size: 12px; }
|
||||
.state-card .state-row { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; font-size: 13px; color: #606266; }
|
||||
.state-card p { line-height: 1.7; color: #303133; margin: 6px 0; }
|
||||
.state-card p.hint { color: #909399; font-size: 12px; }
|
||||
.docs-page .hint { color: #909399; font-size: 13px; margin-top: 0; }
|
||||
</style>
|
||||
295
ruoyi-ui/src/views/oa/docs/purchase/index.vue
Normal file
295
ruoyi-ui/src/views/oa/docs/purchase/index.vue
Normal file
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<div class="app-container docs-page">
|
||||
<h1>采购流 · 使用说明</h1>
|
||||
<el-alert type="info" :closable="false" show-icon
|
||||
title="一张图看懂:采购需求从「谁提」到「谁审」再到「采购完成」全过程。下方的状态对照和速查表帮你弄清自己当下该做什么。"
|
||||
style="margin-bottom: 20px;" />
|
||||
|
||||
<!-- ============ 泳道图 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>一、流程全景(泳道图)</b></div>
|
||||
<div class="swimlane-wrap">
|
||||
<svg viewBox="0 0 1180 560" class="swimlane-svg" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5"
|
||||
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#606266" />
|
||||
</marker>
|
||||
<marker id="arrow-red" viewBox="0 0 10 10" refX="9" refY="5"
|
||||
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#f56c6c" />
|
||||
</marker>
|
||||
<marker id="arrow-green" viewBox="0 0 10 10" refX="9" refY="5"
|
||||
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#67c23a" />
|
||||
</marker>
|
||||
<marker id="arrow-dash" viewBox="0 0 10 10" refX="9" refY="5"
|
||||
markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M0,0 L10,5 L0,10 z" fill="#909399" />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- 泳道背景 -->
|
||||
<g class="lanes">
|
||||
<rect x="120" y="20" width="1040" height="100" fill="#f9f6ff" stroke="#d3adf7" />
|
||||
<text x="20" y="76" class="lane-label">管理员</text>
|
||||
<text x="20" y="92" class="lane-sub">配置审批人</text>
|
||||
|
||||
<rect x="120" y="125" width="1040" height="120" fill="#eef9ff" stroke="#91d5ff" />
|
||||
<text x="20" y="185" class="lane-label">发起人</text>
|
||||
<text x="20" y="201" class="lane-sub">提需求 / 推进</text>
|
||||
|
||||
<rect x="120" y="250" width="1040" height="150" fill="#fff7e6" stroke="#ffd591" />
|
||||
<text x="20" y="325" class="lane-label">系统</text>
|
||||
<text x="20" y="341" class="lane-sub">自动派单 / 提醒</text>
|
||||
|
||||
<rect x="120" y="405" width="1040" height="135" fill="#f6ffed" stroke="#b7eb8f" />
|
||||
<text x="20" y="475" class="lane-label">审批人</text>
|
||||
<text x="20" y="491" class="lane-sub">通过 / 驳回</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 管理员 ===== -->
|
||||
<g class="node node-admin">
|
||||
<rect x="140" y="40" width="180" height="60" rx="6" />
|
||||
<text x="230" y="64" class="t1">① 配置「谁来审」</text>
|
||||
<text x="230" y="82" class="t2">选定审批人</text>
|
||||
<text x="230" y="96" class="t2">设置或签 / 会签</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 发起人 ===== -->
|
||||
<g class="node node-user">
|
||||
<rect x="140" y="150" width="170" height="70" rx="6" />
|
||||
<text x="225" y="178" class="t1">② 新建采购需求</text>
|
||||
<text x="225" y="196" class="t2">填标题/物料/截止</text>
|
||||
<text x="225" y="210" class="t2">/任务/采购需求</text>
|
||||
</g>
|
||||
|
||||
<g class="node node-user">
|
||||
<rect x="900" y="150" width="210" height="70" rx="6" />
|
||||
<text x="1005" y="178" class="t1">⑦ 推进业务状态</text>
|
||||
<text x="1005" y="196" class="t2">未采购 → 采购中 → 完成</text>
|
||||
<text x="1005" y="210" class="t2">(审批通过后才放开)</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 系统 ===== -->
|
||||
<g class="node node-sys">
|
||||
<rect x="350" y="270" width="170" height="70" rx="6" />
|
||||
<text x="435" y="296" class="t1">③ 自动生成审批单</text>
|
||||
<text x="435" y="314" class="t2">把当前审批人 +</text>
|
||||
<text x="435" y="328" class="t2">或签/会签规则定下来</text>
|
||||
</g>
|
||||
<g class="node node-sys">
|
||||
<rect x="560" y="270" width="160" height="70" rx="6" />
|
||||
<text x="640" y="296" class="t1">④ 派单给审批人</text>
|
||||
<text x="640" y="314" class="t2">在「我的审批」</text>
|
||||
<text x="640" y="328" class="t2">出现待办</text>
|
||||
</g>
|
||||
<g class="node node-sys">
|
||||
<rect x="900" y="350" width="210" height="40" rx="6" />
|
||||
<text x="1005" y="376" class="t1">⑥' 判定整单结果(或/会签)</text>
|
||||
</g>
|
||||
|
||||
<!-- ===== 审批人 ===== -->
|
||||
<g class="node node-app">
|
||||
<rect x="560" y="430" width="180" height="80" rx="6" />
|
||||
<text x="650" y="456" class="t1">⑤ 审批人处理</text>
|
||||
<text x="650" y="474" class="t2">通过 ✓ 或 驳回 ✗</text>
|
||||
<text x="650" y="490" class="t2">可写意见</text>
|
||||
<text x="650" y="506" class="t2">流水自动留痕</text>
|
||||
</g>
|
||||
|
||||
<!-- ============ 箭头 ============ -->
|
||||
<!-- 配置 → 审批单生成(虚线,从管理员到 ③) -->
|
||||
<path d="M 320 80 C 400 80, 400 240, 435 270"
|
||||
fill="none" stroke="#909399" stroke-width="1.4"
|
||||
stroke-dasharray="5 4" marker-end="url(#arrow-dash)" />
|
||||
<text x="345" y="160" class="arrow-label dim">仅影响后续新单</text>
|
||||
|
||||
<!-- ② → ③ -->
|
||||
<path d="M 310 200 C 360 200, 360 250, 435 270"
|
||||
fill="none" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
|
||||
<!-- ③ → ④ -->
|
||||
<line x1="520" y1="305" x2="560" y2="305" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
|
||||
<!-- ④ → ⑤ -->
|
||||
<path d="M 640 340 C 640 380, 650 405, 650 430"
|
||||
fill="none" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
<text x="660" y="395" class="arrow-label">收到待办</text>
|
||||
|
||||
<!-- ⑤ → ⑥' 终结判定 -->
|
||||
<path d="M 740 460 C 850 460, 880 410, 950 390"
|
||||
fill="none" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
|
||||
<!-- ⑥' → ⑦ 通过 -->
|
||||
<path d="M 1005 350 L 1005 220"
|
||||
fill="none" stroke="#67c23a" stroke-width="2" marker-end="url(#arrow-green)" />
|
||||
<text x="1018" y="290" class="arrow-label ok">通过</text>
|
||||
|
||||
<!-- ⑥' → ② 驳回(闭环 / 红色) -->
|
||||
<path d="M 900 370 C 700 370, 400 380, 225 240"
|
||||
fill="none" stroke="#f56c6c" stroke-width="2" stroke-dasharray="6 3"
|
||||
marker-end="url(#arrow-red)" />
|
||||
<text x="540" y="408" class="arrow-label bad">驳回 → 改完再提,自动开新单</text>
|
||||
|
||||
<!-- ⑦ → 完成 -->
|
||||
<g class="node node-end">
|
||||
<circle cx="1130" cy="185" r="22" />
|
||||
<text x="1130" y="190" class="t-end">完成</text>
|
||||
</g>
|
||||
<line x1="1110" y1="185" x2="1108" y2="185" stroke="#606266" stroke-width="1.6" marker-end="url(#arrow)" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<span><i class="dot dot-admin"></i>管理员动作</span>
|
||||
<span><i class="dot dot-user"></i>发起人动作</span>
|
||||
<span><i class="dot dot-sys"></i>系统自动</span>
|
||||
<span><i class="dot dot-app"></i>审批人动作</span>
|
||||
<span><svg width="34" height="10"><line x1="0" y1="5" x2="30" y2="5" stroke="#67c23a" stroke-width="2"/></svg> 通过</span>
|
||||
<span><svg width="34" height="10"><line x1="0" y1="5" x2="30" y2="5" stroke="#f56c6c" stroke-width="2" stroke-dasharray="6 3"/></svg> 驳回退回</span>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 状态对照 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>二、两类状态对照(务必区分)</b></div>
|
||||
<p class="hint">列表里你会看到两列状态:左边「审批」表示这条采购需求 <b>谁来批、批了没</b>;右边「状态」表示 <b>采购到哪一步了</b>。两者独立但有关联——审批没通过,业务状态推不动。</p>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<div class="state-card">
|
||||
<h4>「审批」列<span class="state-owner">(谁来批 · 批了没)</span></h4>
|
||||
<div class="state-row"><el-tag size="small" type="info">未提交</el-tag><span>该业务尚未配置审批,或审批未启用</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="warning">待审</el-tag><span>已派给审批人,等他们处理</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="success">通过</el-tag><span>审批结束,可以推进业务了</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="danger">驳回</el-tag><span>审批人不通过,改完字段再保存会自动开新单</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="info">撤回</el-tag><span>发起人自己撤回了,本单作废</span></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="state-card">
|
||||
<h4>「状态」列<span class="state-owner">(采购到哪一步了)</span></h4>
|
||||
<div class="state-row"><el-tag size="small">未采购</el-tag><span>刚新建,等审批通过后才能往下推</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="warning">采购中</el-tag><span>开始下单 / 联系供应商</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="success">完成</el-tag><span>采购到货,闭环结束</span></div>
|
||||
<div class="state-row"><el-tag size="small" type="info">取消</el-tag><span>不再需要的需求,可直接取消(无需审批)</span></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 角色操作速查 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>三、不同角色怎么用(按你是谁找对应行)</b></div>
|
||||
<el-table :data="roleSteps" border size="small">
|
||||
<el-table-column label="你是" prop="role" width="100" />
|
||||
<el-table-column label="去哪儿" prop="menu" width="240" />
|
||||
<el-table-column label="做什么 / 看什么" prop="action" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 或签 vs 会签 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>四、或签 vs 会签</b></div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<div class="state-card">
|
||||
<h4>或签 <el-tag size="mini" type="success">任一人即可</el-tag></h4>
|
||||
<p>审批人列表里 <b>任意一人</b> 点通过,整张单子就通过;任一人点驳回,整张单子立即驳回。</p>
|
||||
<p class="hint">适用:日常事务、找最快有空的同事盖章即可。</p>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="state-card">
|
||||
<h4>会签 <el-tag size="mini" type="warning">必须全员通过</el-tag></h4>
|
||||
<p>审批人列表里 <b>所有人</b> 都点通过,整张单子才通过;只要有一个人驳回,整张单子立即驳回。</p>
|
||||
<p class="hint">适用:跨部门、需要每个负责人都背书的事项。</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- ============ 常见问题 ============ -->
|
||||
<el-card class="doc-card" shadow="never">
|
||||
<div slot="header"><b>五、常见问题</b></div>
|
||||
<el-collapse>
|
||||
<el-collapse-item title="状态下拉点不动,提示「尚未审批通过」?">
|
||||
采购需求必须审批通过才能进入「采购中/完成」。去「审批中心 → 我的发起」看看自己这单是不是还在「待审」或者已经被「驳回」。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="我换了审批人,为什么旧单子上的人没变?">
|
||||
单子在生成那一刻就把当时的审批人定下来了,<b>不会回溯</b>,这是为了避免审批中途偷偷换人。你的修改对后面新提交的单子生效。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="审批被驳回了,我的采购需求还在吗?">
|
||||
在。需求本身不会消失,你改完字段重新保存,系统会自动开一张新审批单。(就是流程图里那条红色虚线的回流路径。)
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="我的事项太急,能不走审批吗?">
|
||||
找管理员在「审批配置」把对应业务的「启用」临时关掉,新提交的就直接生效不走审批。事项处理完再让管理员开回来。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="我发起后又不想审批了,能撤回吗?">
|
||||
可以。在「审批中心 → 我的发起」找到这张单子,只要还是「待审」状态就能撤回;已经被审批人处理过的(通过/驳回)不能撤回。
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="为什么我在「我的审批」里看不到任何待办?">
|
||||
说明目前没人提交需要你审的单。也可能管理员没把你配进审批人列表里,去问下管理员。
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'OaDocPurchaseFlow',
|
||||
data() {
|
||||
return {
|
||||
roleSteps: [
|
||||
{ role: '管理员', menu: '审批中心 → 审批配置', action: '选定每个业务的审批人;切换或签 / 会签;启用或停用某个业务的审批。修改不影响已经发起的旧单。' },
|
||||
{ role: '发起人', menu: '任务 → 采购需求', action: '点「新增」填写需求,保存后系统自动派给审批人;列表里会出现「待审」徽标。' },
|
||||
{ role: '发起人', menu: '审批中心 → 我的发起', action: '看自己提交过的所有审批单 + 完整流水 + 审批意见;待审中可撤回。' },
|
||||
{ role: '审批人', menu: '审批中心 → 我的审批', action: '「待我审批」tab 里点通过 / 驳回并写意见;「我已审批」可回看自己处理过的历史。' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.docs-page { max-width: 1240px; }
|
||||
.docs-page h1 { font-size: 22px; margin: 0 0 16px; }
|
||||
.doc-card { margin-bottom: 16px; }
|
||||
|
||||
.swimlane-wrap { width: 100%; overflow-x: auto; }
|
||||
.swimlane-svg { width: 100%; min-width: 1100px; height: auto; display: block; }
|
||||
.swimlane-svg .lane-label { font-size: 15px; font-weight: 600; fill: #303133; }
|
||||
.swimlane-svg .lane-sub { font-size: 11px; fill: #909399; }
|
||||
.swimlane-svg .node rect { stroke-width: 1.3; }
|
||||
.swimlane-svg .node .t1 { font-size: 13px; font-weight: 600; text-anchor: middle; fill: #303133; }
|
||||
.swimlane-svg .node .t2 { font-size: 11px; text-anchor: middle; fill: #606266; }
|
||||
.swimlane-svg .node-admin rect { fill: #fff; stroke: #b37feb; }
|
||||
.swimlane-svg .node-user rect { fill: #fff; stroke: #40a9ff; }
|
||||
.swimlane-svg .node-sys rect { fill: #fff; stroke: #ffa940; }
|
||||
.swimlane-svg .node-app rect { fill: #fff; stroke: #73d13d; }
|
||||
.swimlane-svg .node-end circle { fill: #67c23a; stroke: #67c23a; }
|
||||
.swimlane-svg .node-end .t-end { font-size: 12px; fill: #fff; font-weight: 600; text-anchor: middle; }
|
||||
.swimlane-svg .arrow-label { font-size: 11px; fill: #606266; }
|
||||
.swimlane-svg .arrow-label.dim { fill: #909399; }
|
||||
.swimlane-svg .arrow-label.ok { fill: #67c23a; font-weight: 600; }
|
||||
.swimlane-svg .arrow-label.bad { fill: #f56c6c; font-weight: 600; }
|
||||
|
||||
.legend {
|
||||
margin-top: 12px; padding: 8px 12px; background: #fafafa; border-radius: 4px;
|
||||
display: flex; flex-wrap: wrap; gap: 18px; font-size: 12px; color: #606266; align-items: center;
|
||||
}
|
||||
.legend .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 6px; vertical-align: middle; }
|
||||
.legend .dot-sys { background: #ffa940; }
|
||||
.legend .dot-user { background: #40a9ff; }
|
||||
.legend .dot-app { background: #73d13d; }
|
||||
.legend .dot-admin { background: #b37feb; }
|
||||
|
||||
.state-card { background: #fafafa; border-radius: 6px; padding: 14px 16px; height: 100%; }
|
||||
.state-card h4 { margin: 0 0 10px; font-size: 14px; }
|
||||
.state-card .state-owner { font-weight: normal; color: #909399; margin-left: 6px; font-size: 12px; }
|
||||
.state-card .state-row { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; font-size: 13px; color: #606266; }
|
||||
.state-card p { line-height: 1.7; color: #303133; margin: 6px 0; }
|
||||
.state-card p.hint { color: #909399; font-size: 12px; }
|
||||
.docs-page .hint { color: #909399; font-size: 13px; margin-top: 0; }
|
||||
</style>
|
||||
@@ -93,6 +93,15 @@
|
||||
<span>{{ parseTime(scope.row.signTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审批" align="center" prop="approvalStatus" width="90">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag v-if="row.approvalStatus == null" size="mini" type="info">未提交</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 0" size="mini" type="warning">待审</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 1" size="mini" type="success">通过</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 2" size="mini" type="danger">驳回</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 3" size="mini" type="info">撤回</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="contractStatus" width="120">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.sys_show_hide" :value="scope.row.contractStatus"/>
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
<span @click="gotoPurchase" style="cursor: pointer;">如要发放采购相关需求请移步采购需求管理(点击此处快速跳转)</span>
|
||||
</el-alert>
|
||||
|
||||
<el-row :gutter="20" v-loading="loading">
|
||||
<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="6" v-for="item in taskList" :key="item.taskId">
|
||||
<el-row :gutter="10" v-loading="loading">
|
||||
<el-col class="task-col" v-for="item in taskList" :key="item.taskId">
|
||||
<el-card class="task-card" shadow="hover" @click.native="handleLookTask(item)">
|
||||
<!-- 卡片头部 -->
|
||||
<template slot="header">
|
||||
@@ -99,6 +99,11 @@
|
||||
</div>
|
||||
<span v-else>{{ item.workerNickName }}</span>
|
||||
</div>
|
||||
<div class="info-item attachment-row" @click.stop>
|
||||
<span class="info-label">附件:</span>
|
||||
<file-preview v-if="item.accessory" :value="item.accessory" class="card-attachment" />
|
||||
<el-tag v-else type="info" size="mini">无附件</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片底部操作按钮 -->
|
||||
@@ -352,11 +357,7 @@
|
||||
附件
|
||||
</template>
|
||||
<div class="attachment-box">
|
||||
<div v-if="form.accessory" class="attachment-item">
|
||||
<el-link :href="form.accessory" :underline="false" target="_blank">
|
||||
<i class="el-icon-document"></i> 查看附件
|
||||
</el-link>
|
||||
</div>
|
||||
<file-preview v-if="form.accessory" v-model="form.accessory" />
|
||||
<div v-else class="no-attachment">暂无附件...</div>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
@@ -374,6 +375,7 @@ import { addTask, delTask, getTask, listTask, updateTask } from "@/api/oa/task";
|
||||
import { deptTreeSelect, selectUser } from "@/api/system/user";
|
||||
import UserSelect from "@/components/UserSelect";
|
||||
import ProjectSelect from "@/components/fad-service/ProjectSelect";
|
||||
import FilePreview from "@/components/FilePreview";
|
||||
import OperationLogDrawer from "@/views/oa/project/operationLog/OperationLogDrawer.vue";
|
||||
|
||||
|
||||
@@ -382,6 +384,7 @@ export default {
|
||||
components: {
|
||||
UserSelect,
|
||||
ProjectSelect,
|
||||
FilePreview,
|
||||
OperationLogDrawer
|
||||
},
|
||||
dicts: ['sys_project_type', 'sys_project_status', 'sys_work_type', 'sys_sort_grade'],
|
||||
@@ -426,7 +429,7 @@ export default {
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
pageSize: 20,
|
||||
searchTime: [],
|
||||
},
|
||||
userList: [],
|
||||
@@ -765,12 +768,116 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.task-col {
|
||||
flex: 0 0 20%;
|
||||
max-width: 20%;
|
||||
}
|
||||
|
||||
.task-card {
|
||||
margin-bottom: 20px;
|
||||
height: 320px;
|
||||
margin-bottom: 12px;
|
||||
height: 300px;
|
||||
/* 固定卡片高度 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.task-card >>> .el-card__header {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.task-card >>> .el-card__body {
|
||||
padding: 10px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.task-card .task-title {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.task-card .info-label {
|
||||
font-size: 12px;
|
||||
min-width: 56px;
|
||||
}
|
||||
|
||||
.task-card .info-item {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.task-card .card-footer .el-button {
|
||||
margin-right: 4px;
|
||||
padding: 4px 2px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
.task-col {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.task-col {
|
||||
flex: 0 0 33.3333%;
|
||||
max-width: 33.3333%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.task-col {
|
||||
flex: 0 0 50%;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.task-col {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-row {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card-attachment {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-attachment >>> .el-upload-list__item {
|
||||
margin: 0;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-attachment >>> .el-upload-list__item:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.card-attachment >>> .ele-upload-list__item-content-action {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.card-attachment >>> .el-upload-list__item:hover .ele-upload-list__item-content-action {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.card-attachment >>> .el-icon-document {
|
||||
color: #409eff;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@@ -817,6 +924,7 @@ export default {
|
||||
padding: 10px 0;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
|
||||
@@ -66,40 +66,25 @@
|
||||
|
||||
<!-- 新增提示组件 -->
|
||||
<el-alert title="提示:列表存在分页,部分信息需翻页查看" type="info" closable show-icon style="margin-bottom: 10px;" />
|
||||
<el-table v-loading="loading" :data="requirementsList" @selection-change="handleSelectionChange"
|
||||
@expand-change="onExpandChange">
|
||||
<el-table-column type="expand" width="36">
|
||||
<template slot-scope="props">
|
||||
<div style="padding: 8px 24px; background:#fafafa;">
|
||||
<div style="font-weight:600; margin-bottom:6px;">
|
||||
已入库批次({{ (batchMap[props.row.requirementId] || []).length }} 批)
|
||||
</div>
|
||||
<el-table v-loading="batchLoading[props.row.requirementId]"
|
||||
:data="batchMap[props.row.requirementId] || []" size="mini" stripe>
|
||||
<el-table-column label="入库时间" prop="signTime" width="160">
|
||||
<template slot-scope="s">{{ parseTime(s.row.signTime, '{y}-{m}-{d} {h}:{i}') }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="入库单" prop="masterNum" width="160" />
|
||||
<el-table-column label="入库人" prop="signUser" width="100" />
|
||||
<el-table-column label="物料概览" prop="summary" min-width="240" show-overflow-tooltip />
|
||||
<el-table-column label="总数量" prop="totalQty" width="80" align="right" />
|
||||
<el-table-column label="总金额" width="110" align="right">
|
||||
<template slot-scope="s">¥{{ Number(s.row.totalAmount || 0).toFixed(2) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div v-if="(batchMap[props.row.requirementId] || []).length === 0
|
||||
&& !batchLoading[props.row.requirementId]"
|
||||
style="text-align:center; color:#909399; padding:12px 0;">
|
||||
暂无入库批次
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table v-loading="loading" :data="requirementsList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="需求标题" align="center" prop="title" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="需求方" align="center" prop="requesterNickName" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="负责人" align="center" prop="ownerNickName" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="关联项目" align="center" prop="projectName" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="采购物料" align="left" min-width="240">
|
||||
<template slot-scope="{ row }">
|
||||
<template v-if="row.materials && row.materials.length">
|
||||
<div v-for="m in row.materials" :key="m.id" class="mat-row">
|
||||
<span class="mat-name">{{ m.name }}<span v-if="m.model" class="mat-model"> / {{ m.model }}</span></span>
|
||||
<span class="mat-stock" :class="{ low: (m.inventory||0) <= 0 }">
|
||||
库存 {{ m.inventory == null ? 0 : m.inventory }}{{ m.unit || '' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<span v-else style="color:#c0c4cc;">未关联</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="需求描述" align="center" prop="description" min-width="200" show-overflow-tooltip>
|
||||
<template slot-scope="{ row }">
|
||||
<span v-if="row.description" class="copyable-text" @click="copyText(row.description)"
|
||||
@@ -129,6 +114,15 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 表格中的状态列(替换原有列) -->
|
||||
<el-table-column label="审批" align="center" prop="approvalStatus" width="90">
|
||||
<template slot-scope="{ row }">
|
||||
<el-tag v-if="row.approvalStatus == null" size="mini" type="info">未提交</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 0" size="mini" type="warning">待审</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 1" size="mini" type="success">通过</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 2" size="mini" type="danger">驳回</el-tag>
|
||||
<el-tag v-else-if="row.approvalStatus === 3" size="mini" type="info">撤回</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status" width="160">
|
||||
<template slot-scope="{ row }">
|
||||
<el-select v-model="row.status" size="mini" placeholder="选择状态"
|
||||
@@ -153,9 +147,6 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" icon="el-icon-truck" style="color:#409eff"
|
||||
v-if="scope.row.status !== 2 && scope.row.status !== 3"
|
||||
@click="handleGoToInbound(scope.row)">执行入库</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-check" @click="handleComplete(scope.row)"
|
||||
v-if="scope.row.status === 1">完成</el-button>
|
||||
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
|
||||
@@ -184,6 +175,25 @@
|
||||
<el-form-item label="关联项目" prop="projectId">
|
||||
<project-select v-model="form.projectId" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="采购物料" prop="materialIdArr">
|
||||
<div class="mat-trigger">
|
||||
<el-button size="mini" icon="el-icon-search" @click="openMaterialPicker">
|
||||
选择物料{{ selectedMaterials.length ? `(已选 ${selectedMaterials.length})` : '' }}
|
||||
</el-button>
|
||||
<span v-if="!selectedMaterials.length" class="mat-trigger-hint">可多选;留空也可保存,后续再关联</span>
|
||||
</div>
|
||||
<div v-if="selectedMaterials.length" class="mat-stock-panel">
|
||||
<div v-for="m in selectedMaterials" :key="m.id" class="mat-stock-row">
|
||||
<span class="mat-stock-name">{{ m.name }}<span v-if="m.model" style="color:#909399;"> / {{ m.model }}</span></span>
|
||||
<span class="mat-stock-tag" :class="{ low: (m.inventory||0) <= 0 }">
|
||||
库存 {{ m.inventory == null ? 0 : m.inventory }}{{ m.unit || '' }}
|
||||
</span>
|
||||
<span v-if="m.brand" class="mat-stock-meta">品牌:{{ m.brand }}</span>
|
||||
<span v-if="m.specifications" class="mat-stock-meta">规格:{{ m.specifications }}</span>
|
||||
<i class="el-icon-close mat-remove" title="移除" @click="removeSelectedMaterial(m.id)"></i>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="需求描述" prop="description">
|
||||
<el-input v-model="form.description" type="textarea" placeholder="请输入需求描述" />
|
||||
</el-form-item>
|
||||
@@ -205,6 +215,84 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 物料选择器 -->
|
||||
<el-dialog title="选择采购物料" :visible.sync="materialPickerOpen" width="780px"
|
||||
append-to-body :close-on-click-modal="false" @open="onPickerOpen">
|
||||
<div class="picker-toolbar">
|
||||
<el-input v-model="picker.kw" placeholder="搜索 名称 / 型号 / 品牌 / 规格"
|
||||
size="mini" clearable prefix-icon="el-icon-search"
|
||||
style="width: 320px;" @input="onPickerSearch" @clear="onPickerSearch" />
|
||||
<el-button size="mini" type="primary" plain icon="el-icon-plus"
|
||||
style="margin-left:8px;" @click="openInlineNew">未找到?新增物料</el-button>
|
||||
<span class="picker-tip" v-if="picker.tempSelected.length">
|
||||
已勾选 <b>{{ picker.tempSelected.length }}</b> 项
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 内嵌新增物料表单 -->
|
||||
<el-card v-if="newMat.show" shadow="never" class="new-mat-card">
|
||||
<div slot="header" class="new-mat-header">
|
||||
<span><i class="el-icon-plus" /> 新增物料到库存</span>
|
||||
<el-button type="text" size="mini" icon="el-icon-close" @click="newMat.show = false">收起</el-button>
|
||||
</div>
|
||||
<el-form :model="newMat" :rules="newMatRules" ref="newMatForm" size="mini"
|
||||
label-width="70px" :inline="true">
|
||||
<el-form-item label="名称" prop="name" required>
|
||||
<el-input v-model="newMat.name" placeholder="物料名称" style="width:180px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="型号" prop="model">
|
||||
<el-input v-model="newMat.model" placeholder="可选" style="width:140px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="规格" prop="specifications">
|
||||
<el-input v-model="newMat.specifications" placeholder="可选" style="width:140px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="品牌" prop="brand">
|
||||
<el-input v-model="newMat.brand" placeholder="可选" style="width:120px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="单位" prop="unit">
|
||||
<el-input v-model="newMat.unit" placeholder="个/箱/kg" style="width:100px;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="初始库存" prop="inventory" required>
|
||||
<el-input-number v-model="newMat.inventory" :min="0" :step="1" size="mini" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button size="mini" type="primary" :loading="newMat.saving" @click="submitNewMat">保存并选中</el-button>
|
||||
<el-button size="mini" @click="newMat.show = false">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-table :data="picker.list" v-loading="picker.loading" size="mini"
|
||||
ref="pickerTable" border highlight-current-row max-height="380"
|
||||
row-key="id" :row-class-name="pickerRowClass"
|
||||
@row-click="onPickerRowClick"
|
||||
@selection-change="onPickerSelectionChange">
|
||||
<el-table-column type="selection" width="42" reserve-selection />
|
||||
<el-table-column label="名称" prop="name" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column label="型号" prop="model" width="120" show-overflow-tooltip />
|
||||
<el-table-column label="规格" prop="specifications" width="110" show-overflow-tooltip />
|
||||
<el-table-column label="品牌" prop="brand" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="单位" prop="unit" width="60" align="center" />
|
||||
<el-table-column label="库存" width="90" align="right">
|
||||
<template slot-scope="s">
|
||||
<span :style="{color: (s.row.inventory||0) <= 0 ? '#f56c6c' : '#67c23a', fontWeight:600}">
|
||||
{{ s.row.inventory == null ? 0 : s.row.inventory }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="picker-pager" v-if="picker.total > picker.pageSize">
|
||||
<el-pagination small background layout="prev, pager, next, total"
|
||||
:total="picker.total" :page-size="picker.pageSize" :current-page.sync="picker.pageNum"
|
||||
@current-change="loadPickerList" />
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<el-button @click="materialPickerOpen = false">取 消</el-button>
|
||||
<el-button type="primary" @click="confirmPicker">确定({{ picker.tempSelected.length }})</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 需求详情对话框 -->
|
||||
<el-dialog title="需求详情" :visible.sync="detailDialog" width="600px" append-to-body>
|
||||
<el-descriptions :column="1" border>
|
||||
@@ -233,8 +321,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { addRequirements, delRequirements, getRequirements, getRequirementBatches, listRequirements, updateRequirements } from "@/api/oa/requirement";
|
||||
import { addRequirements, delRequirements, getRequirements, listRequirements, updateRequirements } from "@/api/oa/requirement";
|
||||
import { listUser } from "@/api/system/user";
|
||||
import { listOaWarehouse, getOaWarehouse, addOaWarehouse } from "@/api/oa/warehouse/oaWarehouse";
|
||||
import FilePreview from '@/components/FilePreview';
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
import ProjectSelect from "@/components/fad-service/ProjectSelect";
|
||||
@@ -244,9 +333,35 @@ export default {
|
||||
components: { FileUpload, FilePreview, ProjectSelect },
|
||||
data () {
|
||||
return {
|
||||
// 入库批次(按 requirementId 缓存)
|
||||
batchMap: {},
|
||||
batchLoading: {},
|
||||
// 当前已选物料明细(用于库存展示)
|
||||
selectedMaterials: [],
|
||||
// 物料选择器
|
||||
materialPickerOpen: false,
|
||||
picker: {
|
||||
kw: '',
|
||||
loading: false,
|
||||
list: [],
|
||||
total: 0,
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
tempSelected: [], // 选择器内部勾选项(含完整物料对象)
|
||||
searchTimer: null,
|
||||
},
|
||||
// 新增物料内嵌表单
|
||||
newMat: {
|
||||
show: false,
|
||||
saving: false,
|
||||
name: '',
|
||||
model: '',
|
||||
specifications: '',
|
||||
brand: '',
|
||||
unit: '',
|
||||
inventory: 0,
|
||||
},
|
||||
newMatRules: {
|
||||
name: [{ required: true, message: '物料名称不能为空', trigger: 'blur' }],
|
||||
inventory: [{ required: true, message: '初始库存不能为空', trigger: 'change' }],
|
||||
},
|
||||
// 按钮loading
|
||||
buttonLoading: false,
|
||||
// 遮罩层
|
||||
@@ -324,11 +439,120 @@ export default {
|
||||
this.refreshStat();
|
||||
},
|
||||
methods: {
|
||||
// 跳到入库明细页面,并预填该采购需求
|
||||
handleGoToInbound (row) {
|
||||
this.$router.push({
|
||||
path: '/step/in',
|
||||
query: { requirementId: String(row.requirementId), requirementTitle: row.title }
|
||||
// ====== 物料选择器 ======
|
||||
openMaterialPicker () {
|
||||
this.picker.kw = ''
|
||||
this.picker.pageNum = 1
|
||||
// 当前已选项作为初始勾选
|
||||
this.picker.tempSelected = this.selectedMaterials.slice()
|
||||
this.newMat.show = false
|
||||
this.materialPickerOpen = true
|
||||
},
|
||||
onPickerOpen () {
|
||||
this.loadPickerList().then(() => this.syncPickerSelectionUI())
|
||||
},
|
||||
loadPickerList () {
|
||||
this.picker.loading = true
|
||||
const params = {
|
||||
pageNum: this.picker.pageNum,
|
||||
pageSize: this.picker.pageSize,
|
||||
}
|
||||
if (this.picker.kw && this.picker.kw.trim()) params.name = this.picker.kw.trim()
|
||||
return listOaWarehouse(params).then(res => {
|
||||
this.picker.list = res.rows || []
|
||||
this.picker.total = res.total || 0
|
||||
this.$nextTick(() => this.syncPickerSelectionUI())
|
||||
}).finally(() => { this.picker.loading = false })
|
||||
},
|
||||
onPickerSearch () {
|
||||
clearTimeout(this.picker.searchTimer)
|
||||
this.picker.searchTimer = setTimeout(() => {
|
||||
this.picker.pageNum = 1
|
||||
this.loadPickerList()
|
||||
}, 250)
|
||||
},
|
||||
// 表格分页/刷新后把已勾选项重新打勾
|
||||
syncPickerSelectionUI () {
|
||||
const tbl = this.$refs.pickerTable
|
||||
if (!tbl) return
|
||||
tbl.clearSelection()
|
||||
const ids = new Set(this.picker.tempSelected.map(m => m.id))
|
||||
for (const row of this.picker.list) {
|
||||
if (ids.has(row.id)) tbl.toggleRowSelection(row, true)
|
||||
}
|
||||
},
|
||||
onPickerSelectionChange (rows) {
|
||||
// 当前页勾选 + 之前其它页保留下来的(不在 list 中的)
|
||||
const curIds = new Set(this.picker.list.map(m => m.id))
|
||||
const keepOther = this.picker.tempSelected.filter(m => !curIds.has(m.id))
|
||||
this.picker.tempSelected = keepOther.concat(rows)
|
||||
},
|
||||
onPickerRowClick (row) {
|
||||
const tbl = this.$refs.pickerTable
|
||||
if (!tbl) return
|
||||
const idx = this.picker.tempSelected.findIndex(m => m.id === row.id)
|
||||
tbl.toggleRowSelection(row, idx === -1)
|
||||
},
|
||||
pickerRowClass ({ row }) {
|
||||
return this.picker.tempSelected.find(m => m.id === row.id) ? 'picker-row-checked' : ''
|
||||
},
|
||||
confirmPicker () {
|
||||
this.selectedMaterials = this.picker.tempSelected.slice()
|
||||
this.form.materialIdArr = this.selectedMaterials.map(m => m.id)
|
||||
this.materialPickerOpen = false
|
||||
},
|
||||
removeSelectedMaterial (id) {
|
||||
this.selectedMaterials = this.selectedMaterials.filter(m => m.id !== id)
|
||||
this.form.materialIdArr = this.selectedMaterials.map(m => m.id)
|
||||
},
|
||||
// ====== 新增物料 ======
|
||||
openInlineNew () {
|
||||
this.newMat.show = true
|
||||
this.newMat.name = this.picker.kw || ''
|
||||
this.newMat.model = ''
|
||||
this.newMat.specifications = ''
|
||||
this.newMat.brand = ''
|
||||
this.newMat.unit = ''
|
||||
this.newMat.inventory = 0
|
||||
this.$nextTick(() => { this.$refs.newMatForm && this.$refs.newMatForm.clearValidate() })
|
||||
},
|
||||
submitNewMat () {
|
||||
this.$refs.newMatForm.validate(valid => {
|
||||
if (!valid) return
|
||||
this.newMat.saving = true
|
||||
const payload = {
|
||||
name: this.newMat.name.trim(),
|
||||
model: this.newMat.model || undefined,
|
||||
specifications: this.newMat.specifications || undefined,
|
||||
brand: this.newMat.brand || undefined,
|
||||
unit: this.newMat.unit || undefined,
|
||||
inventory: Number(this.newMat.inventory) || 0,
|
||||
}
|
||||
addOaWarehouse(payload).then(res => {
|
||||
// 后端返回的 data 可能是 id 或对象,统一兜底再 get 一次
|
||||
const newId = (res && (res.data && (res.data.id || typeof res.data === 'number'))) ? (res.data.id || res.data) : null
|
||||
const finish = (full) => {
|
||||
if (!full) return
|
||||
// 直接放进当前页顶端 + 勾选
|
||||
if (!this.picker.list.find(m => m.id === full.id)) {
|
||||
this.picker.list.unshift(full)
|
||||
}
|
||||
if (!this.picker.tempSelected.find(m => m.id === full.id)) {
|
||||
this.picker.tempSelected.push(full)
|
||||
}
|
||||
this.syncPickerSelectionUI()
|
||||
this.newMat.show = false
|
||||
this.$modal.msgSuccess('已新增并自动选中')
|
||||
}
|
||||
if (newId) {
|
||||
getOaWarehouse(newId).then(r => finish(r.data))
|
||||
} else {
|
||||
// 兜底:用刚提交的字段拼一个临时对象
|
||||
finish({ ...payload, id: Date.now() })
|
||||
// 重新查一下保证 id 真实
|
||||
this.loadPickerList()
|
||||
}
|
||||
}).finally(() => { this.newMat.saving = false })
|
||||
})
|
||||
},
|
||||
// 后端已联查 sys_oss 拼好字符串 "ossId|name|url,,ossId|name|url"
|
||||
@@ -368,18 +592,6 @@ export default {
|
||||
if (file && file.url) window.open(file.url, '_blank')
|
||||
}
|
||||
},
|
||||
// 展开行:加载该需求的入库批次
|
||||
onExpandChange (row, expanded) {
|
||||
if (!expanded || !expanded.length) return
|
||||
const id = row.requirementId
|
||||
if (this.batchMap[id]) return // 已有缓存
|
||||
this.$set(this.batchLoading, id, true)
|
||||
getRequirementBatches(id).then(res => {
|
||||
this.$set(this.batchMap, id, res.data || [])
|
||||
}).finally(() => {
|
||||
this.$set(this.batchLoading, id, false)
|
||||
})
|
||||
},
|
||||
async onStatusChange (row, newVal) {
|
||||
row._updating = true
|
||||
// 如果后端需要字符串,可改为 String(newVal)
|
||||
@@ -453,6 +665,8 @@ export default {
|
||||
requesterId: undefined,
|
||||
ownerId: undefined,
|
||||
projectId: undefined,
|
||||
materialIds: undefined,
|
||||
materialIdArr: [],
|
||||
description: undefined,
|
||||
deadline: undefined,
|
||||
status: 0,
|
||||
@@ -488,6 +702,7 @@ export default {
|
||||
handleAdd () {
|
||||
this.reset();
|
||||
this.form.requesterId = this.$store.state.user.id;
|
||||
this.selectedMaterials = [];
|
||||
this.open = true;
|
||||
this.title = "添加OA 需求";
|
||||
},
|
||||
@@ -504,10 +719,21 @@ export default {
|
||||
handleUpdate (row) {
|
||||
this.loading = true;
|
||||
this.reset();
|
||||
this.selectedMaterials = [];
|
||||
const requirementId = row.requirementId || this.ids
|
||||
getRequirements(requirementId).then(response => {
|
||||
this.loading = false;
|
||||
this.form = response.data;
|
||||
const data = response.data || {};
|
||||
const ids = (data.materialIds || '').split(',').map(s => Number(s)).filter(n => !isNaN(n) && n)
|
||||
data.materialIdArr = ids
|
||||
this.form = data;
|
||||
if (data.materials && data.materials.length) {
|
||||
this.selectedMaterials = data.materials.slice()
|
||||
} else if (ids.length) {
|
||||
// 兜底:逐个拉
|
||||
Promise.all(ids.map(id => getOaWarehouse(id).then(r => r.data).catch(() => null)))
|
||||
.then(list => { this.selectedMaterials = list.filter(Boolean) })
|
||||
}
|
||||
this.open = true;
|
||||
this.title = "修改OA 需求";
|
||||
});
|
||||
@@ -517,6 +743,8 @@ export default {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
this.buttonLoading = true;
|
||||
const arr = Array.isArray(this.form.materialIdArr) ? this.form.materialIdArr : []
|
||||
this.form.materialIds = arr.length ? arr.join(',') : null
|
||||
if (this.form.requirementId != null) {
|
||||
updateRequirements(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功");
|
||||
@@ -652,6 +880,67 @@ export default {
|
||||
cursor: pointer;
|
||||
&:hover { color: #409eff; text-decoration: underline; }
|
||||
}
|
||||
.mat-trigger {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
.mat-trigger-hint { font-size: 12px; color: #909399; }
|
||||
}
|
||||
.mat-remove {
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
color: #c0c4cc;
|
||||
&:hover { color: #f56c6c; }
|
||||
}
|
||||
.picker-toolbar {
|
||||
display: flex; align-items: center; margin-bottom: 10px;
|
||||
.picker-tip { margin-left: auto; font-size: 12px; color: #606266; }
|
||||
}
|
||||
.picker-pager { text-align: right; margin-top: 10px; }
|
||||
.new-mat-card {
|
||||
margin-bottom: 10px;
|
||||
::v-deep .el-card__header { padding: 6px 12px; background: #f5f7fa; }
|
||||
.new-mat-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
font-size: 13px; color: #303133;
|
||||
}
|
||||
}
|
||||
::v-deep .picker-row-checked > td { background: #ecf5ff !important; }
|
||||
.mat-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
& + .mat-row { border-top: 1px dashed #ebeef5; }
|
||||
.mat-name { flex: 1; color: #303133; }
|
||||
.mat-model { color: #909399; margin-left: 2px; }
|
||||
.mat-stock {
|
||||
flex-shrink: 0;
|
||||
padding: 0 6px;
|
||||
border-radius: 3px;
|
||||
background: #f0f9eb; color: #67c23a;
|
||||
&.low { background: #fef0f0; color: #f56c6c; }
|
||||
}
|
||||
}
|
||||
.mat-stock-panel {
|
||||
margin-top: 4px;
|
||||
padding: 6px 8px;
|
||||
background: #fafafa;
|
||||
border-radius: 3px;
|
||||
.mat-stock-row {
|
||||
display: flex; flex-wrap: wrap; gap: 6px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
& + .mat-stock-row { border-top: 1px dashed #ebeef5; }
|
||||
}
|
||||
.mat-stock-name { flex: 1; min-width: 120px; color: #303133; }
|
||||
.mat-stock-tag {
|
||||
padding: 0 6px; border-radius: 3px;
|
||||
background: #f0f9eb; color: #67c23a;
|
||||
&.low { background: #fef0f0; color: #f56c6c; }
|
||||
}
|
||||
.mat-stock-meta { color: #909399; }
|
||||
}
|
||||
.accessory-link {
|
||||
display: inline-block;
|
||||
max-width: 160px;
|
||||
|
||||
Reference in New Issue
Block a user