You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

764 lines
23 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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 style="margin-bottom: 10px;">
<el-button type="primary" @click="execScript('write')">执行编辑js</el-button>
<el-button type="primary" @click="execScript('read')">执行查看js</el-button>
</div>
<div class="form-container" id="print-content">
<el-form :model="flowLinkForm" label-position="right" label-width="130px">
<el-form-item label="out_pay_id">
<el-input v-model="flowLinkForm.out_pay_id"></el-input>
</el-form-item>
<el-form-item label="out_away_id">
<el-input v-model="flowLinkForm.out_away_id"></el-input>
</el-form-item>
<el-form-item label="out_contract_id">
<el-input v-model="flowLinkForm.out_contract_id"></el-input>
</el-form-item>
</el-form>
<DesktopForm
:device="device"
ref="desktopForm"
:config="config"
:is-first-node="isFirstNode"
:sub-form="subConfig"
:fields="fields"
:original-form="form"
:readable="readableFields"
:writeable="writeableFields"
:rules="{}"
:sub-rules="{}"
:script-content="scriptContent"
:logs="config.logs"
></DesktopForm>
</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"],
]),
flowLinkForm: {
out_pay_id: '',
out_away_id: '',
out_contract_id: ''
},
form: {},
result: {},
fileList: {},
subFileList: {},
rules: {},
subRules: {},
flows: [],
csrf_token: '',
scriptContent: '',
};
},
methods: {
execScript(mode='write') {
if (mode === 'write') {
this.scriptContent = this.writeScriptContent
} else if (mode === 'read') {
this.scriptContent = this.readScriptContent
}
},
// 处理url中default_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)",
});
if (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;
this.flowLinkForm.out_contract_id = res?.flow?.out_contract_id;
this.flowLinkForm.out_away_id = res?.flow?.out_away_id;
this.flowLinkForm.out_pay_id = res?.flow?.out_pay_id;
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();
}
}
},
async submit(type) {
if (window.$_uploading) {
this.$message.warning("文件正在上传中")
return
}
let copyForm;
try {
await this.$refs['desktopForm'].validate()
} catch (err) {
console.warn(err)
this.$message.warning('数据校验失败')
return
}
copyForm = deepCopy(this.$refs["desktopForm"].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]
}
}
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,
Object.assign(copyForm, this.flowLinkForm)
);
this.result = flow;
this.$message.success("操作成功")
}
} 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.fields?.map(i => i.id) || [];
},
node() {
return this.config?.currentNode || {};
},
writeScriptContent() {
return this.config?.customModel?.js;
},
readScriptContent() {
return this.config?.customModel?.view_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' || this.$route.path === '/flow/edit'
}
},
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>