xy 11 months ago
parent 032e35e41c
commit 451c3b9120

@ -52,7 +52,14 @@ export function deal(flow_id,data) {
data
})
}
// 编辑
export function save(flow_id, data) {
return request({
method: 'post',
url: `/api/oa/flow/save/${flow_id}`,
data
})
}
export function getNextNode(params,isLoading=false) {
return request({
method: 'get',

@ -81,7 +81,7 @@ export const constantRoutes = [
name: "detail",
component: () => import("@/views/flow/create"),
hidden: true,
},
}
],
},

@ -0,0 +1,860 @@
<template>
<div class="container">
<el-card
:shadow="device === 'desktop' ? 'always' : 'never'"
class="card"
:style="{
border: device === 'desktop' ? '' : 'none',
background: device === 'desktop' ? '' : '#f7f8fa',
'min-height': device === 'desktop' ? '' : '100vh',
}"
>
<template #header>
<p>{{ config.customModel ? config.customModel.name : "办理" }}</p>
</template>
<template>
<div class="steps" v-if="!/\/detail/.test($route.path)">
<el-steps :space="120" finish-status="success" align-center>
<template v-if="!isFirstNode">
<el-step
v-for="step in config.logs"
:title="step.node.name"
:status="step.status !== -1 ? 'success' : 'error'"
icon="el-icon-circle-check"
></el-step>
</template>
<el-step
:title="node.name"
status="finish"
icon="el-icon-edit"
></el-step>
<el-step
icon="el-icon-right"
status="wait"
>
<template #title>
<div style="max-width: 180px;overflow: hidden;white-space: nowrap;text-overflow: ellipsis;">
<span v-for="(nextNode, index) in ((node.nextNodes && node.nextNodes instanceof Array) ? node.nextNodes : [])">{{ index === 0 ? '' : ',' }}{{ nextNode.name }}</span>
</div>
<div v-if="node.nextNodes">{{ node.nextNodes.length }}</div>
</template>
</el-step>
</el-steps>
<el-divider></el-divider>
</div>
<div class="form-container" id="print-content">
<template v-if="device === 'desktop'">
<DesktopForm
:device="device"
ref="desktopForm"
:config="config"
:is-first-node="isFirstNode"
:sub-form="subConfig"
:fields="fields"
:original-form="form"
:readable="readableFields"
:script-content="scriptContent"
:writeable="writeableFields"
:rules="rules"
:sub-rules="subRules"
:logs="config.logs"
></DesktopForm>
</template>
<template v-else>
<MobileForm
:device="device"
ref="mobileForm"
:config="config"
:is-first-node="isFirstNode"
:sub-form="subConfig"
:fields="fields"
:original-form="form"
:readable="readableFields"
:script-content="scriptContent"
:writeable="writeableFields"
:rules="rules"
:sub-rules="subRules"
:logs="config.logs"
></MobileForm>
</template>
</div>
</template>
<!-- 审批日志-->
<div v-if="/\/detail/.test($route.path)" style="margin-top: 10px">
<div>流转记录</div>
<vxe-table
style="margin-top: 10px;"
show-footer
ref="table"
stripe
class="log-table-scroll"
keep-source
show-overflow
:column-config="{ resizable: true }"
:print-config="{}"
:export-config="{}"
:custom-config="{ mode: 'popup' }"
:footer-data="footerData"
:data="config.logs || []"
@cell-dblclick="cellDblclickEvent"
>
<vxe-column
type="seq"
width="62"
align="center"
field="seq"
title="编号"
/>
<vxe-column
width="140"
title="节点名称"
align="center"
field="node.name"
:formatter="({ cellValue }) => cellValue || '节点已调整'"
></vxe-column>
<vxe-column
width="80"
align="center"
title="办理状态"
field="status"
:formatter="({ cellValue }) => myStatus.get(cellValue)"
>
<template #default="{ row }">
<el-tag
size="mini"
:type="statusColor.get(row.status)"
effect="dark"
>{{ myStatus.get(row.status) }}</el-tag
>
</template>
</vxe-column>
<vxe-column
align="center"
width="80"
title="承办人员"
field="user.name"
></vxe-column>
<vxe-column
align="center"
width="200"
title="流转时间"
field="created_at"
:formatter="
({ cellValue }) =>
$moment(cellValue).format('YYYY年MM月DD日 HH:mm:ss')
"
></vxe-column>
<vxe-column
min-width="200"
header-align="center"
title="退回原因"
field="reason"
></vxe-column>
<vxe-column
align="center"
width="200"
title="办理时间"
field="updated_at"
>
<template #default="{ row }">
<span
:style="{
color:
row.deadline &&
row.updated_at &&
$moment(row.updated_at).isAfter(
$moment(row.deadline).endOf('day')
)
? 'red'
: '',
}"
>{{
$moment(row.updated_at).format("YYYY年MM月DD日 HH:mm:ss")
}}</span
>
</template>
</vxe-column>
<vxe-column align="center" title="耗时" field="use_time" width="120">
<template #default="{ row }">
<span>{{ diffTime(row.updated_at, row.created_at) }}</span>
</template>
</vxe-column>
</vxe-table>
</div>
<div class="btns" ref="btns">
<el-button
v-if="$route.query.flow_id"
icon="el-icon-document-add"
type="primary"
size="small"
@click="submit()"
>确认</el-button
>
</div>
</el-card>
<el-backtop></el-backtop>
</div>
</template>
<script>
import DesktopForm from "./DesktopForm.vue";
import MobileForm from "./MobileForm.vue";
import assign from "./components/assign.vue";
import { generateRandomString } from '@/utils'
import {
create,
deal,
fieldConfig, flowList,
preConfig,
preDeal,
updateNodeTime,
view, save
} from "@/api/flow";
import { deepCopy } from "@/utils";
import { validation, validationName } from "@/utils/validate";
import { print } from "@/utils/print";
export default {
components: {
DesktopForm,
MobileForm,
},
data() {
return {
printKey: 0,
isShowRollback: false,
isShowForward: false,
isShowAssign: false,
info: [],
config: {},
subConfig: new Map(),
myStatus: new Map([
[-2, "会签退回"],
[-1, "退回"],
[0, "办理中"],
[1, "已完成"],
]),
statusColor: new Map([
[-2, "warning"],
[-1, "warning"],
[0, ""],
[1, "success"],
]),
form: {},
result: {},
fileList: {},
subFileList: {},
rules: {},
subRules: {},
flows: [],
csrf_token: '',
};
},
methods: {
// urldefault_json
handleDefaultJSON() {
try {
if(!this.$route.query?.default_json) return
const res = JSON.parse(this.$route.query?.default_json)
console.log('default_json', res)
for (let key in this.$route.query) {
if(/^out_(.*)_id/.test(key)) {
this.form[key] = this.$route.query[key]
}
}
for (let key in res) {
try {
let jsonObj = JSON.parse(res[key]);
if (this.form.hasOwnProperty(key)) {
this.form[key] = jsonObj;
}
} catch (err) {
if (this.form.hasOwnProperty(key)) {
this.form[key] = res[key];
}
}
}
} catch (err) {
console.error(err)
}
},
async print(isLog=false) {
const _this = this
let customModelId = this.config.customModel.id || this.$route.query.module_id
const modelRes = await fieldConfig(customModelId,true)
let pickTemplate = 0
const printTemplates = [{
id: 0,
name: '基础模版',
print_format: modelRes.customModel.print_format
},...modelRes.customModel.print_formats]
const h = this.$createElement;
await this.$msgbox({
title: '打印模版选择',
message: h('div',{
class: 'print-template-radios',
key: this.printKey++
},[
h('div',{
},printTemplates.map(i => h('div',{
style: {
display: 'flex',
'align-items': 'center',
'margin-top': '4px',
}
},[
h('span',{
class: 'el-radio__input'
},[
h('span', {
class: 'el-radio__inner custom-cursor-on-hover'
}),
h('input', {
style: {
cursor: 'pointer',
opacity: 0,
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
margin: 0
},
attrs: {
//
type: "radio",
id: `print-radio-${i.id}`,
name: "Radio",
value: i.id,
checked: pickTemplate === i.id,
},
on: {
change: () => {
pickTemplate = i.id
}
}
})
]),
h('label', {
style: {
flex: 1
},
attrs: {
for: `print-radio-${i.id}`
},
class: 'el-radio__label',
}, i.name)
])))
]),
showCancelButton: true,
confirmButtonText: '确定',
cancelButtonText: '取消',
})
const printText = printTemplates.find(i => i.id === pickTemplate)?.print_format
if(isLog) {
const res = await this.$refs['table'].exportData({
type: 'html',
download: false
})
await print.bind(this)(printText, isLog, _this.config.flow, res.content)
} else {
await print.bind(this)(printText, isLog, _this.config.flow)
}
},
generateForm(object, fields, relation = false, pname) {
fields.forEach((field) => {
if (field.rules && field.rules.length > 0 && this.writeableFields.find(i => i === field.id) && !relation) {
this.rules[field.name] = field.rules.map((rule) => {
switch (rule) {
case "required":
if (field.type === 'relation') {
return {
validator: (myRule, value, callback) => {
if (value instanceof Array && value.length > 0) {
callback()
} else {
callback(`请填写${field.label}`)
}
},
message: `请填写${field.label}`,
trigger: "blur",
};
} else {
return {
required: true,
message: `请填写${field.label}`,
trigger: "blur",
};
}
default:
return {
validator: (myRule, value, callback) => {
if (validation.get(rule).test(value) || value === '') {
callback();
} else {
callback(
new Error(
`${field.label}必须为${validationName.get(rule)}`
)
);
}
},
trigger: "blur",
pattern: validation.get(rule),
message: `${field.label}必须为${validationName.get(rule)}`,
};
}
});
}
if (relation) {
this.subRules[`${pname}_rules`][field.name] = field.rules.map((rule) => {
switch (rule) {
case "required":
return {
required: true,
message: `请填写${field.label}`,
};
default:
return {
validator: ({ cellValue }) => {
return new Promise((resolve, reject) => {
if (validation.get(rule).test(cellValue) || cellValue === '') {
resolve()
} else {
reject(
new Error(
`${field.label}必须为${validationName.get(rule)}`
)
);
}
})
},
trigger: "blur",
pattern: validation.get(rule),
message: `${field.label}必须为${validationName.get(rule)}`,
};
}
});
}
if (field.type === "relation") {
this.subRules[`${field.name}_rules`] = {}
object[field.name] = [{}];
this.generateForm(
object[field.name][0],
this.subConfig.get(field.sub_custom_model_id)?.customModel?.fields,
true,
field.name
);
} else {
if (/\/detail/.test(this.$route.path) && this.$route.query.flow_id) {
object[field.name] = "";
} else {
if (this.writeableFields.indexOf(field.id) !== -1 || this.readableFields.indexOf(field.id) !== -1) {
object[field.name] = (this.writeableFields.indexOf(field.id) !== -1 && field.default_value) ? field.default_value : (field.type === 'file' ? [] : "");
}
}
}
});
this.form['flow_title'] = this.config?.flow?.title ?? `${this.config.customModel.name}${this.$store.getters.name} ${this.$moment().format('YYYY-MM-DD HH:mm')}`
},
formatTime(time) {
const days = parseInt(time / (1000 * 60 * 60 * 24));
const hours = parseInt((time % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const minutes = parseInt((time % (1000 * 60 * 60)) / (1000 * 60));
const seconds = (time % (1000 * 60)) / 1000;
return `${days > 0 ? days + "天" : ""}${
hours > 0 ? hours + "时" : ""
}${minutes}分${seconds}`;
},
async getConfig() {
const loading = this.$loading({
lock: true,
text: "拼命加载中",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.8)",
});
// detail
if (/\/detail/.test(this.$route.path) && this.$route.query.flow_id) {
try {
const res = await view(this.$route.query.flow_id);
const { fields } = res?.customModel;
let subFormRequest = [];
const getSubForm = (id) => {
subFormRequest.push(fieldConfig(id));
};
fields.forEach((field) => {
if (field.sub_custom_model_id) {
getSubForm(field.sub_custom_model_id);
}
});
const subConfigs = await Promise.all(subFormRequest);
subConfigs.forEach((sub) => {
if (sub.customModel?.id) {
this.subConfig.set(sub.customModel?.id, sub);
}
});
this.config = res;
this.generateForm(this.form, fields);
this.form = Object.assign({}, this.form);
const { data } = res?.flow;
for (let key in data) {
try {
let jsonObj = JSON.parse(data[key]);
jsonObj.forEach(item => {
//
for (const key in item) {
if (typeof item[key] === 'string') {
try {
// JSON
const parsedValue = JSON.parse(item[key]);
//
item[key] = parsedValue;
} catch (e) {
//
}
}
}
})
if (this.form.hasOwnProperty(key)) {
this.form[key] = jsonObj;
}
} catch (err) {
if (this.form.hasOwnProperty(key)) {
if (data[key] instanceof Array) {
if (data[key].length > 0 && data[key][0].hasOwnProperty('url')) {
this.form[key] = data[key].map(i => ({
name: i.original_name,
url: i.url,
response: i,
TYPE_FILE: 1
}))
} else {
this.form[key] = ''
}
}
this.form[key] = data[key];
}
}
}
loading.close();
} catch (err) {
console.error(err);
this.$message.error("配置失败");
loading.close();
}
} else if (!this.$route.query.flow_id) {
//
try {
this.csrf_token = generateRandomString()
const res = await preConfig(this.$route.query.module_id);
const { fields } = res?.customModel;
let subFormRequest = [];
const getSubForm = (id) => {
subFormRequest.push(fieldConfig(id));
};
fields.forEach((field) => {
if (field.sub_custom_model_id) {
getSubForm(field.sub_custom_model_id);
}
});
const subConfigs = await Promise.all(subFormRequest);
subConfigs.forEach((sub) => {
if (sub.customModel?.id) {
this.subConfig.set(sub.customModel?.id, sub);
}
});
this.config = res;
this.generateForm(this.form, fields);
this.handleDefaultJSON();
this.form = Object.assign({}, this.form);
loading.close();
} catch (err) {
console.error(err);
this.$message.error("配置失败");
loading.close();
}
} else {
//
try {
const res = await preDeal(this.$route.query.flow_id);
const { fields } = res?.customModel;
let subFormRequest = [];
const getSubForm = (id) => {
subFormRequest.push(fieldConfig(id));
};
fields.forEach((field) => {
if (field.sub_custom_model_id) {
getSubForm(field.sub_custom_model_id);
}
});
const subConfigs = await Promise.all(subFormRequest);
subConfigs.forEach((sub) => {
if (sub.customModel?.id) {
this.subConfig.set(sub.customModel?.id, sub);
}
});
this.config = res;
this.generateForm(this.form, fields);
this.handleDefaultJSON();
const { data } = res?.flow;
for (let key in data) {
try {
let jsonObj = JSON.parse(data[key]);
jsonObj.forEach(item => {
//
for (const key in item) {
if (typeof item[key] === 'string') {
try {
// JSON
const parsedValue = JSON.parse(item[key]);
//
item[key] = parsedValue;
} catch (e) {
//
}
}
}
})
if (this.form.hasOwnProperty(key)) {
this.form[key] = jsonObj;
}
} catch (err) {
if (this.form.hasOwnProperty(key)) {
if (data[key] instanceof Array) {
if (data[key].length > 0) {
this.form[key] = data[key];
} else {
this.form[key] = ''
}
}
if (data[key] && data[key] !== 'null' && data[key] !== 'undefined') {
this.form[key] = data[key];
}
}
}
}
this.form = Object.assign({}, this.form);
loading.close();
} catch (err) {
console.error(err);
this.$message.error("配置失败");
loading.close();
}
}
},
async submit(type) {
if (window.$_uploading) {
this.$message.warning("文件正在上传中")
return
}
let copyForm;
if (this.device === "desktop") {
try {
await this.$refs['desktopForm'].validate()
} catch (err) {
console.warn(err)
this.$message.warning('数据校验失败')
return
}
copyForm = deepCopy(this.$refs["desktopForm"].form);
} else {
try {
await this.$refs['mobileForm'].validate()
} catch (err) {
console.warn(err)
this.$message.warning('数据校验失败')
return
}
copyForm = deepCopy(this.$refs["mobileForm"].form);
}
const uploadHandler = (form) => {
let keys = Object.keys(form)
keys.forEach(key => {
if (form[key] instanceof Array) {
if (form[key].length > 0) {
if (form[key][0].hasOwnProperty('response')) {
form[key] = form[key].map(i => i.response)
} else {
form[key].forEach(i => {
uploadHandler(i)
})
}
} else {
form[key] = ''
}
} else {
if (form[key] === 'null' || form[key] === 'undefined') {
form[key] = ''
}
}
})
}
uploadHandler(copyForm)
for (let key in copyForm) {
let myField = this.fields.find(i => i.name === key)
if (myField && this.writeableFields.indexOf(myField.id) === -1) {
delete copyForm[key]
}
}
copyForm["current_node_id"] = this.config.currentNode.id;
try {
if (this.$route.query.flow_id) {
copyForm.id = this.$route.query.flow_id;
const { flow, is_last_handled_log } = await save(
this.$route.query.flow_id,
copyForm
);
this.result = flow;
}
} catch (err) {
console.error(err);
}
},
async cellDblclickEvent({ row, column }) {
// if(this.$store.state.user.username !== 'admin') return
if(column.field === 'created_at' || column.field === 'updated_at') {
this.$prompt('请输入时间', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^(?:19|20)[0-9][0-9]-(?:(?:0[1-9])|(?:1[0-2]))-(?:(?:[0-2][1-9])|(?:[1-3][0-1])) (?:(?:[0-2][0-3])|(?:[0-1][0-9])):[0-5][0-9]:[0-5][0-9]$/,
inputErrorMessage: '时间格式不正确'
}).then(({ value }) => {
updateNodeTime({
id: row.id,
date: value,
date_type: column.field
}).then(_ => {
this.$message.success('更新成功')
this.getConfig()
})
})
}
},
},
computed: {
device() {
return this.$store.state.app.device;
},
fields() {
return this.config?.customModel?.fields || [];
},
readableFields() {
return /\/detail/.test(this.$route.path)
? this.fields?.map((i) => i.id)
: this.config?.currentNode?.readable || [];
},
writeableFields() {
return this.config?.currentNode?.writeable || [];
},
node() {
return this.config?.currentNode || {};
},
scriptContent() {
if (this.config?.customModel?.view_js && this.$route.query.flow_id && /\/detail/.test(this.$route.path)) {
return this.config?.customModel?.view_js;
} else if (this.config?.customModel?.js && !/\/detail/.test(this.$route.path)) {
return this.config?.customModel?.js;
}
},
diffTime() {
return function (end, start) {
const diff = this.$moment(end).diff(this.$moment(start));
return this.formatTime(diff);
};
},
footerData() {
const diff = this.$moment(this.config?.logs?.at(-1)?.updated_at).diff(
this.$moment(this.config?.logs?.at(0)?.created_at)
);
return [
{
seq: "总耗时",
use_time: this.formatTime(diff),
},
];
},
isFirstNode() {
return this.config?.logs?.length === 0 || this.config?.currentNode?.category === 'start'
}
},
created() {
this.getConfig();
},
mounted() {},
};
</script>
<style scoped lang="scss">
::v-deep .el-step__title {
font-size: 14px;
line-height: 1.5;
}
::v-deep .el-step__icon.is-icon {
border-radius: 100%;
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
width: 36px;
height: 36px;
border: solid 2px;
}
::v-deep .el-step.is-center .el-step__line {
top: 50%;
}
::v-deep .el-card__header {
}
.container {
padding: 20px;
.card {
position: relative;
}
.btns {
display: flex;
justify-content: center;
margin-top: 10px;
flex-wrap: wrap;
}
}
.form-container {
}
@media (max-width: 768px) {
.container {
padding: 0;
}
.btns {
justify-content: space-evenly;
& > * {
margin: 4px 6px;
}
}
::v-deep .el-steps--horizontal {
display: flex;
flex-wrap: wrap;
}
}
</style>
<style lang="scss">
.log-table-scroll {
::-webkit-scrollbar {
height: 0;
}
}
.print-template-radios .el-radio__input:has(input[type=radio]:checked) .el-radio__inner {
border-color: var(--theme-color);
background: var(--theme-color);
&::after {
transform: translate(-50%,-50%) scale(1);
}
}
</style>
Loading…
Cancel
Save