---初始化后台管理web页面项目

This commit is contained in:
2025-08-20 14:39:30 +08:00
parent ad49711a7e
commit 87545a8baf
2057 changed files with 282864 additions and 213 deletions

View File

@ -0,0 +1,382 @@
<template>
<!-- 表单信息 -->
<div class="form-container">
<div class="box">
<div :style="{}" class="form-right">
<div v-for="(item, index) in forms.configs" :key="index" :tab="item.formName">
<div v-show="activeIndex == index">
<!-- 如果是生成文件模式 formType = 0也就是FormType.SYSTEM会加载对应项目的Form.vue-->
<SystemForm v-if="item.formType == FormType.SYSTEM" :ref="setItemRef" :formModel="item.formModel" :isViewProcess="props.disabled" :systemComponent="item.systemComponent" :workflowConfig="item" class="form-box" />
<SimpleForm v-else-if="item.formType == FormType.CUSTOM" :ref="setItemRef" :formModel="item.formModel" :formProps="item.formProps" :isWorkFlow="true" class="form-box" />
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import SimpleForm from '/@/components/SimpleForm/src/SimpleForm.vue';
import { onBeforeUpdate, nextTick, onMounted, reactive, ref } from 'vue';
import { TaskApproveOpinion, ValidateForms } from '/@/model/workflow/bpmnConfig';
import { cloneDeep } from 'lodash-es';
import { GeneratorConfig } from '/@/model/generator/generatorConfig';
import { FormEventColumnConfig } from '/@/model/generator/formEventConfig';
import { changeFormJson } from '/@/hooks/web/useWorkFlowForm';
import { SystemForm } from '/@/components/SystemForm/index';
import { FormType } from '/@/enums/workflowEnum';
import { createFormEvent, loadFormEvent, submitFormEvent } from '/@/hooks/web/useFormEvent';
const props = withDefaults(
defineProps<{
disabled: boolean | undefined;
formInfos: Array<any>;
opinions?: Array<TaskApproveOpinion> | undefined;
opinionsComponents?: Array<string> | undefined;
formAssignmentData?: null | Recordable;
}>(),
{
disabled: false,
formInfos: () => {
return [];
}
}
);
const emits = defineEmits(['getFormConfigs']);
let uploadComponent: { ids: Array<string> } = reactive({ ids: [] });
let activeIndex = ref(0);
let itemRefs = ref([]) as any;
const setItemRef = (el: never) => {
itemRefs.value.push(el);
};
onBeforeUpdate(() => {
itemRefs.value = [];
});
let forms: {
formModels: Array<Recordable>;
configs: Array<{
formName: string;
formProps: {};
formModel: Recordable;
formKey: string;
validate: boolean;
formType: FormType;
workflowPermissions?: Array<any>;
opinions?: Array<any>;
opinionsComponents?: Array<any>;
systemComponent?: {
functionalModule: string;
functionName: string;
functionFormName: string;
};
formJson?: Array<any>;
isOldSystem?: boolean;
}>;
formEventConfigs: FormEventColumnConfig[];
} = reactive({
formModels: [],
configs: [],
formEventConfigs: []
});
onMounted(async () => {
for await (let element of props.formInfos) {
let formModels = {};
if (element.formData) {
formModels = cloneDeep(element.formData);
}
// 参数赋值[赋值权限最大]
if (props.formAssignmentData) {
if (props.formAssignmentData[element.formConfig.formId]) {
formModels = { ...formModels, ...props.formAssignmentData[element.formConfig.formId] };
}
}
forms.formModels.push(formModels);
// 系统表单
if (element.formType == FormType.SYSTEM) {
forms.configs.push({
formName: element.formConfig.formName,
formProps: {},
formModel: formModels,
formKey: element.formConfig.key,
validate: true,
formType: element.formType,
workflowPermissions: element.formConfig.children,
opinions: props.opinions,
opinionsComponents: props.opinionsComponents,
systemComponent: {
functionalModule: element.functionalModule,
functionName: element.functionName,
functionFormName: 'Form'
},
formJson: element.formJson,
isOldSystem: false
});
// 上传组件Id集合
setTimeout(() => {
getSystemUploadComponentIds();
}, 0);
} else {
const model = JSON.parse(element.formJson) as GeneratorConfig;
const { formJson, formEventConfig } = model;
if (formEventConfig) {
forms.formEventConfigs.push(formEventConfig);
//初始化表单
await createFormEvent(formEventConfig, formModels, true);
//加载表单
await loadFormEvent(formEventConfig, formModels, true);
//TODO 暂不放开 工作流没有获取表单数据这个步骤 获取表单数据
// getFormDataEvent(formEventConfig, formModels,true);
}
let formKey = element.formConfig.key;
let config = {
formName: element.formConfig.formName,
formProps: {},
formModel: {},
formKey,
validate: true,
formType: element.formType
};
let isViewProcess = props.disabled;
let { buildOptionJson, uploadComponentIds } = changeFormJson(
{
formJson,
formConfigChildren: element.formConfig.children,
formConfigKey: element.formConfig.key,
opinions: props.opinions,
opinionsComponents: props.opinionsComponents
},
isViewProcess,
uploadComponent.ids
);
uploadComponent.ids = uploadComponentIds;
if (buildOptionJson.schemas) {
config.formProps = buildOptionJson;
forms.configs.push(config);
}
}
// });
}
await nextTick();
setTimeout(() => {
setFormModel();
}, 0);
emits('getFormConfigs', forms.configs.length ? forms.configs[activeIndex.value] : null);
});
function setFormModel() {
for (let index = 0; index < itemRefs.value.length; index++) {
if (itemRefs.value[index]) {
itemRefs.value[index].setFieldsValue(forms.formModels[index]);
}
}
}
async function setFormData(formData) {
await nextTick();
forms.formModels = formData;
setFormModel();
}
function getSystemUploadComponentIds() {
for (let index = 0; index < itemRefs.value.length; index++) {
if (itemRefs.value[index] && itemRefs.value[index].getUploadComponentIds) {
let ids = itemRefs.value[index].getUploadComponentIds();
uploadComponent.ids = [...uploadComponent.ids, ...ids];
}
}
}
// 获取上传组件的字段值集合
function getUploadComponentIds() {
return uploadComponent.ids;
}
async function validateForm() {
let validateForms: ValidateForms = [];
for (let index = 0; index < itemRefs.value.length; index++) {
if (itemRefs.value[index]) {
try {
await itemRefs.value[index]?.validate();
validateForms.push({
validate: true,
msgs: [],
isOldSystem: forms.configs[index].isOldSystem
});
forms.configs[index].validate = true;
} catch (error: any | Array<{ errors: Array<string>; name: Array<string> }>) {
validateForms.push({
validate: false,
msgs: error?.errorFields
});
forms.configs[index].validate = false;
}
}
}
return validateForms;
}
async function saveDraftData() {
let formModes = {};
for (let index = 0; index < forms.configs.length; index++) {
const ele = forms.configs[index];
if (ele.formType == FormType.SYSTEM) {
let values = await itemRefs.value[index].validate();
formModes[ele.formKey] = values;
} else {
formModes[ele.formKey] = ele.formModel;
}
}
return formModes;
}
async function getFormModels(saveRowKey) {
let formModes = {};
for (let index = 0; index < forms.configs.length; index++) {
const ele = forms.configs[index];
if (ele.formType == FormType.SYSTEM) {
let values = await itemRefs.value[index].workflowSubmit(saveRowKey);
formModes[ele.formKey] = values;
} else {
formModes[ele.formKey] = ele.formModel;
}
}
// forms.configs.forEach((ele) => {
// formModes[ele.formKey] = ele.formModel;
// });
forms.formEventConfigs.forEach(async (ele, i) => {
//此组件 获取数据 就是为了提交表单 所以 表单提交数据 事件 就此处执行
await submitFormEvent(ele, forms.configs[i]?.formModel);
});
return formModes;
}
function getSystemType() {
let system = {};
for (let index = 0; index < forms.configs.length; index++) {
const ele = forms.configs[index];
if (ele.formType == FormType.SYSTEM) {
system[ele.formKey] = itemRefs.value[index].getIsOldSystem();
}
}
return system;
}
defineExpose({
validateForm,
getFormModels,
saveDraftData,
setFormData,
getUploadComponentIds,
getSystemType
});
</script>
<style lang="less" scoped>
.form-container {
display: flex;
margin-top: -10px;
}
.box {
width: 100%;
.form-left {
float: left;
height: 100vh;
box-shadow: 5px 5px 5px rgb(0 0 0 / 10%);
z-index: 9998;
.resize-shrink-sidebar {
cursor: col-resize;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
bottom: 0;
right: 0;
z-index: 9999;
.shrink-sidebar-text {
padding: 0 2px;
background: #f2f2f2;
border-radius: 10px;
}
}
.left-box {
margin-right: 10px;
border-right: 1px solid #f0f0f0;
height: 80vh;
}
span {
font-size: 16px;
font-weight: 500;
padding-left: 4px;
}
.form-name {
height: 36px;
display: flex;
align-items: center;
font-size: 14px;
cursor: pointer;
color: rgb(102 102 102 / 99.6%);
margin-right: -2px;
padding-left: 4px;
}
.actived {
border-right: 1px solid #5e95ff;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: transparent;
margin-right: 4px;
}
.validate {
background-color: @clear-color;
}
.icon-box {
font-size: 16px;
margin-right: 4px;
}
.left-title {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
padding: 10px 4px 10px 0;
top: 0;
left: 0;
.in-or-out {
position: absolute;
right: 10px;
}
}
}
.form-right {
padding-top: 20px;
}
}
</style>

View File

@ -0,0 +1,318 @@
<template>
<div :class="prefixCls" class="login-box relative w-full h-full" v-if="!isSingleLogin">
<div class="center-box">
<div class="login-left-title">
<div class="sub-title">{{ sysName }}</div>
</div>
<div :class="`${prefixCls}-form`">
<LoginForm />
</div>
</div>
</div>
<div style="width: 100%; height: 300px; display: flex;justify-content: center;align-items: center;" v-else>
<a-spin />
</div>
</template>
<script lang="ts" setup>
import LoginForm from './LoginForm.vue';
import { getAppEnvConfig } from '/@/utils/env';
import { useUserStore } from '/@/store/modules/user';
import { useDesign } from '/@/hooks/web/useDesign';
import { useAppStore } from '/@/store/modules/app';
import { getLogoInfo } from '/@/api/system/login';
import { onMounted } from 'vue';
import { ref } from 'vue';
import { LogoConfig } from '/#/config';
import { useRouter } from 'vue-router';
import { useMessage } from '/@/hooks/web/useMessage';
import { useI18n } from '/@/hooks/web/useI18n';
const { currentRoute } = useRouter();
const userStore = useUserStore();
const isSingleLogin = ref(false)
const { notification } = useMessage();
const { t } = useI18n();
onMounted(async () => {
const fullPath = currentRoute.value.fullPath;
if (currentRoute.value.query?.ltpasToken) {
isSingleLogin.value = true
let targetURL = ''
let redirect = ''
if (fullPath.includes('targetURL')) {
const list = fullPath.split('targetURL=');
targetURL = list[1];
} else {
const list = fullPath.split('redirect=');
redirect = list[1];
}
let tenantCode='';
if(getAppEnvConfig().VITE_GLOB_TENANT_ENABLED=='true'){
let url='';
if(targetURL){
url=targetURL;
}else{
url=redirect;
}
if(url.includes('tenantCode')){
const fullString = decodeURIComponent(url);
const queryString = fullString.split('?')[1];
tenantCode = (queryString.match(/tenantCode=([^&]+)/) || [])[1]
}
if(!tenantCode){
notification.error({
message: t('提示'),
description: t('租户码不能为空!'),
});
return;
}
}
let params = {...currentRoute.value.query, targetURL: targetURL,redirect: redirect, mode: 'none',tenantCode:tenantCode }; //不要默认的错误提示
await userStore.singleLogin(params);
}
});
const appStore = useAppStore();
defineProps({
sessionTimeout: {
type: Boolean
}
});
const { prefixCls } = useDesign('login');
const sysName = ref(import.meta.env.VITE_SYSTEM_NAME);
let logoConfig = ref<LogoConfig>({});
let show = ref(false);
getLogoInfo().then((res) => {
show.value = true;
logoConfig.value = {
companyName: res.companyName,
shortName: res.shortName,
refreshLogoUrl: res.refreshLogoUrl,
backgroundLogoUrl: res.backgroundLogoUrl,
designerLogoUrl: res.designerLogoUrl,
loginLogoUrl: res.loginLogoUrl,
menuLogoUrl: res.menuLogoUrl
};
appStore.setLogoConfig(logoConfig.value);
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-login';
@logo-prefix-cls: ~'@{namespace}-app-logo';
@countdown-prefix-cls: ~'@{namespace}-countdown-input';
@dark-bg: linear-gradient(to bottom, #364876, #112049);
.vben-login-form {
padding: 40px;
}
.login-box {
display: flex;
background: linear-gradient(180deg, #00356d 0%, rgb(0 53 109 / 0%) 100%), linear-gradient(180deg, #0074d3 2%, #011853 100%);
align-items: center;
justify-content: center;
}
.logo-box {
position: fixed;
top: 50px;
left: 40px;
}
.logo-box img {
width: 180px;
}
.center-box {
background-color: #fff;
}
.login-left-title {
position: relative;
top: 30px;
}
.login-left-title .title {
font-size: 20px;
border: none;
margin-bottom: 10px;
}
.login-left-title .sub-title {
font-size: 36px;
font-weight: bold;
color: #5e95ff;
text-align: center;
}
.login-box .right-box {
position: absolute;
right: 0;
}
.fixed-tool {
position: fixed;
right: 20px;
top: 40px;
display: flex;
z-index: 2;
}
.right-top-box {
width: 400px;
position: fixed;
right: 0;
top: 0;
}
html[data-theme='dark'] {
.login-box {
background: @dark-bg;
}
.login-left-title {
border: none;
}
.login-left-title .title {
color: #fff;
}
.@{prefix-cls} {
background-color: @dark-bg;
// &::before {
// background-image: url(/@/assets/svg/login-bg-dark.svg);
// }
.ant-input,
.ant-input-password {
background-color: #232a3b;
}
.ant-input-affix-wrapper {
border-color: #525e7c;
background-color: #232a3b;
}
.ant-checkbox-inner {
border-color: #525e7c;
}
.ant-btn:not(.ant-btn-link, .ant-btn-primary) {
border: 1px solid #4a5569;
}
&-form {
background: transparent !important;
}
.app-iconify {
color: #fff;
}
}
input.fix-auto-fill,
.fix-auto-fill input {
-webkit-text-fill-color: #c9d1d9 !important;
box-shadow: inherit !important;
}
}
.@{prefix-cls} {
min-height: 100%;
overflow: hidden;
@media (max-width: @screen-xl) {
background-color: @dark-bg;
.@{prefix-cls}-form {
background-color: #fff;
}
}
.@{logo-prefix-cls} {
position: absolute;
top: 12px;
height: 30px;
&__title {
font-size: 16px;
color: #fff;
}
img {
width: 32px;
}
}
.container {
.@{logo-prefix-cls} {
display: flex;
width: 60%;
height: 80px;
&__title {
font-size: 24px;
color: #fff;
}
img {
width: 48px;
}
}
}
&-sign-in-way {
.anticon {
font-size: 22px;
color: #888;
cursor: pointer;
&:hover {
color: @primary-color;
}
}
}
input:not([type='checkbox']) {
min-width: 320px;
@media (max-width: @screen-xl) {
min-width: 280px;
}
@media (max-width: @screen-lg) {
min-width: 260px;
}
@media (max-width: @screen-md) {
min-width: 240px;
}
@media (max-width: @screen-sm) {
min-width: 160px;
}
}
.@{countdown-prefix-cls} input {
min-width: unset;
}
.ant-divider-inner-text {
font-size: 12px;
color: @text-color-secondary;
}
}
</style>
<style scoped>
:deep(.center-box) {
width: 540px;
}
</style>

View File

@ -0,0 +1,496 @@
<template>
<Form v-show="getShow" ref="formRef" :model="formData" :rules="getFormRules" class="p-4 enter-x form-box" @keypress.enter="handleLogin">
<div>
<FormItem class="enter-x" name="account" v-if="loginType =='pw'">
<label class="form-title"> {{ t('账号') }}</label>
<Input v-model:value="formData.account" :placeholder="t('账号')" class="fix-auto-fill" size="large" style="height: 58px;" @blur="handleBlur">
<template #prefix>
<IconFontSymbol class="user-icon" icon="yonghu-xianxing" />
</template>
</Input>
</FormItem>
<FormItem class="enter-x" name="password" v-if="loginType =='pw'">
<label class="form-title"> {{ t('密码') }}</label>
<InputPassword v-model:value="formData.password" :placeholder="t('密码')" size="large" style="height: 58px;" visibilityToggle>
<template #prefix>
<IconFontSymbol class="user-icon" icon="mima" />
</template>
</InputPassword>
</FormItem>
<FormItem class="enter-x" name="captchaCode" v-if="loginType =='pw' && loginUseType == 'captcha'">
<label class="form-title"> {{ t('图形验证码') }}</label>
<Input v-model:value="formData.captchaCode" :placeholder="t('图形验证码')" class="fix-auto-fill pic-code" size="large" style="height: 58px; flex: 1; min-width: auto;" visibilityToggle>
<template #prefix>
<PictureOutlined :style="{fontSize: '25px', color: '#717c91'}"/>
</template>
<template #suffix>
<a-image
:src="captchaImage.imgBase64 || ''"
alt="验证码"
class="captcha-image"
/>
<ReloadOutlined :style="{fontSize: '25px', color: '#717c91'}" @click="refreshCaptcha"/>
</template>
</Input>
</FormItem>
<FormItem v-if="getAppEnvConfig().VITE_GLOB_TENANT_ENABLED && getAppEnvConfig().VITE_GLOB_TENANT_INPUT_REQUIRED!=='false' && loginType =='pw'" name="tenantCode" class="enter-x">
<label class="form-title"> {{ t('租户码') }}</label>
<Input
size="large"
visibilityToggle
v-model:value="formData.tenantCode"
:placeholder="t('租户码')"
style="height: 58px; width: 450px;"
>
<template #prefix>
<IconFontSymbol icon="zuzhiguanli" class="user-icon" />
</template>
</Input>
</FormItem>
<FormItem class="enter-x" name="mobile" v-if="loginType =='mobile'">
<label class="form-title"> {{ t('手机号') }}</label>
<Input v-model:value="formData.mobile" :placeholder="t('手机号')" class="fix-auto-fill" size="large" style="height: 58px;">
<template #prefix>
<IconFontSymbol class="user-icon" icon="yonghu-xianxing" />
</template>
</Input>
</FormItem>
<FormItem class="enter-x" name="code" v-if="loginType =='mobile'">
<label class="form-title"> {{ t('验证码') }}</label>
<Input v-model:value="formData.code" :placeholder="t('验证码')" class="fix-auto-fill" size="large" style="height: 58px;">
<template #prefix>
<PhoneOutlined :style="{fontSize: '25px', color: '#717c91'}"/>
</template>
<template #suffix>
<Button type="link" class="f-16" @click="getLoginCode" size="small" :disabled="codeButtonDisabled">
{{ getCodeButtonName }}
</Button>
</template>
</Input>
</FormItem>
<ARow class="enter-x">
<ACol :span="12">
<FormItem v-if="loginType =='pw'">
<!-- No logic, you need to deal with it yourself -->
<Checkbox v-model:checked="rememberMe" class="f-16" size="small">
{{ t('记住我') }}
</Checkbox>
</FormItem>
</ACol>
<ACol :span="12" style="text-align: right;">
<FormItem>
<!-- No logic, you need to deal with it yourself -->
<Button type="link" class="f-16" @click="changeLoginType" size="small">
{{ loginType == 'mobile' ? t('账号密码登录') : t('手机登录') }}
</Button>
</FormItem>
</ACol>
<!-- <ACol :span="12">
<FormItem :style="{ 'text-align': 'right' }">
No logic, you need to deal with it yourself
<Button type="link" size="small" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)">
{{ t('忘记密码') }}
</Button>
</FormItem>
</ACol> -->
</ARow>
<FormItem class="enter-x">
<Button :loading="loading" :style="{ 'border-radius': '8px' }" block class="sub-button" type="primary" @click="handleLogin">
{{ t('登录') }}
</Button>
</FormItem>
<div class="tip-msg">本平台为非涉密平台严禁处理传输国家秘密工作秘密商业秘密敏感信息</div>
</div>
</Form>
<a-modal
v-model:visible="visible"
@ok="handleOk"
@cancel="handleCancel"
:maskClosable="false"
centered
title="请完成下列验证后继续"
width="20%"
cancelText=""
>
<div class="login-modal-content">
<div class="refresh" @click="refreshTodo">
刷新
<Icon :spin="refreshLoading" icon="ant-design:redo-outlined" class="redo-outlined" />
</div>
<a-image
:width="200"
:src="imgObj.imgBase64 || ''"
/>
<Input v-model:value="imgCode" :placeholder="t('验证码')" size="large" />
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { reactive, ref, unref, computed, onMounted } from 'vue';
import { Checkbox, Form, Input, Row, Col, Button } from 'ant-design-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
import { useUserStore } from '/@/store/modules/user';
import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from '/@/views/sys/login/useLogin';
import { getMobileLoginCode, getMobileLoginImg, sendMobileLoginCode, checkAccountCaptchaApi, } from '/@/api/system/login';
import { useDesign } from '/@/hooks/web/useDesign';
import { Base64 } from 'js-base64';
import IconFontSymbol from '/@/components/IconFontSymbol/Index.vue';
import { useRouter } from 'vue-router';
import { getAppEnvConfig } from '/@/utils/env';
import Icon from '/@/components/Icon/index';
import { PhoneOutlined, PictureOutlined, ReloadOutlined } from '@ant-design/icons-vue';
const ACol = Col;
const ARow = Row;
const FormItem = Form.Item;
const InputPassword = Input.Password;
const { t } = useI18n();
const { notification, createErrorModal } = useMessage();
const { prefixCls } = useDesign('login');
const userStore = useUserStore();
const { currentRoute } = useRouter();
const { getLoginState, setLoginState } = useLoginState();
const { getFormRules } = useFormRules();
const formRef = ref();
// const iframeRef = ref();
const loading = ref(false);
const rememberMe = ref(false);
const loginType = ref('pw')
const loginUseType = ref();
const countdown = ref(60)
const visible = ref(false);
const refreshLoading = ref(false);
const imgObj = ref({
imgBase64: ''
})
const captchaImage = ref({
imgBase64: ''
})
const imgCode = ref('')
const formData = reactive({
account: '',
password: '',
mobile: '',
code: '',
tenantCode: 'system',
captchaCode: ''
});
const getCodeButtonName = ref('获取验证码')
const codeButtonDisabled = ref(false)
let setCodeInterval = null
onMounted(async () => {
//如果是第三方登录跳转回来 会携带token
if (currentRoute.value.query.token) {
oauthLogin(currentRoute.value.query.token)
}
//如果第三方登录 登录错误 会携带错误信息
if (currentRoute.value.query.error) {
createErrorModal({
title: t('错误提示'),
content: t(currentRoute.value.query.error as string),
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body
});
}
const loginInfo = window.localStorage.getItem('USER__LOGIN__INFO__');
if (loginInfo) {
formData.account = Base64.decode(JSON.parse(loginInfo).account);
formData.password = Base64.decode(JSON.parse(loginInfo).password);
formData.tenantCode = Base64.decode(JSON.parse(loginInfo).tenantCode);
}
});
const oauthLogin = async (token) => {
try {
loading.value = true;
const userInfo = await userStore.oauthLogin({
token: token as string,
mode: 'none' //不要默认的错误提示
});
if (userInfo) {
notification.success({
message: t('登录成功'),
description: `${t('欢迎回来')}: ${userInfo.name}`,
duration: 3
});
}
} catch (error) {
createErrorModal({
title: t('错误提示'),
content: (error as unknown as Error).message || t('网络异常,请检查您的网络连接是否正常!'),
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body
});
} finally {
loading.value = false;
}
}
const changeLoginType = () => {
loginType.value = loginType.value == 'pw' ? 'mobile' : 'pw'
}
const getLoginCode = async () => {
countdown.value = 60
if(isValidPhoneNumber(formData.mobile)) {
visible.value = true
imgCode.value = ''
onMobileLoginImg()
} else {
notification.error({
message: t('手机号有误'),
description: `${t('手机号有误,请重新填写')}`,
duration: 3
});
}
}
/**
* 账号失焦 请求后台获取业务状态
*/
async function handleBlur() {
if (!formData.account) {
return;
}
let checkAccountCaptcha = await checkAccountCaptchaApi({username: formData.account})
if(checkAccountCaptcha == true) {
loginUseType.value = 'captcha';
// setLoginState(LoginStateEnum.LOGIN_WITH_CAPTCHA)
refreshCaptcha();
} else {
loginUseType.value = '';
// setLoginState(LoginStateEnum.LOGIN)
}
}
function refreshTodo() {
refreshLoading.value = true
onMobileLoginImg()
refreshLoading.value = false
}
// 图形验证
async function onMobileLoginImg() {
imgObj.value = await getMobileLoginImg({account: formData.mobile})
}
// 图形账号验证
async function refreshCaptcha() {
captchaImage.value = await getMobileLoginImg({account: formData.account})
}
async function handleOk() {
await getMobileLoginCode({captchaCode: imgCode.value ,mobile: formData.mobile})
setCodeInterval = setInterval(updateCountdown, 1000);
onVisible()
}
function handleCancel() {
onVisible()
}
function onVisible () {
visible.value = false
srcImg.value = ''
}
// 更新倒计时显示
function updateCountdown() {
if (countdown.value === 0) {
getCodeButtonName.value = '获取验证码';
codeButtonDisabled.value = false;
clearInterval(setCodeInterval)
} else {
countdown.value--;
getCodeButtonName.value = countdown.value + ' 秒后可重发';
codeButtonDisabled.value = true;
}
}
const isValidPhoneNumber = (phoneNumber) => {
// 中国大陆手机号码正则表达式
const reg = /^1[3|4|5|7|8]\d{9}$/
return reg.test(phoneNumber);
}
const { validForm } = useFormValid(formRef);
//onKeyStroke('Enter', handleLogin);
const getShow = computed(() => unref(getLoginState) === LoginStateEnum.LOGIN);
async function handleLogin() {
const data = await validForm();
if (!data) return;
if (loginType.value == 'pw') {
try {
// 校验有没带图形验证码提交
if (loginUseType.value == 'captcha' && !data.captchaCode) {
createErrorModal({
title: t('错误提示'),
content: '请输入图形验证码',
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body
});
return;
}
loading.value = true;
const userInfo = await userStore.login({
password: data.password,
userName: data.account,
tenantCode: data.tenantCode,
captchaCode: data.captchaCode,
deviceType: 0, //pc-0,app-1
mode: 'none' //不要默认的错误提示
});
if (userInfo) {
notification.success({
message: t('登录成功'),
description: `${t('欢迎回来')}: ${userInfo.name}`,
duration: 3
});
if (rememberMe.value) {
const info = {
account: Base64.encode(data.account),
password: Base64.encode(data.password),
tenantCode: Base64.encode(data.tenantCode)
};
window.localStorage.setItem('USER__LOGIN__INFO__', JSON.stringify(info));
} else {
window.localStorage.removeItem('USER__LOGIN__INFO__');
}
}
} catch (error) {
if ((error as unknown as Error).message.includes('验证码')) {
loginUseType.value = 'captcha';
// setLoginState(LoginStateEnum.LOGIN_WITH_CAPTCHA);
refreshCaptcha();
}
createErrorModal({
title: t('错误提示'),
content: (error as unknown as Error).message || t('网络异常,请检查您的网络连接是否正常!'),
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body
});
} finally {
loading.value = false;
handleBlur();
}
} else {
try {
let params = {
mobile: data.mobile,
code: data.code
}
let res = await sendMobileLoginCode(params)
await oauthLogin(res.token)
} catch (error) {
createErrorModal({
title: t('错误提示'),
content: (error as unknown as Error).message || t('网络异常,请检查您的网络连接是否正常!'),
getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body
});
} finally {
loading.value = false;
}
}
}
</script>
<style lang="less" scoped>
.tip-msg {
color: red;
font-size: 12px;
text-align: center;
}
.form-box {
font-size: 16px;
}
.f-16 {
font-size: 16px;
}
:deep(.ant-checkbox-inner) {
border-color: #ced5f2;
width: 18px;
height: 18px;
border-radius: 0;
}
:deep(.ant-form-item input[type='checkbox']) {
width: 18px;
height: 18px;
}
.form-title {
line-height: 40px;
display: block;
}
.sub-button {
height: 48px;
font-size: 20px;
}
.ant-form label {
font-size: 16px;
}
.user-icon {
font-size: 26px;
margin-right: 8px;
color: #707c92;
}
.ant-input-affix-wrapper {
border-color: #ced5f2;
border-style: none none solid;
}
:deep(.ant-input-password-icon) {
font-size: 20px;
color: #707c92;
margin-right: 8px;
}
:deep(.ant-input-affix-wrapper-lg) {
padding-left: 0;
}
.login-modal-content {
text-align: center;
.refresh {
text-align: right;
cursor: pointer;
}
}
:deep(.captcha-image) {
cursor: pointer;
border: 1px solid #ddd;
border-radius: 4px;
height: 40px;
}
// :deep(.phone-outlined-class) {
// height: 25.99px;
// width: 25.99px;
// }
</style>
<style lang="less">
.pic-code {
input {
min-width: 0!important;
}
}
</style>

View File

@ -0,0 +1,24 @@
<template>
<div style="width: 100%; height: 300px; display: flex;justify-content: center;align-items: center;">
<a-spin />
</div>
</template>
<script lang="ts" setup>
import { useUserStore } from '/@/store/modules/user';
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
const { currentRoute } = useRouter();
const userStore = useUserStore();
onMounted(async () => {
const fullPath = currentRoute.value.fullPath;
const list = fullPath.split('targetURL=');
const targetURL = list[1];
let params = {...currentRoute.value.query, targetURL: targetURL, mode: 'none' }; //不要默认的错误提示
await userStore.singleLogin(params);
});
</script>
<style lang="less">
</style>

View File

@ -0,0 +1,40 @@
<template>
<div style="width: 100%; height: 300px; display: flex;justify-content: center;align-items: center;">
<a-spin />
</div>
</template>
<script lang="ts" setup>
import { useUserStore } from '/@/store/modules/user';
import { onMounted } from 'vue';
import { useRouter } from 'vue-router';
const { currentRoute } = useRouter();
const userStore = useUserStore();
onMounted(async () => {
const fullPath = currentRoute.value.fullPath;
let targetURL = ''
let redirect = ''
if (fullPath.includes('targetURL')) {
const list = fullPath.split('targetURL=');
targetURL = list[1];
} else {
const list = fullPath.split('redirect=');
redirect = list[1];
}
let params = {...currentRoute.value.query, targetURL: targetURL,redirect: redirect, mode: 'none',goHome:true }; //不要默认的错误提示
const token=currentRoute.value.query?.token;
//cas ticket的话走一遍后端换成token
const ticket=currentRoute.value.query?.ticket;
if(ticket){
params.ticket=ticket;
await userStore.casLogin(params);
}else if (token) {
await userStore.tokenLogin(token,params);
}
});
</script>
<style lang="less">
</style>

View File

@ -0,0 +1,546 @@
<template>
<a-spin :spinning="spinning" tip="请稍后...">
<div class="page-bg-wrap">
<div class="geg-flow-page">
<div class="top-toolbar">
<a-space :size="10" wrap>
<a-button style="margin-right: 10px" @click="close">
<slot name="icon">
<close-outlined />
</slot>
关闭
</a-button>
<a-button @click="saveDraft" v-if="!readonly">
暂存
</a-button>
<a-button @click="setDraft" v-if="rDraftsId && !readonly">
从草稿导入
</a-button>
<a-button v-if="!readonly && hasBtnApprove" type="primary" @click="onApproveClick()">
<slot name="icon">
<check-circle-outlined />
</slot>
同意
</a-button>
<a-button v-if="!readonly && hasBtnReject" @click="onDenyClick">
<slot name="icon">
<stop-outlined />
</slot>
拒绝
</a-button>
<a-dropdown>
<template #overlay>
<a-menu @click="onMoreClick">
<a-menu-item v-if="!readonly && hasBtnFinish" key="finish">终止</a-menu-item>
<a-menu-item v-if="!readonly" key="transfer">转办</a-menu-item>
<a-menu-item v-if="readonly && drawNode" key="drawBack">撤回</a-menu-item>
<a-menu-item key="flowchart">查看流程图</a-menu-item>
</a-menu>
</template>
<a-button>
更多
<down-outlined />
</a-button>
</a-dropdown>
</a-space>
</div>
<FormInformation
:key="renderKey"
ref="formInformation"
:disabled="readonly"
:formAssignmentData="data.formAssignmentData"
:formInfos="data.formInfos"
:opinions="data.opinions"
:opinionsComponents="data.opinionsComponents"
@get-form-configs="(config) => (formConfigs = config)"
/>
<Title :font-size="18" default-value="流转信息"></Title>
<flow-history :items="getTaskRecords()"></flow-history>
<opinion-dialog ref="opinionDlg" />
<transfer-dialog ref="transferDlg" />
<a-modal :closable="false" v-if="showFlowChart" visible="true" centered class="geg" title="流程图" width="1200px" @cancel="closeFlowChart">
<process-information :process-id="processId" :xml="data.xml"
:currentTaskInfo="data.currentTaskInfo"
:currentTaskAssigneeNames="data.currentTaskAssigneeNames"
:currentTaskAssignees="data.currentTaskAssignees"/>
<template #footer>
<a-button type="primary" @click="closeFlowChart">关闭</a-button>
</template>
</a-modal>
</div>
</div>
</a-spin>
</template>
<script setup>
import { useRouter } from 'vue-router';
import { onMounted, reactive, ref, unref, createVNode } from 'vue';
import FormInformation from '/@/views/secondDev/FormInformation.vue';
import userTaskItem from '/@/views/workflow/task/hooks/userTaskItem';
import { getApprovalProcess, postApproval, postGetNextTaskMaybeArrival, postTransfer, getDrawNode, withdraw } from '/@/api/workflow/task';
import { ApproveCode, ApproveType } from '/@/enums/workflowEnum';
import { CheckCircleOutlined, StopOutlined, CloseOutlined, DownOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
import OpinionDialog from '/@/components/SecondDev/OpinionDialog.vue';
import TransferDialog from '/@/components/SecondDev/TransferDialog.vue';
import { separator } from '/@bpmn/config/info';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
import Title from '/@/components/Title/src/Title.vue';
import FlowHistory from '/@/components/SecondDev/FlowHistory.vue';
import { message, Modal } from 'ant-design-vue';
import useEventBus from '/@/hooks/event/useEventBus';
import ProcessInformation from '/@/views/workflow/task/components/flow/ProcessInformation.vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
import { useUserStore } from '/@/store/modules/user';
import { deleteDraft, postDraft, putDraft, getDraftInfo } from '/@/api/workflow/process';
import { TaskTypeUrl } from '/@/enums/workflowEnum';
const spinning = ref(false);
const userStore = useUserStore();
const { t } = useI18n();
const { notification } = useMessage();
const { data, approveUserData, initProcessData, notificationError, notificationSuccess } = userTaskItem();
const { bus, FLOW_PROCESSED } = useEventBus();
const tabStore = useMultipleTabStore();
const router = useRouter();
const currentRoute = router.currentRoute.value;
const fullPath = currentRoute.fullPath;
const rQuery = currentRoute.query;
const rParams = currentRoute.params;
const schemaId = ref(rParams.arg1);
const taskId = ref(rQuery.taskId);
const processId = ref(rParams.arg2);
const readonly = ref(!!rQuery.readonly); // 查看流程会触发只读模式
const rDraftsId = ref(rQuery.draftId || '')
const renderKey = ref('');
const formConfigs = ref();
const opinionDlg = ref();
const transferDlg = ref();
const validateSuccess = ref(false);
const formInformation = ref();
const showFlowChart = ref(false);
const hasBtnApprove = ref(true);
const hasBtnReject = ref(false);
const hasBtnFinish = ref(false);
let draftData = {}
const drawNode = ref('');
const props = defineProps({
rowKeyData: {
type: String
}
});
let approvalData = reactive({
isCountersign: false,
isAddOrSubSign: false,
isEnd: false,
stampInfo: {
stampId: '',
password: ''
},
buttonConfigs: [],
approvedType: ApproveType.AGREE,
approvedResult: ApproveCode.AGREE,
approvedContent: '',
rejectNodeActivityId: '',
rejectNodeActivityIds: [],
circulateConfigs: [],
nextTaskUser: {} // 格式为taskKey: 用户id逗号分隔
});
let approvedType = ref(ApproveType.AGREE);
function onMoreClick(e) {
const key = e.key;
if (key === 'flowchart') {
openFlowChart();
} else if (key === 'finish') {
Modal.confirm({
title: () => '提示',
content: () => '确定终止吗?',
onOk: () => {
onFinishClick();
}
});
} else if (key === 'transfer') {
onTransferClick();
} else if (key === 'drawBack') {
Modal.confirm({
title: t('提示'),
icon: createVNode(ExclamationCircleOutlined),
content: t('请确认是否撤回该流程?'),
okText: t('确定'),
okType: 'danger',
cancelText: t('取消'),
onOk() {
openSpinning();
withdraw(processId.value, drawNode.value).then((res) => {
if (res) {
notification.open({
type: 'success',
message: t('撤回'),
description: t('撤回成功')
});
} else {
notification.open({
type: 'error',
message: t('撤回'),
description: t('撤回失败')
});
}
}).finally(()=>{
closeSpinning();
});
},
onCancel() {}
});
}
}
function closeFlowChart() {
showFlowChart.value = false;
}
function openFlowChart() {
showFlowChart.value = true;
}
function close() {
tabStore.closeTab(currentRoute, router);
}
async function setDraft(needModal = true) {
let formData = [];
let params = {
taskId: taskId.value,
userId: userStore.getUserInfo.id,
}
let res = await getDraftInfo('', taskId.value, userStore.getUserInfo.id)
if(res) {
draftData = JSON.parse(res.formData)
rDraftsId.value = res.id
if(!needModal) return
Modal.confirm({
title: () => '提示',
content: () => '确认使用草稿覆盖当前数据?',
onOk: async () => {
data.formInfos.forEach((item) => {
if (draftData && item.formConfig && item.formConfig.key && draftData[item.formConfig.key]) {
formData.push(item.formConfig.key ? draftData[item.formConfig.key] : {});
}
});
await formInformation.value.setFormData(formData);
}
});
}
}
async function saveDraft() {
try {
spinning.value = true;
let formModels = await formInformation.value.saveDraftData();
if (rDraftsId.value) {
let res = await putDraft(schemaId.value, formModels, rDraftsId.value, props.rowKeyData, processId.value, taskId.value);
showResult(res, '保存草稿');
} else {
let res = await postDraft(schemaId.value, formModels, props.rowKeyData, processId.value, taskId.value );
showResult(res, '保存草稿');
}
setDraft(false)
spinning.value = false;
} catch (error) {
spinning.value = false;
notificationError('保存草稿');
}
}
function showResult(res, title) {
if (res) {
notificationSuccess(title);
} else {
notificationError(title);
}
}
async function onApproveClick(isAutoAgreeBreak = false) {
openSpinning();
if (!isAutoAgreeBreak) {
await submit();
}
if (!validateSuccess.value) {
closeSpinning();
return;
}
const params = await getApproveParams();
const nextNodes = await postGetNextTaskMaybeArrival(params);
approvalData.approvedType = ApproveType.AGREE;
approvalData.approvedResult = ApproveCode.AGREE;
//如果是自动同意触发的关闭弹层的loading
if (isAutoAgreeBreak) {
opinionDlg.value.stopLoading()
}
closeSpinning();
opinionDlg.value.toggleDialog({
action: 'agree',
nextNodes,
callback: (args) => {
approvalData.approvedContent = args.opinion;
approvalData.nextTaskUser = args.nextTaskUser;
approvalData.isEnd=args.isEnd;
onFinish('approve');
}
});
}
async function onDenyClick() {
approvalData.approvedType = ApproveType.REJECT;
approvalData.approvedResult = ApproveCode.REJECT;
opinionDlg.value.toggleDialog({
action: 'reject',
processId: processId.value,
taskId: taskId.value,
callback: (args) => {
approvalData.approvedContent = args.opinion;
approvalData.rejectNodeActivityId = args.rejectNodeId;
approvalData.nextTaskUser = args.nextTaskUser;
onFinish('reject');
}
});
}
function onTransferClick() {
transferDlg.value.toggleDialog({
taskId: taskId.value,
callback: (args) => {
onTransfer(args);
}
});
}
function onTransfer({ taskId, userId }) {
transferDlg.value.validate().then(() => {
transferDlg.value.startLoading();
postTransfer(taskId, userId)
.then((res) => {
transferDlg.value.toggleDialog({
isClose: true
});
message.success('转办成功');
setTimeout(() => {
bus.emit(FLOW_PROCESSED);
close();
}, 500);
})
.catch((err) => {
message.error('转办失败');
transferDlg.value.toggleDialog({
isClose: true
});
});
});
}
function onTransferFlowFail() {
transferDlg.value.stopLoading();
}
async function onFinishClick() {
approvalData.approvedType = ApproveType.FINISH;
approvalData.approvedResult = ApproveCode.FINISH;
onFinish('finish');
}
function flowSuccess() {
opinionDlg.value.toggleDialog({
isClose: true
});
message.success('操作成功');
setTimeout(() => {
bus.emit(FLOW_PROCESSED);
close();
}, 500);
}
function flowFail() {
opinionDlg.value.stopLoading();
}
function reset() {
approvalData.isAddOrSubSign = false;
approvalData.stampInfo = {
stampId: '',
password: ''
};
approvalData.buttonConfigs = [];
approvalData.approvedType = ApproveType.AGREE;
approvalData.approvedContent = '';
approvalData.rejectNodeActivityId = '';
approvalData.rejectNodeActivityIds = [];
approvalData.circulateConfigs = [];
}
function setBtnStatus() {
const btnConfigs = approvalData.buttonConfigs;
btnConfigs.forEach((btn) => {
const code = btn.buttonCode;
if (code === 'reject') {
hasBtnReject.value = true;
} else if (code === 'finish') {
hasBtnFinish.value = true;
}
});
}
function getTaskRecords() {
if (data?.taskApproveOpinions?.length) {
return data.taskApproveOpinions || [];
}
}
onMounted(async () => {
try {
let res = await getApprovalProcess(unref(taskId), unref(processId));
initProcessData(res);
const title = res?.schemaInfo?.name;
if (title) {
const tabPrefix = readonly.value ? '查看' : '审批';
tabStore.changeTitle(fullPath, `${tabPrefix}${title}`);
}
if (!readonly.value) {
if (res.buttonConfigs) {
approvalData.buttonConfigs = res.buttonConfigs;
setBtnStatus();
}
if (res.relationTasks) {
data.predecessorTasks = res.relationTasks;
}
if (res.isAddOrSubSign) {
approvalData.isAddOrSubSign = res.isAddOrSubSign;
}
approvalData.approvedType = ApproveType.AGREE;
approvedType.value = ApproveType.AGREE;
approvalData.approvedContent = '';
approvalData.rejectNodeActivityId = '';
approvalData.rejectNodeActivityIds = [];
approvalData.circulateConfigs = [];
}
renderKey.value = Math.random() + '';
getBackNode();
setDraft()
} catch (error) {}
});
function getBackNode() {
getDrawNode(processId.value).then((res) => {
if (res.length) {
drawNode.value = res[0].activityId;
} else {
drawNode.value = '';
}
});
}
async function submit() {
data.submitLoading = true;
validateSuccess.value = false;
try {
let validateForms = await formInformation.value.validateForm();
if (validateForms.length > 0) {
let successValidate = validateForms.filter((ele) => {
return ele.validate;
});
if (successValidate.length == validateForms.length) {
validateSuccess.value = true;
data.submitLoading = false;
} else {
data.submitLoading = false;
notificationError(t('审批流程'), t('表单校验未通过'));
}
}
} catch (error) {
data.submitLoading = false;
notificationError(t('审批流程'), t('审批流程失败'));
}
}
function getUploadFileFolderIds(formModels) {
let fileFolderIds = [];
let uploadComponentIds = formInformation.value.getUploadComponentIds();
uploadComponentIds.forEach((ids) => {
if (ids.includes(separator)) {
let arr = ids.split(separator);
if (arr.length == 2 && formModels[arr[0]][arr[1]]) {
fileFolderIds.push(formModels[arr[0]][arr[1]]);
} else if (arr.length == 3 && formModels[arr[0]][arr[1]] && Array.isArray(formModels[arr[0]][arr[1]])) {
formModels[arr[0]][arr[1]].forEach((o) => {
fileFolderIds.push(o[arr[2]]);
});
}
}
});
return fileFolderIds;
}
async function getApproveParams() {
let formModels = await formInformation.value.getFormModels();
let system = formInformation.value.getSystemType();
let fileFolderIds = getUploadFileFolderIds(formModels);
return {
approvedType: approvalData.approvedType,
approvedResult: approvalData.approvedResult, // approvalData.approvedType 审批结果 如果为 4 就需要传buttonCode
approvedContent: approvalData.approvedContent,
formData: formModels,
rejectNodeActivityId: approvalData.rejectNodeActivityId,
taskId: taskId.value,
fileFolderIds,
circulateConfigs: approvalData.circulateConfigs,
/*stampId: values.stampId,
stampPassword: values.password,*/
isOldSystem: system,
isEnd:approvalData.isEnd,
nextTaskUser: approvalData.nextTaskUser
};
}
async function onFinish(values) {
try {
if (validateSuccess.value || values === 'reject' || values === 'finish') {
let params = await getApproveParams();
let response = await postApproval(params);
// 判断返回值是否带有isAutoAgree 来判断中间是否有自动审批的业务如有再执行判断待审人员是否包含自己不包含就直接flowSuccess
if (checkIsAutoAgree(response)) return
flowSuccess();
data.submitLoading = false;
}
} catch (error) {
flowFail();
}
}
/**
* 判断该次审核是否触发自动同意事务并且检验返回得task 是否是自身作为被审需要弹框再次审核
* @param response
*/
function checkIsAutoAgree(response) {
if (response != null
&& response.length != 0
&& response[0].isAutoAgree == true //
&& response[0].approveUserIds.includes(userStore.getUserInfo.id)) {
console.error('will reSelect user=', response[0].taskId);
// 注入新得taskId
taskId.value = response[0].taskId;
data.submitLoading = false;
onApproveClick(true);
return true;
} else {
return false;
}
}
function openSpinning() {
spinning.value = true;
}
function closeSpinning() {
spinning.value = false;
}
</script>

View File

@ -0,0 +1,393 @@
<template>
<div class="page-bg-wrap">
<a-spin :spinning="loading" tip="加载中...">
<div class="geg-flow-page">
<div class="top-toolbar">
<a-space :size="10" wrap>
<a-button style="margin-right: 10px" @click="close">
<slot name="icon">
<close-outlined />
</slot>
关闭
</a-button>
<a-button :disabled="data.submitLoading" type="primary" @click="saveLaunch">
<slot name="icon">
<send-outlined />
</slot>
提交
</a-button>
<a-button :disabled="data.submitLoading" @click="saveDraft">
<slot name="icon">
<clock-circle-outlined />
</slot>
暂存
</a-button>
<a-button>
<slot name="icon">
<printer-outlined />
</slot>
打印
</a-button>
<a-button @click="openFlowChart">
<slot name="icon">
<apartment-outlined />
</slot>
流程图
</a-button>
</a-space>
</div>
<div class="flow-content">
<FormInformation :key="randKey" ref="formInformation" :disabled="false" :formAssignmentData="data.formAssignmentData" :formInfos="data.formInfos" :opinions="data.opinions" :opinionsComponents="data.opinionsComponents" />
</div>
</div>
<a-modal :visible="showFlowChart" centered class="geg" closable title="流程图" width="1200px" @cancel="closeFlowChart">
<process-information :process-id="processId" :xml="data.xml" />
<template #footer>
<a-button type="primary" @click="closeFlowChart">关闭</a-button>
</template>
</a-modal>
<opinion-dialog ref="opinionDlg" />
</a-spin>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router';
import userTaskItem from '/@/views/workflow/task/hooks/userTaskItem';
import FormInformation from '/@/views/secondDev/FormInformation.vue';
import ProcessInformation from '/@/views/workflow/task/components/flow/ProcessInformation.vue';
import { getStartProcessInfo, getReStartProcessInfo, reLaunch, postLaunch, postApproval, postGetNextTaskMaybeArrival } from '/@/api/workflow/task';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
import { CloseOutlined, SendOutlined, ClockCircleOutlined, PrinterOutlined, ApartmentOutlined } from '@ant-design/icons-vue';
import { nextTick, onMounted, ref, toRaw, reactive } from 'vue';
import { deleteDraft, postDraft, putDraft } from '/@/api/workflow/process';
import { useI18n } from '/@/hooks/web/useI18n';
import { separator } from '/@bpmn/config/info';
import { message } from 'ant-design-vue';
import OpinionDialog from '/@/components/SecondDev/OpinionDialog.vue';
import { ApproveCode, ApproveType } from '/@/enums/workflowEnum';
import useEventBus from '/@/hooks/event/useEventBus';
const { bus, CREATE_FLOW } = useEventBus();
const router = useRouter();
const tabStore = useMultipleTabStore();
const { t } = useI18n();
const { data, approveUserData, initProcessData, notificationSuccess, notificationError } = userTaskItem();
const currentRoute = router.currentRoute.value;
const rParams = currentRoute.params;
const fullPath = currentRoute.fullPath;
const rQuery = currentRoute.query;
const rSchemaId = rParams.arg1;
const rDraftsId = rParams.arg2;
const taskId = ref();
const loading = ref(false);
const draftsJsonStr = localStorage.getItem('draftsJsonStr');
const formJsonStr = localStorage.getItem('formJsonStr');
let formInformation = ref();
const opinionDlg = ref();
let pageMode = 'new';
const showFlowChart = ref(false);
const disableSubmit = ref(false);
const mainFormModels = ref();
let randKey = ref('randkey'); // 强制表单重新渲染
let approvalData = reactive({
isCountersign: false,
isAddOrSubSign: false,
stampInfo: {
stampId: '',
password: ''
},
buttonConfigs: [],
approvedType: ApproveType.AGREE,
approvedResult: ApproveCode.AGREE,
approvedContent: '',
rejectNodeActivityId: '',
rejectNodeActivityIds: [],
circulateConfigs: [],
nextTaskUser: {} // 格式为taskKey: 用户id逗号分隔
});
if (draftsJsonStr) {
localStorage.removeItem('draftsJsonStr');
pageMode = 'draft';
}
if (formJsonStr) {
localStorage.removeItem('formJsonStr');
}
function closeFlowChart() {
showFlowChart.value = false;
}
function openFlowChart() {
showFlowChart.value = true;
}
function close() {
tabStore.closeTab(currentRoute, router);
}
const props = defineProps({
rowKeyData: {
type: String
}
});
onMounted(async () => {
try {
// 发起流程
let res = await getStartProcessInfo(rSchemaId);
const title = res?.schemaInfo?.name;
if (title) {
const tabPrefix = pageMode === 'new' ? '新建' : '草稿';
tabStore.changeTitle(fullPath, `${tabPrefix}${title}`);
}
initProcessData(res);
} catch (error) {}
randKey.value = Math.random() + '';
// 这里的顺序不能变 表单不渲染的时候 设置表单初值没用
await nextTick();
await initDraftsFormData();
await initFromFormData();
});
async function initDraftsFormData() {
//isPublish.value = Object.keys(data.formInfos).length > 0 ? false : true;
if (draftsJsonStr) {
let formDataJson = JSON.parse(draftsJsonStr);
let formData = [];
data.formInfos.forEach((item) => {
if (formDataJson && item.formConfig && item.formConfig.key && formDataJson[item.formConfig.key]) {
formData.push(item.formConfig.key ? formDataJson[item.formConfig.key] : {});
}
});
await formInformation.value.setFormData(formData);
}
}
async function initFromFormData() {
if (formJsonStr) {
let formDataJson = JSON.parse(formJsonStr);
let formData = [];
if(rQuery.fromKey&&formDataJson[rQuery.fromKey]){
formData.push(formDataJson[rQuery.fromKey]);
}
await formInformation.value.setFormData(formData);
}
}
async function saveDraft() {
try {
disableSubmit.value = true;
let formModels = await formInformation.value.saveDraftData();
if (rDraftsId !== '0') {
let res = await putDraft(rSchemaId, formModels, rDraftsId, props.rowKeyData);
showResult(res, '保存草稿');
} else {
let res = await postDraft(rSchemaId, formModels, props.rowKeyData);
showResult(res, '保存草稿');
}
} catch (error) {
notificationError('保存草稿');
}
}
function showResult(res, title) {
if (res) {
notificationSuccess(title);
} else {
notificationError(title);
}
}
function createFlowSuccess(taskList) {
/*opinionDlg.value.toggleDialog({
isClose: true
});*/
if (taskList) {
approvalCreate();
} else {
message.success('流程发起成功');
data.submitLoading = false;
loading.value = false;
setTimeout(() => {
bus.emit(CREATE_FLOW, {});
close();
}, 500);
}
}
async function approvalCreate() {
const params = await getApproveParams();
const nextNodes = await postGetNextTaskMaybeArrival(params);
if(nextNodes.length==0){
message.error('流程没有可以选择的下一节点');
loading.value = false;
data.submitLoading = false;
return;
}
opinionDlg.value.toggleDialog({
action: 'agree',
nextNodes,
rejectCancel: () => {
loading.value = false;
data.submitLoading = false;
},
callback: (args) => {
approvalData.approvedContent = args.opinion;
approvalData.nextTaskUser = args.nextTaskUser;
onFinish({});
}
});
}
function flowSuccess() {
opinionDlg.value.toggleDialog({
isClose: true
});
message.success('操作成功');
setTimeout(() => {
bus.emit(CREATE_FLOW, {});
close();
}, 500);
}
function flowFail() {
opinionDlg.value.stopLoading();
}
async function onFinish(values) {
try {
if (/*validateSuccess.value*/ true) {
let params = await getApproveParams();
await postApproval(params);
flowSuccess();
data.submitLoading = false;
loading.value = false;
}
} catch (error) {
flowFail();
}
}
async function getApproveParams() {
let formModels = mainFormModels.value;
let system = formInformation.value.getSystemType();
let fileFolderIds = getUploadFileFolderIds(formModels);
return {
approvedType: approvalData.approvedType,
approvedResult: approvalData.approvedResult, // approvalData.approvedType 审批结果 如果为 4 就需要传buttonCode
approvedContent: approvalData.approvedContent,
formData: formModels,
rejectNodeActivityId: approvalData.rejectNodeActivityId,
taskId: taskId.value,
fileFolderIds,
circulateConfigs: approvalData.circulateConfigs,
/*stampId: values.stampId,
stampPassword: values.password,*/
isOldSystem: system,
nextTaskUser: approvalData.nextTaskUser,
draftId: rDraftsId,
};
}
async function saveLaunch() {
data.submitLoading = true;
loading.value = true;
try {
let validateForms = await formInformation.value.validateForm();
let system = formInformation.value.getSystemType();
if (validateForms.length > 0) {
let successValidate = validateForms.filter((ele) => {
return ele.validate;
});
if (successValidate.length == validateForms.length) {
mainFormModels.value = await formInformation.value.getFormModels(true);
/*for (let i in mainFormModels.value) {
const item = mainFormModels.value[i];
if (!item['_id']) {
throw new Error('发起失败');
} else {
delete item['_id'];
}
}*/
let relationTasks = [];
if (data.predecessorTasks && data.predecessorTasks.length > 0) {
relationTasks = data.predecessorTasks.map((ele) => {
return { taskId: ele.taskId, schemaId: ele.schemaId };
});
}
let fileFolderIds = getUploadFileFolderIds(mainFormModels.value);
if (taskId.value) {
// 如果在草稿节点取消,重新从草稿开始走
return createFlowSuccess(true);
}
//如果传入了processId 代表是重新发起流程
let res;
res = await postLaunch(rSchemaId, mainFormModels.value, relationTasks, fileFolderIds, system);
// 下一节点审批人
let taskList = [];
if (res && res.length > 0) {
taskId.value = res[0].taskId;
taskList = res
.filter((ele) => {
return ele.isMultiInstance == false && ele.isAppoint == true;
})
.map((ele) => {
return {
taskId: ele.taskId,
taskName: ele.taskName,
provisionalApprover: ele.provisionalApprover,
selectIds: []
};
});
const strictDesign = false;
if (strictDesign && taskList.length > 0) {
notificationError('提交失败', '流程设计错误,开始后的第一个节点必须是审批人为发起人的起草节点。');
data.submitLoading = false;
loading.value = false;
} else {
createFlowSuccess(taskList);
}
} else {
createFlowSuccess();
}
} else {
data.submitLoading = false;
notificationError(t('发起流程'), t('表单校验未通过'));
loading.value = false;
}
}
} catch (error) {
data.submitLoading = false;
loading.value = false;
notificationError(t('发起流程'), t('发起流程失败'));
}
}
function getUploadFileFolderIds(formModels) {
let fileFolderIds = [];
let uploadComponentIds = formInformation.value.getUploadComponentIds();
uploadComponentIds.forEach((ids) => {
if (ids.includes(separator)) {
let arr = ids.split(separator);
if (arr.length == 2 && formModels[arr[0]][arr[1]]) {
fileFolderIds.push(formModels[arr[0]][arr[1]]);
} else if (arr.length == 3 && formModels[arr[0]][arr[1]] && Array.isArray(formModels[arr[0]][arr[1]])) {
formModels[arr[0]][arr[1]].forEach((o) => {
fileFolderIds.push(o[arr[2]]);
});
}
}
});
return fileFolderIds;
}
</script>
<style lang="less"></style>

View File

@ -0,0 +1,141 @@
<template>
<div class="page-bg-wrap">
<div class="top-toolbar">
<a-space :size="10" wrap style="gap:0">
<a-button style="margin-right: 10px" @click="close">
<slot name="icon">
<close-outlined />
</slot>
关闭
</a-button>
<a-button v-if="mode != 'view'" type="primary" @click="handleSubmit">
<slot name="icon">
<check-circle-outlined />
</slot>
确认
</a-button>
</a-space>
</div>
<component :is="dynamicComponent" ref="formRef" :fromPage="FromPageType.MENU" @form-mounted="onFormMounted" />
</div>
</template>
<script setup>
import { useRouter } from 'vue-router';
import { FromPageType } from '/@/enums/workflowEnum';
import { ref, computed, onMounted, onBeforeMount, nextTick, defineAsyncComponent } from 'vue';
import { useMessage } from '/@/hooks/web/useMessage';
import { useI18n } from '/@/hooks/web/useI18n';
import { CheckCircleOutlined, StopOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
import useEventBus from '/@/hooks/event/useEventBus';
const dynamicComponent = ref(null);
const formType = ref('2'); // 0 新建 1 修改 2 查看
const formRef = ref();
const props = defineProps({});
const { bus, FORM_LIST_MODIFIED } = useEventBus();
const router = useRouter();
const { currentRoute } = router;
const { formPath } = currentRoute.value.query;
const pathArr = formPath.split('/');
const tabStore = useMultipleTabStore();
const formProps = ref(null);
const formId = ref(currentRoute.value?.params?.id);
const { notification } = useMessage();
const { t } = useI18n();
const hash = location.hash||location.pathname;
const mode = ref('read');
if (hash.indexOf('createForm') > 0) {
mode.value = 'create';
} else if (hash.indexOf('updateForm') > 0) {
mode.value = 'update';
} else if (hash.indexOf('viewForm') > 0) {
mode.value = 'view';
}
dynamicComponent.value = defineAsyncComponent({
loader: () => import(`./../../views/${pathArr[0]}/${pathArr[1]}/components/Form.vue`)
});
function onFormMounted(_formProps) {
formProps.value = _formProps;
setFormType();
}
async function setFormType() {
const _mode = mode.value;
if (_mode === 'create') {
return;
}
await nextTick();
if (_mode === 'view') {
await formRef.value.setDisabledForm();
}
await formRef.value.setFormDataFromId(formId.value);
}
function close() {
tabStore.closeTab(currentRoute.value, router);
}
async function handleSubmit() {
try {
const saveSuccess = await saveModal();
if (saveSuccess) {
notification.success({
message: 'Tip',
description: formType.value === '0' ? t('新增成功!') : t('修改成功!')
}); //提示消息
formRef.value.resetFields();
setTimeout(() => {
bus.emit(FORM_LIST_MODIFIED, { path: formPath, mode });
close();
}, 1000);
}
} finally {
}
}
async function saveModal() {
let saveSuccess = false;
const _mode = mode.value;
try {
const values = await formRef.value?.validate();
//添加隐藏组件
if (formProps.hiddenComponent?.length) {
formProps.hiddenComponent.forEach((component) => {
values[component.bindField] = component.value;
});
}
if (values !== false) {
try {
if (_mode === 'create') {
saveSuccess = await formRef.value.add(values);
} else {
saveSuccess = await formRef.value.update({ values, rowId: formId.value });
}
return saveSuccess;
} catch (error) { }
}
} catch (error) {
console.error('saveModal Error: ', error);
}
}
</script>
<style lang="less" scoped>
.page-bg-wrap {
background-color: #fff;
}
.top-toolbar {
min-height: 44px;
margin-bottom: 12px;
border-bottom: 1px solid #eee;
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<PageWrapper contentClass="flex" contentFullHeight dense fixedHeight>
<component :is="data.componentName" />
</PageWrapper>
</template>
<script lang="ts" setup>
import { reactive, shallowRef, defineAsyncComponent, unref, ref } from 'vue';
import { PageWrapper } from '/@/components/Page';
import ToDoTasks from '/@/views/workflow/task/components/processTasks/ToDoTasksV2.vue';
import { useRouter } from 'vue-router';
import { useI18n } from '/@/hooks/web/useI18n';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
const { t } = useI18n();
const { currentRoute } = useRouter();
const tabStore = useMultipleTabStore();
const FlowLaunch = defineAsyncComponent({
loader: () => import('../workflow/task/FlowLaunch.vue')
});
const MyProcess = defineAsyncComponent({
loader: () => import('../workflow/task/components/processTasks/MyProcessV2.vue')
});
const TaskDone = defineAsyncComponent({
loader: () => import('../workflow/task/components/processTasks/TaskDoneV2.vue')
});
const RecycleBin = defineAsyncComponent({
loader: () => import('../workflow/task/components/processTasks/RecycleBin.vue')
});
const MyCirculation = defineAsyncComponent({
loader: () => import('../workflow/task/components/processTasks/MyCirculation.vue')
});
const Drafts = defineAsyncComponent({
loader: () => import('../workflow/task/components/processTasks/DraftsV2.vue')
});
const componentByType: Map<string, any> = new Map([
['ToDoTasks', ToDoTasks],
['FlowLaunch', FlowLaunch],
['TaskDone', TaskDone],
['MyProcess', MyProcess],
['RecycleBin', RecycleBin],
['MyCirculation', MyCirculation],
['Drafts', Drafts]
]);
const treeData = [
{
key: 'ToDoTasks',
id: 'ToDoTasks',
name: t('待办任务'),
icon: 'ant-design:profile-outlined'
},
{
key: 'FlowLaunch',
id: 'FlowLaunch',
name: t('发起流程'),
icon: 'ant-design:profile-outlined'
},
{
key: 'TaskDone',
id: 'TaskDone',
name: t('已办任务'),
icon: 'ant-design:profile-outlined'
},
{
key: 'MyProcess',
id: 'MyProcess',
name: t('我的流程'),
icon: 'ant-design:profile-outlined'
},
{
key: 'MyCirculation',
id: 'MyCirculation',
name: t('我的传阅'),
icon: 'ant-design:profile-outlined'
},
{
key: 'Drafts',
id: 'Drafts',
name: t('草稿箱'),
icon: 'ant-design:profile-outlined'
},
{
key: 'RecycleBin',
id: 'RecycleBin',
name: t('回收站'),
icon: 'ant-design:profile-outlined'
}
];
const selectedKeys = ref(['ToDoTasks']);
const query = unref(currentRoute).query;
let id = 'ToDoTasks';
const lHash = location.hash||location.pathname;
if (lHash.indexOf('/draft') > 0) {
id = 'Drafts';
} else if (lHash.indexOf('/myProcess') > 0) {
id = 'MyProcess';
} else if (lHash.indexOf('/done') > 0) {
id = 'TaskDone';
}
let data = reactive({
componentName: shallowRef(ToDoTasks)
});
if (id) {
selectedKeys.value = [id.toString()];
handleSelect([id]);
}
function handleSelect(componentRow) {
if (componentRow.length > 0 && componentRow[0]) {
data.componentName = componentByType.has(componentRow[0]) ? componentByType.get(componentRow[0]) : ToDoTasks;
}
}
</script>
<style lang="less" scoped></style>