feat(系统管理): 组织模板工作流

This commit is contained in:
xinxin.wu 2023-11-03 14:38:42 +08:00 committed by Craftsman
parent 94c7a68df5
commit 1eb66b2a8e
24 changed files with 1174 additions and 37 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,11 @@
<svg width="112" height="64" viewBox="0 0 112 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="112" height="64" rx="4" fill="#F9F9FE"/>
<mask id="mask0_11587_359849" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="2" y="0" width="108" height="64">
<rect x="2" width="108" height="64" rx="4" fill="#F9F9FE"/>
</mask>
<g mask="url(#mask0_11587_359849)">
<rect opacity="0.3" width="16.3769" height="336.154" transform="matrix(0.80878 0.588111 -0.416709 0.90904 91.3379 -144.327)" fill="#EDEDF1"/>
<rect opacity="0.3" width="16.3769" height="336.154" transform="matrix(0.80878 0.588111 -0.416709 0.90904 126.385 -144.327)" fill="#EDEDF1"/>
<rect opacity="0.3" width="16.3769" height="336.154" transform="matrix(0.80878 0.588111 -0.416709 0.90904 161.645 -144.327)" fill="#EDEDF1"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 812 B

View File

@ -0,0 +1,23 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d_10176_404554)">
<circle cx="15" cy="15" r="8" fill="url(#paint0_linear_10176_404554)"/>
<circle cx="15" cy="15" r="7.5" stroke="white"/>
</g>
<path d="M16.24 12.184H15.056V11.632H19.032C19.032 14.376 18.992 16.072 18.912 16.72C18.816 17.712 18.392 18.216 17.624 18.216C17.448 18.216 17.144 18.2 16.712 18.184L16.568 17.656C16.944 17.672 17.264 17.688 17.536 17.688C18.008 17.688 18.28 17.288 18.352 16.488C18.4 16.016 18.432 14.584 18.448 12.184H16.808V12.816C16.776 15.336 16.088 17.176 14.744 18.344L14.304 17.96C15.568 16.888 16.216 15.176 16.24 12.816V12.184ZM13.168 15.144C12.864 15.48 12.536 15.808 12.168 16.144L12 15.56C13.032 14.616 13.72 13.688 14.056 12.792H12.176V12.264H13.256C13.128 11.88 12.968 11.512 12.776 11.152L13.336 11C13.536 11.416 13.688 11.8 13.792 12.152L13.512 12.264H14.616V12.736C14.424 13.312 14.128 13.888 13.72 14.456V14.512C13.864 14.608 13.992 14.704 14.12 14.808C14.392 14.616 14.664 14.32 14.92 13.936L15.272 14.288C15.032 14.624 14.768 14.896 14.472 15.096C14.704 15.296 14.92 15.504 15.12 15.728L14.808 16.208C14.448 15.744 14.088 15.36 13.72 15.04V18.352H13.168V15.144Z" fill="#783887"/>
<defs>
<filter id="filter0_d_10176_404554" x="0" y="0" width="30" height="30" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="3.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0.0571092 0 0 0 0 0.00126737 0 0 0 0 0.304167 0 0 0 0.09 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_10176_404554"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_10176_404554" result="shape"/>
</filter>
<linearGradient id="paint0_linear_10176_404554" x1="7" y1="15" x2="23" y2="15" gradientUnits="userSpaceOnUse">
<stop offset="0.0336452" stop-color="#F2E9F6"/>
<stop offset="1" stop-color="white"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -44,6 +44,7 @@ export interface SearchKeyType {
type: string; // Vue控件名称
label: string; // 显示名称
rules?: FieldRule[];
request?: any;
props?: {
[key: string]: string | number | boolean;
};

View File

@ -159,7 +159,7 @@
<div
v-if="showBatchAction || attrs.showPagination"
class="mt-[16px] flex h-[32px] w-[100%] flex-row flex-nowrap items-center justify-end px-0"
:class="{ 'justify-between': showBatchAction, 'min-w-[952px]': attrs.selectable }"
:class="{ 'justify-between': showBatchAction, 'min-w-[934px]': attrs.selectable }"
>
<batch-action
v-if="showBatchAction"

View File

@ -63,6 +63,7 @@ export enum SettingRouteEnum {
SETTING_ORGANIZATION_TEMPLATE_FILED_SETTING = 'settingOrganizationTemplateFiledSetting',
SETTING_ORGANIZATION_TEMPLATE_MANAGEMENT = 'settingOrganizationTemplateManagement',
SETTING_ORGANIZATION_TEMPLATE_MANAGEMENT_DETAIL = 'settingOrganizationTemplateManagementDetail',
SETTING_ORGANIZATION_TEMPLATE_MANAGEMENT_WORKFLOW = 'settingOrganizationTemplateWorkFlow',
SETTING_ORGANIZATION_SERVICE = 'settingOrganizationService',
SETTING_ORGANIZATION_LOG = 'settingOrganizationLog',
}

View File

@ -146,14 +146,14 @@ export type StateList = {
};
// 设置工作流状态初始或结束
export interface SetStateType {
scopeId: string;
scene: string;
statusDefinitions: StateList[];
statusId: string;
definitionId: string;
enable: boolean;
}
// 更新流转状态
export interface UpdateWorkFlowSetting {
scopeId: string;
scene: string;
statusFlows: { fromId: string; toId: string }[];
fromId: string;
toId: string;
enable: boolean;
}

View File

@ -255,6 +255,26 @@ const Setting: AppRouteRecordRaw = {
],
},
},
// 模板列表-模板管理-工作流
{
path: 'templateWorkFlow',
name: SettingRouteEnum.SETTING_ORGANIZATION_TEMPLATE_MANAGEMENT_WORKFLOW,
component: () => import('@/views/setting/organization/template/components/workflowTable.vue'),
meta: {
locale: 'menu.settings.organization.templateManagementWorkFlow',
roles: ['*'],
breadcrumbs: [
{
name: SettingRouteEnum.SETTING_ORGANIZATION_TEMPLATE,
locale: 'menu.settings.organization.template',
},
{
name: SettingRouteEnum.SETTING_ORGANIZATION_TEMPLATE_MANAGEMENT_WORKFLOW,
locale: 'menu.settings.organization.templateManagementWorkFlow',
},
],
},
},
{
path: 'log',
name: SettingRouteEnum.SETTING_ORGANIZATION_LOG,

View File

@ -28,6 +28,21 @@ const useTemplateStore = defineStore('template', {
console.log(error);
}
},
// 获取项目模板状态
getProjectTemplateState() {
const { FUNCTIONAL, API, UI, TEST_PLAN, BUG } = this.templateStatus;
return {
FUNCTIONAL: !FUNCTIONAL,
API: !API,
UI: !UI,
TEST_PLAN: !TEST_PLAN,
BUG: !BUG,
} as Record<string, boolean>;
},
// 获取组织模板状态
getOrdTemplateState() {
return this.templateStatus;
},
},
});

View File

@ -22,7 +22,7 @@
></a-input-search
></div>
</div>
<ms-base-table
<MsBaseTable
v-bind="propsRes"
:action-config="tableBatchActions"
@selected-change="handleTableSelect"
@ -67,7 +67,7 @@
@ok="removeMember(record)"
/>
</template>
</ms-base-table>
</MsBaseTable>
<AddMemberModal
ref="projectMemberRef"
v-model:visible="addMemberVisible"
@ -87,7 +87,7 @@
/**
* @description 项目管理-项目与权限-成员
*/
import { onBeforeMount, ref } from 'vue';
import { onMounted, ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
@ -108,7 +108,7 @@
} from '@/api/modules/project-management/projectMember';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { useAppStore, useTableStore } from '@/store';
import { useAppStore } from '@/store';
import { characterLimit } from '@/utils';
import type {
@ -123,22 +123,20 @@
const { openModal } = useModal();
const appStore = useAppStore();
const tableStore = useTableStore();
const lastProjectId = computed(() => appStore.getCurrentProjectId);
const columns: MsTableColumn = [
{
title: 'project.member.tableColumnEmail',
dataIndex: 'email',
showInTable: true,
width: 200,
showTooltip: true,
},
{
title: 'project.member.tableColumnName',
dataIndex: 'name',
showInTable: true,
width: 200,
showTooltip: true,
fixed: 'left',
},
{
title: 'project.member.tableColumnEmail',
dataIndex: 'email',
showInTable: true,
showTooltip: true,
},
{
@ -159,6 +157,7 @@
slotName: 'enable',
dataIndex: 'enable',
showInTable: true,
width: 150,
},
{
title: 'project.member.tableColumnActions',
@ -168,7 +167,6 @@
showInTable: true,
},
];
await tableStore.initColumn(TableKeyEnum.PROJECT_MEMBER, columns, 'drawer');
const tableBatchActions = {
baseAction: [
@ -193,7 +191,7 @@
tableKey: TableKeyEnum.PROJECT_MEMBER,
selectable: true,
showSetting: true,
size: 'default',
columns,
scroll: {
x: 1200,
},
@ -355,8 +353,7 @@
const userGroupAll = ref<ProjectUserOption[]>([]);
onMounted(async () => {
initData();
const initOptions = async () => {
userGroupOptions.value = await getProjectUserGroup(lastProjectId.value);
userGroupAll.value = [
{
@ -365,6 +362,11 @@
},
...userGroupOptions.value,
];
};
onMounted(() => {
initData();
initOptions();
});
</script>

View File

@ -220,7 +220,7 @@
};
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getMemberList, {
tableKey: TableKeyEnum.ORGANIZATION_MEMBER,
scroll: { x: 2000 },
scroll: { x: 1800 },
selectable: true,
showSetting: true,
size: 'default',

View File

@ -0,0 +1,135 @@
<template>
<MsDialog
v-model:visible="visible"
dialog-size="medium"
:title="title"
:ok-text="isEdit ? 'common.update' : 'common.create'"
:confirm="confirmHandler"
:close="closeHandler"
:switch-props="{
switchName: t('system.orgTemplate.anyStateToAll'),
switchTooltip: t('system.orgTemplate.stateTip'),
showSwitch: isEdit ? false : true,
enable: form.allTransferTo,
}"
>
<div class="form">
<a-form ref="formRef" :model="form" size="large" layout="vertical">
<a-form-item
field="name"
:label="t('system.orgTemplate.stateName')"
asterisk-position="end"
:rules="[{ required: true, message: t('system.orgTemplate.stateNameNotNull') }]"
>
<a-input
v-model="form.name"
show-word-limit
:max-length="8"
:placeholder="t('system.orgTemplate.stateNameDescription')"
></a-input>
</a-form-item>
<a-form-item field="remark" :label="t('system.orgTemplate.description')" asterisk-position="end">
<a-textarea
v-model:model-value="form.remark"
:max-length="250"
:placeholder="t('system.config.auth.descPlaceholder')"
allow-clear
></a-textarea>
</a-form-item>
</a-form>
</div>
</MsDialog>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { FormInstance, Message } from '@arco-design/web-vue';
import MsDialog from '@/components/pure/ms-dialog/index.vue';
import { createWorkFlowStatus, updateWorkFlowStatus } from '@/api/modules/setting/template';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import type { OrdWorkStatus } from '@/models/setting/template';
const appStore = useAppStore();
const currentOrgId = computed(() => appStore.currentOrgId);
const route = useRoute();
const { t } = useI18n();
const emits = defineEmits<{
(e: 'update:visible', visible: boolean): void;
(e: 'success'): void;
}>();
const visible = ref<boolean>(false);
const initFormValue: OrdWorkStatus = {
scopeId: currentOrgId.value,
id: '',
name: '',
scene: route.query.type,
remark: '',
allTransferTo: false, //
};
const form = ref({ ...initFormValue });
const formRef = ref<FormInstance | null>(null);
const closeHandler = () => {
formRef.value?.resetFields();
visible.value = false;
form.value = { ...initFormValue };
};
const title = computed(() => {
return form.value.id ? t('system.orgTemplate.updateWorkFlowStatus') : t('system.orgTemplate.addWorkFlowStatus');
});
const isEdit = computed(() => !!form.value.id);
//
const confirmHandler = async (enable: boolean | undefined) => {
await formRef.value?.validate().then(async (error) => {
if (!error) {
try {
if (!isEdit.value) {
form.value.allTransferTo = enable as boolean;
}
if (!form.value.id) {
await createWorkFlowStatus(form.value);
Message.success(t('system.orgTemplate.addSuccessState'));
} else {
await updateWorkFlowStatus(form.value);
Message.success(t('system.orgTemplate.updateSuccess'));
}
closeHandler();
emits('success');
} catch (e) {
console.log(e);
}
} else {
return false;
}
});
};
//
function handleEdit(record: OrdWorkStatus) {
visible.value = true;
form.value.name = record.name;
form.value.id = record.id;
form.value.remark = record.remark;
}
watch(
() => visible.value,
(val) => {
emits('update:visible', val);
}
);
defineExpose({
handleEdit,
});
</script>
<style scoped></style>

View File

@ -47,6 +47,8 @@ export const getFieldType = (selectFieldType: FormItemType) => {
}
};
const organizationState = computed(() => templateStore.getOrdTemplateState());
const projectState = computed(() => templateStore.getProjectTemplateState());
// 模板列表Icon
export const cardList = [
{
@ -86,6 +88,55 @@ export const cardList = [
},
];
export function getCardList(type: string): Record<string, any>[] {
const dataList = [
{
id: 1001,
key: 'FUNCTIONAL',
value: TemplateCardEnum.FUNCTIONAL,
name: t('system.orgTemplate.caseTemplates'),
},
{
id: 1002,
key: 'API',
value: TemplateCardEnum.API,
name: t('system.orgTemplate.APITemplates'),
},
{
id: 1003,
key: 'UI',
value: TemplateCardEnum.UI,
name: t('system.orgTemplate.UITemplates'),
},
{
id: 1004,
key: 'TEST_PLAN',
value: TemplateCardEnum.TEST_PLAN,
name: t('system.orgTemplate.testPlanTemplates'),
},
{
id: 1005,
key: 'BUG',
value: TemplateCardEnum.BUG,
name: t('system.orgTemplate.defectTemplates'),
},
];
if (type === 'organization') {
return dataList.map((item) => {
return {
...item,
enable: organizationState.value[item.key],
};
});
}
return dataList.map((item) => {
return {
...item,
enable: projectState.value[item.key],
};
});
}
// table名称展示图标类型表格展示类型
export const fieldIconAndName: fieldIconAndNameModal[] = [
{

View File

@ -79,7 +79,7 @@
import type { AddOrUpdateField, SeneType } from '@/models/setting/template';
import { TableKeyEnum } from '@/enums/tableEnum';
import { cardList, getIconType } from './fieldSetting';
import { getCardList, getIconType } from './fieldSetting';
const templateStore = useTemplateStore();
@ -172,7 +172,7 @@
const tableRef = ref();
const isEnable = computed(() => {
return templateStore.templateStatus[scene.value as string];
}); //
});
//
const isEnableOperation = () => {
@ -241,7 +241,7 @@
//
const updateBreadcrumbList = () => {
const { breadcrumbList } = appStore;
const breadTitle = cardList.find((item) => item.key === route.query.type);
const breadTitle = getCardList('organization').find((item) => item.key === route.query.type);
if (breadTitle) {
breadcrumbList[0].locale = breadTitle.name;
appStore.setBreadcrumbList(breadcrumbList);

View File

@ -87,7 +87,7 @@
import type { ActionTemplateManage, CustomField, DefinedFieldItem } from '@/models/setting/template';
import { SettingRouteEnum } from '@/enums/routeEnum';
import { cardList } from './fieldSetting';
import { getCardList } from './fieldSetting';
const { t } = useI18n();
const route = useRoute();
@ -296,7 +296,7 @@
// title
const breadTitle = computed(() => {
const firstBreadTitle = cardList.find((item) => item.key === route.query.type)?.name;
const firstBreadTitle = getCardList('organization').find((item) => item.key === route.query.type)?.name;
const ThirdBreadTitle = title.value;
return {
firstBreadTitle,

View File

@ -20,7 +20,7 @@
<a-divider v-if="!props.cardItem.enable || props.cardItem.key === 'BUG'" direction="vertical" />
</span>
<span v-if="props.cardItem.key === 'BUG'" class="operation hover:text-[rgb(var(--primary-5))]">
<span>{{ t('system.orgTemplate.workflowSetup') }}</span>
<span @click="workflowSetup">{{ t('system.orgTemplate.workflowSetup') }}</span>
<a-divider v-if="!props.cardItem.enable && props.cardItem.key === 'BUG'" direction="vertical" />
</span>
<span v-if="!props.cardItem.enable" class="rounded p-[2px] hover:bg-[rgb(var(--primary-9))]">
@ -44,11 +44,15 @@
import { isEnableTemplate } from '@/api/modules/setting/template';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import useTemplateStore from '@/store/modules/setting/template';
import { SettingRouteEnum } from '@/enums/routeEnum';
const { t } = useI18n();
const appStore = useAppStore();
const templateStore = useTemplateStore();
const currentOrgId = computed(() => appStore.currentOrgId);
const props = defineProps<{
cardItem: Record<string, any>;
@ -67,8 +71,9 @@
//
const enableHandler = async () => {
try {
await isEnableTemplate(appStore.currentOrgId);
await isEnableTemplate(currentOrgId.value);
Message.success(t('system.orgTemplate.enabledSuccessfully'));
templateStore.getStatus();
} catch (error) {
console.log(error);
}
@ -98,6 +103,15 @@
},
});
};
const workflowSetup = () => {
router.push({
name: SettingRouteEnum.SETTING_ORGANIZATION_TEMPLATE_MANAGEMENT_WORKFLOW,
query: {
type: props.cardItem.key,
},
});
};
</script>
<style scoped lang="less">

View File

@ -64,7 +64,7 @@
import { SettingRouteEnum } from '@/enums/routeEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
import { cardList } from './fieldSetting';
import { getCardList } from './fieldSetting';
const route = useRoute();
const { t } = useI18n();
@ -219,7 +219,7 @@
//
const updateBreadcrumbList = () => {
const { breadcrumbList } = appStore;
const breadTitle = cardList.find((item) => item.key === route.query.type);
const breadTitle = getCardList('organization').find((item: any) => item.key === route.query.type);
if (breadTitle) {
breadcrumbList[0].locale = breadTitle.name;
appStore.setBreadcrumbList(breadcrumbList);

View File

@ -0,0 +1,380 @@
<template>
<div
class="wrapper"
:class="{
...styleClass.wrapper,
_hover_Wrapper: isEnableProjectState || isNotAllowCreate ? false : true,
_pointer: !isNotAllowCreate,
_not_allowed: isNotAllowCreate,
_disabled_gray_bg: isEnableProjectState,
}"
>
<!-- 不允许状态流转 -->
<img v-if="isNotAllowCreate" src="@/assets/images/notAllow_bg.png" class="h-[100%] w-[100%]" alt="" />
<!-- 未创建 hover 禁用 选中 -->
<div v-else-if="isUnCreateWorkFlow" class="action" @click="createFlowStep">
<icon-plus
:style="{ 'font-size': '16px' }"
class="_unSelect_SvgIcon"
:class="{ ...styleClass.SvgIcon, _hover_SvgIcon: isEnableProjectState ? false : true }"
/>
<span
class="_unSelect_CreateStep"
:class="{ ...styleClass.createStep, _hover_CreateStep: isEnableProjectState ? false : true }"
@click="createFlowStep"
>{{ t('system.orgTemplate.createFlowStep') }}</span
>
</div>
<!-- 已创建 -->
<div v-else-if="isCreated" class="created flex h-full w-full items-center justify-center" @click="createFlowStep">
<icon-check :style="{ 'font-size': '16px' }" class="text-[rgb(var(--success-6))]" />
</div>
<a-modal
v-model:visible="visible"
title-align="start"
:class="['ms-modal-form']"
:width="400"
@cancel="handleCancel"
>
<template #title> {{ title }} </template>
<div class="flex w-[60%] items-center justify-between text-[var(--color-text-1)]">
<div class="flex flex-col">
<span class="mb-2">{{ t('system.orgTemplate.startState') }} </span>
<MsTag>{{ startState }}</MsTag>
</div>
<icon-arrow-right :style="{ 'font-size': '16px' }" class="mt-8 text-[var(--color-text-brand)]" />
<div class="flex flex-col">
<span class="mb-2"> {{ t('system.orgTemplate.endState') }}</span>
<MsTag>{{ endState }}</MsTag>
</div>
</div>
<template #footer>
<a-button @click="handleCancel">{{ t('common.cancel') }}</a-button>
<a-button v-if="isUnCreateWorkFlow" type="primary" :loading="loading" @click="changeWorkFlow('create')">{{
t('common.create')
}}</a-button>
<a-button v-else type="primary" status="danger" class="!bg-[rgb(var(--danger-7))]" @click="cancelFlowStep">{{
t('system.orgTemplate.deleteSteps')
}}</a-button>
</template>
</a-modal>
</div>
</template>
<script setup lang="ts">
/**
* @description 系统设置-组织-工作流table小卡片
*/
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { Message, TableColumnData } from '@arco-design/web-vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { updateOrdWorkStateFlow } from '@/api/modules/setting/template';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useTemplateStore from '@/store/modules/setting/template';
import type { UpdateWorkFlowSetting, WorkFlowType } from '@/models/setting/template';
const templateStore = useTemplateStore();
const { t } = useI18n();
const { openModal } = useModal();
const props = defineProps<{
stateItem: WorkFlowType;
columnItem: TableColumnData;
cellCoordinates: { rowId: string; columnId: string };
totalData: WorkFlowType[];
}>();
const emit = defineEmits<{
(e: 'ok'): void;
}>();
const stateInfo = ref<WorkFlowType>();
const stateColumn = ref<TableColumnData>();
watchEffect(() => {
stateInfo.value = { ...props.stateItem };
stateColumn.value = { ...props.columnItem };
});
//
const isNotAllowCreate = computed(() => {
return props.stateItem.id === props.columnItem.dataIndex;
});
// +
const isUnCreateWorkFlow = computed(() => {
if (props.columnItem.dataIndex)
return (
props.stateItem.statusFlowTargets.length < 1 ||
!props.stateItem.statusFlowTargets.includes(props.columnItem.dataIndex)
);
});
//
const isCreated = computed(() => {
if (props.columnItem.dataIndex) return props.stateItem.statusFlowTargets.includes(props.columnItem.dataIndex);
});
//
const startState = computed(() => {
const startStatus = props.totalData.find((item: WorkFlowType) => item.id === props.stateItem.id);
if (startStatus) {
return startStatus.name;
}
});
//
const endState = computed(() => {
const endStatus = props.totalData.find((item: WorkFlowType) => item.id === props.columnItem.dataIndex);
if (endStatus) {
return endStatus.name;
}
});
//
const isEnableProjectState = computed(() => {
const projectState = templateStore.getProjectTemplateState();
return projectState[props.stateItem.scene];
});
const title = computed(() => {
return isCreated.value ? t('system.orgTemplate.updateFlowStep') : t('system.orgTemplate.createFlowStep');
});
const isSelected = ref<boolean>(false);
const styleClass = ref<Record<string, any>>({});
// class
function setSelectClass() {
if (isUnCreateWorkFlow.value && isSelected.value) {
styleClass.value = {
wrapper: { _select_unCreate_Selected: true },
createStep: { _selected_CreateStep: true },
SvgIcon: { _selected_SvgIcon: true },
};
} else if (isCreated.value && !isNotAllowCreate.value && isSelected.value) {
styleClass.value = {
wrapper: { _select_created_Selected: true },
createStep: { _selected_CreateStep: true },
SvgIcon: { _selected_SvgIcon: true },
};
} else if (isUnCreateWorkFlow.value && !isSelected.value) {
styleClass.value = {
wrapper: { _select_unCreate_Selected: false },
createStep: { _selected_CreateStep: false },
SvgIcon: { _selected_SvgIcon: false },
};
} else if (isCreated.value && !isNotAllowCreate.value && !isSelected.value) {
styleClass.value = {
wrapper: { _select_created_Selected: false },
createStep: { _selected_CreateStep: false },
SvgIcon: { _selected_SvgIcon: false },
};
} else {
styleClass.value = {};
}
}
watch(
() => props.cellCoordinates,
() => {
if (
props.cellCoordinates.rowId === props.stateItem.id &&
props.cellCoordinates.columnId === props.columnItem.dataIndex
) {
isSelected.value = true;
} else {
isSelected.value = false;
}
setSelectClass();
},
{ deep: true }
);
watch(
() => isUnCreateWorkFlow.value,
() => {
setSelectClass();
}
);
watch(
() => isCreated.value,
() => {
setSelectClass();
}
);
watch(
() => isSelected.value,
() => {
setSelectClass();
}
);
const visible = ref<boolean>(false);
//
function createFlowStep() {
if (isEnableProjectState.value) {
return;
}
visible.value = true;
}
const loading = ref<boolean>(false);
//
async function changeWorkFlow(type: string) {
try {
loading.value = true;
const params: UpdateWorkFlowSetting = {
fromId: props.stateItem?.id,
toId: props.columnItem?.dataIndex as string,
enable: type === 'create' ? true : props.stateItem?.statusDefinitions.join().includes('END'),
};
await updateOrdWorkStateFlow(params);
Message.success(
type === 'delete' ? t('system.orgTemplate.deleteSuccess') : t('system.orgTemplate.createSuccess')
);
visible.value = false;
emit('ok');
isSelected.value = false;
} catch (error) {
console.log(error);
} finally {
loading.value = false;
}
}
//
function cancelFlowStep() {
if (isEnableProjectState.value) {
return;
}
openModal({
type: 'error',
title: t('system.orgTemplate.deleteStateStepTitle', { name: stateInfo.value?.name }),
content: t('system.orgTemplate.deleteStateStepContent'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
try {
await changeWorkFlow('delete');
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
function handleCancel() {
visible.value = false;
}
</script>
<style scoped lang="scss">
.wrapper {
width: 100%;
min-width: 112px;
height: 100%;
transition: 0.2;
@apply flex items-center justify-center rounded;
.img {
object-fit: cover;
}
.action {
width: 100%;
@apply flex flex-col items-center justify-center;
}
&:hover {
._hover_CreateStep {
opacity: 1;
}
}
&:hover {
._hover_SvgIcon {
margin-top: 0;
color: rgb(var(--primary-5));
}
}
}
// hover
._hover_Wrapper {
@apply hover:shadow-xl;
&:hover {
border-radius: 4px;
}
}
// hover----
._hover_CreateStep {
&:hover {
opacity: 1;
}
}
._hover_SvgIcon {
&:hover {
margin-top: 0;
color: rgb(var(--primary-5));
}
}
// ----
._unSelect_SvgIcon {
margin-top: 30px;
color: var(--color-text-brand);
}
._unSelect_CreateStep {
margin-top: 8px;
color: rgb(var(--primary-5));
opacity: 0;
}
._selected_CreateStep {
opacity: 1;
}
._selected_SvgIcon {
margin-top: 0;
color: rgb(var(--primary-5));
}
// ----
//
._select_unCreate_Selected {
background: var(--color-bg-3);
box-shadow: none;
}
//
._select_created_Selected {
border: 1px solid rgb(var(--primary-5));
background: none;
box-shadow: none;
}
//
._disabled_unCreate {
background: var(--color-text-n8);
}
//
._pointer {
@apply cursor-pointer;
}
._not_allowed {
@apply cursor-not-allowed;
}
//
._disabled_gray_bg {
background: var(--color-text-n8);
@apply cursor-not-allowed;
}
</style>

View File

@ -0,0 +1,454 @@
<template>
<MsCard has-breadcrumb simple>
<a-alert class="mb-6" type="warning">{{ t('system.orgTemplate.workFlowTip') }}</a-alert>
<div class="mb-4">
<div class="mb-4 flex items-center"
><a-button class="mr-2" type="outline" @click="addStatus">{{ t('system.orgTemplate.addState') }}</a-button>
<a-popover title="" position="right">
<MsButton class="!mr-1">{{ t('system.orgTemplate.example') }}</MsButton>
<template #content>
<div class="w-[410px] bg-[var(--color-bg-3)] p-[16px]">
<img src="@/assets/images/schematicDrawing.png" alt="" />
</div>
</template>
</a-popover>
<a-tooltip :content="t('system.orgTemplate.workFlowTip')">
<icon-exclamation-circle class="text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]" />
<template #content>
<div class="whitespace-nowrap">{{ t('system.orgTemplate.workFlowToolTip') }}</div>
<div>{{ t('system.orgTemplate.workFlowToolTipHover') }}</div>
</template>
</a-tooltip>
</div>
<a-table
:columns="workFlowColumns"
:data="dataList"
row-key="id"
:bordered="{ cell: true }"
:hoverable="false"
:pagination="false"
:draggable="{ type: 'handle', width: 39 }"
:loading="tableLoading"
@change="handleChange"
>
<template #columns>
<a-table-column
v-for="column in workFlowColumns"
:key="column.dataIndex"
:data-index="column.dataIndex"
:title="(column?.title as string)"
:header-cell-class="column.headerCellClass"
:fixed="column.fixed"
>
<template #title>
<div v-if="column.dataIndex !== 'statusName'" class="w-full">
<MsTag class="relative" size="large" theme="light">{{ column.title }} </MsTag></div
>
<div v-else class="splitBox">
<div class="startStatus"> {{ t('system.orgTemplate.startState') }} </div>
<div class="line"></div>
<div class="endStatus"> {{ t('system.orgTemplate.endState') }} </div>
</div>
</template>
<template #cell="{ record }">
<div v-if="column.dataIndex === 'statusName'">
<div class="flex items-center justify-between">
<div class="relative">
<MsTag class="relative" size="large" theme="light">{{ record.name }}</MsTag>
<span v-if="record.statusDefinitions.join() === 'START'" class="absolute -top-6 left-7">
<svg-icon width="36px" height="36px" class="inline-block text-[white]" name="start"></svg-icon
></span>
</div>
<div class="action mr-2 flex h-8 w-8 items-center justify-center rounded opacity-0">
<MsTableMoreAction
:list="getMoreActions(record)"
@select="(item) => handleMoreActionSelect(item, record)"
></MsTableMoreAction
></div>
</div>
</div>
<div v-else class="!h-[82px] min-w-[116px] p-[2px]">
<WorkflowCard
:column-item="column"
:state-item="record"
:cell-coordinates="cellCoordinates"
:total-data="dataList"
@click="selectCard(record, column.dataIndex)"
@ok="getWorkFetchList()"
/>
</div>
</template>
</a-table-column>
<a-table-column :title="t('system.orgTemplate.operation')" :width="360" header-cell-class="splitOperation">
<template #cell="{ record }">
<div class="flex">
<MsButton class="!mr-0 ml-4" @click="editWorkStatus(record)">{{ t('common.edit') }}</MsButton>
<a-divider direction="vertical" />
<a-checkbox v-model="record.currentState" @change="(value) => changeState(value, record)">
<MsButton>{{ t('system.orgTemplate.endState') }}</MsButton></a-checkbox
>
<MsButton class="!mr-0 ml-4" @click="detailWorkStatus(record)">{{
t('system.orgTemplate.details')
}}</MsButton>
</div>
</template>
</a-table-column>
</template>
</a-table>
<div class="mt-4 flex items-center text-[var(--color-text-4)]">
<span>tips: </span>
<MsIcon type="icon-icon_drag" class="mx-4 text-[16px] text-[var(--color-text-4)]" />
<span>{{ t('system.orgTemplate.anyStateToAll') }}</span>
<a-popover title="" position="right">
<MsButton class="!mr-0 ml-2">{{ t('system.orgTemplate.example') }}</MsButton>
<template #content>
<div class="bg-[var(--color-bg-3)]">
<img src="@/assets/images/colorSelect.png" alt="" />
</div>
</template>
</a-popover>
</div>
<AddWorkStatusModal ref="addWorkStateRef" v-model:visible="showModel" @success="getWorkFetchList()" />
<MsDrawer
ref="detailDrawerRef"
v-model:visible="showDetailVisible"
:width="480"
:footer="false"
:title="t('system.orgTemplate.stateDetail', { name: detailInfo?.name })"
>
<div class="flex p-4">
<div class="flex w-[40%] flex-col">
<span class="label">{{ t('system.orgTemplate.stateName') }}</span>
<span class="label">{{ t('system.orgTemplate.description') }}</span>
</div>
<div class="flex w-[60%] flex-col">
<span class="content">{{ detailInfo?.name }}</span>
<span class="content">{{ detailInfo?.remark || '-' }}</span>
</div>
</div>
</MsDrawer>
</div>
</MsCard>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { Message, TableColumnData, TableData } from '@arco-design/web-vue';
import { isEqual } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import AddWorkStatusModal from './addWorkStatusModal.vue';
import WorkflowCard from './workflowCard.vue';
import {
deleteOrdWorkState,
getWorkFlowList,
setOrdWorkState,
setOrdWorkStateSort,
} from '@/api/modules/setting/template';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import { useAppStore } from '@/store';
import useTemplateStore from '@/store/modules/setting/template';
import { characterLimit } from '@/utils';
import type { SetStateType, WorkFlowType } from '@/models/setting/template';
const { t } = useI18n();
const appStore = useAppStore();
const templateStore = useTemplateStore();
const currentOrgId = computed(() => appStore.currentOrgId);
const { openModal } = useModal();
const route = useRoute();
const defaultStatusColumn: TableColumnData = {
title: '',
dataIndex: 'statusName',
fixed: 'left',
width: 196,
headerCellClass: 'splitTitle',
};
//
const isEnableProjectState = computed(() => {
const projectState = templateStore.getProjectTemplateState();
return projectState[route.query.type as string];
});
const dataList = ref<any>([]);
//
const workData = ref<WorkFlowType[]>([]);
// '
const workFlowColumns = ref<TableColumnData[]>([]);
function getMoreActions(record: WorkFlowType) {
const moreActions: ActionsItem[] = [
{
label: 'system.orgTemplate.setInitState',
eventTag: 'setInit',
disabled: record.statusDefinitions.join().includes('START'),
},
{
isDivider: true,
},
{
label: 'system.orgTemplate.delete',
eventTag: 'delete',
danger: true,
},
];
return moreActions;
}
const tableLoading = ref<boolean>(false);
// table
async function getWorkFetchList() {
try {
tableLoading.value = true;
workData.value = await getWorkFlowList(currentOrgId.value, route.query.type);
workFlowColumns.value = workData.value.map((item, index) => {
const columns = {
title: item.name,
dataIndex: item.id,
};
return columns;
});
workFlowColumns.value.splice(0, 0, defaultStatusColumn);
dataList.value = workData.value.map((item, index) => {
if (index === 0) {
return {
statusName: item.name,
...item,
[item.id]: item.name,
index,
};
}
return {
...item,
[item.id]: item.name,
index,
currentState: item.statusDefinitions.join().includes('END'),
};
});
} catch (error) {
console.log(error);
} finally {
tableLoading.value = false;
}
}
const addWorkStateRef = ref();
const showModel = ref<boolean>(false);
//
function editWorkStatus(record: WorkFlowType) {
showModel.value = true;
addWorkStateRef.value.handleEdit(record);
}
//
function addStatus() {
showModel.value = true;
}
//
function deleteHandler(record: WorkFlowType) {
if (record.statusDefinitions.join().includes('START')) {
Message.warning(t('system.orgTemplate.noAllowDeleteInitState'));
return;
}
openModal({
type: 'error',
title: t('system.orgTemplate.deleteStateTitle', { name: characterLimit(record.name) }),
content: t('system.orgTemplate.deleteStateContent'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
onBeforeOk: async () => {
try {
if (record.id) await deleteOrdWorkState(record.id);
Message.success(t('system.orgTemplate.deleteSuccess'));
getWorkFetchList();
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
// ||
async function setState(record: WorkFlowType, type: string) {
const params: SetStateType = {
statusId: record.id,
definitionId: type,
enable: type === 'START' ? true : record.currentState,
};
try {
await setOrdWorkState(params);
Message.success(
type === 'END' ? t('system.orgTemplate.setEndStateSuccess') : t('system.orgTemplate.setInitStateSuccess')
);
getWorkFetchList();
} catch (error) {
console.log(error);
}
}
function handleMoreActionSelect(item: ActionsItem, record: WorkFlowType) {
if (item.eventTag === 'delete') {
deleteHandler(record);
} else {
setState(record, 'START');
}
}
//
async function handleChange(_data: TableData[]) {
const originIds = dataList.value.map((item: any) => item.id);
dataList.value = _data;
const dataIds = _data.map((item: any) => item.id);
const isChange = isEqual(originIds, dataIds);
if (isChange) return {};
try {
await setOrdWorkStateSort(currentOrgId.value, route.query.type, dataIds);
getWorkFetchList();
} catch (error) {
console.log(error);
}
}
// &
function changeState(value: any, record: WorkFlowType) {
setState(record, 'END');
}
// card
const cellCoordinates = ref<{ rowId: string; columnId: string }>({
rowId: '',
columnId: '',
});
//
function selectCard(record: WorkFlowType, columnDataIndex: string | undefined) {
if (isEnableProjectState.value) {
return;
}
cellCoordinates.value = {
rowId: '',
columnId: '',
};
cellCoordinates.value = {
rowId: record.id,
columnId: columnDataIndex || '',
};
}
const showDetailVisible = ref<boolean>(false);
const detailInfo = ref();
//
function detailWorkStatus(record: WorkFlowType) {
showDetailVisible.value = true;
detailInfo.value = { ...record };
}
onBeforeMount(() => {
getWorkFetchList();
});
</script>
<style scoped lang="less">
:deep(.arco-table-border) {
border: 1px solid var(--color-text-n8) !important;
}
:deep(.arco-table .arco-table-td) {
height: 82px;
}
:deep(.arco-table-tr .arco-table-th) {
height: 82px;
color: var(--color-text-3);
}
:deep(.arco-table .arco-table-cell) {
padding: 0 !important;
}
:deep(.arco-table-cell-align-left) {
justify-content: center;
text-align: center;
}
:deep(.arco-table-cell-align-left):last-of-type {
justify-content: center;
text-align: center;
}
:deep(.arco-table-th.splitTitle .arco-table-cell-align-left) {
justify-content: start;
}
:deep(.arco-table-th.splitOperation .arco-table-cell-align-left) {
justify-content: start;
.arco-table-th-title {
padding-left: 16px !important;
}
}
:deep(.arco-table-tr) {
.splitTitle {
width: 196px !important;
min-width: 196px !important;
}
}
:deep(.arco-table-drag-handle) {
border-right: 1px solid transparent !important;
.arco-icon-drag-dot-vertical {
color: var(--color-text-brand);
}
&:hover + .arco-table-td .action {
background: rgb(var(--primary-9));
opacity: 1;
transition: 0.1;
}
& + .arco-table-td:hover .action {
background: rgb(var(--primary-9));
opacity: 1;
transition: 0.1;
}
}
:deep(.arco-table-operation) {
border-right: 1px solid transparent !important;
}
.startStatus {
position: absolute;
bottom: 16px;
left: -24px;
}
.endStatus {
position: absolute;
top: 16px;
right: 20px;
}
.line {
position: absolute;
left: -35px;
width: 117%;
height: 1px;
background: var(--color-text-n8);
transform: rotateZ(18deg);
}
.label {
margin-top: 16px;
color: var(--color-text-3);
}
.content {
margin-top: 16px;
color: var(--color-text-1);
}
</style>

View File

@ -14,7 +14,7 @@
:card-min-width="360"
class="flex-1"
:shadow-limit="50"
:list="cardList"
:list="getCardList('organization')"
:is-proportional="false"
:gap="16"
padding-bottom-space="16px"
@ -41,7 +41,7 @@
import useVisit from '@/hooks/useVisit';
import useTemplateStore from '@/store/modules/setting/template';
import { cardList } from './components/fieldSetting';
import { getCardList } from './components/fieldSetting';
const templateStore = useTemplateStore();

View File

@ -110,7 +110,8 @@ export default {
'system.orgTemplate.workFlowToolTipHover':
'hover can replace the initial state, and the operation column can select the end state',
'system.orgTemplate.addWorkFlowStatus': 'Add State',
'system.orgTemplate.StateName': 'State name',
'system.orgTemplate.updateWorkFlowStatus': 'Update State',
'system.orgTemplate.stateName': 'State name',
'system.orgTemplate.stateNameDescription':
'Please enter the status name, in order to display complete, try to be less than 8 characters',
'system.orgTemplate.stateNameNotNull': 'The status name cannot be empty',
@ -130,4 +131,19 @@ export default {
'system.orgTemplate.deleteStateStepContent':
'After delete, will be effective in the project and delete irrevocable, please careful operation.',
'system.orgTemplate.stateDetail': '{name} State detail',
'system.orgTemplate.addState': 'Add State',
'system.orgTemplate.example': 'example',
'system.orgTemplate.startState': 'Start State',
'system.orgTemplate.endState': 'End State',
'system.orgTemplate.iconTip': 'Icon to adjust state order',
'system.orgTemplate.anyStateToAll': 'Any state may switch to change state',
'system.orgTemplate.enableAnyStateToAll': 'enabled',
'system.orgTemplate.enableNotAnyStateToAll': 'off',
'system.orgTemplate.createFlowStep': 'Create flow step',
'system.orgTemplate.deleteSteps': 'Delete steps',
'system.orgTemplate.updateFlowStep': 'Update flow step',
'system.orgTemplate.details': 'details',
'system.orgTemplate.stateTip':
'Open, the existing state will transfer to the state, only in the state of new Settings',
'system.orgTemplate.createSuccess': 'Created successfully',
};

View File

@ -126,4 +126,18 @@ export default {
'system.orgTemplate.deleteStateStepTitle': '确定删除 {name} 步骤吗?',
'system.orgTemplate.deleteStateStepContent': '删除后,会在项目中立即生效且删除不可撤回,请谨慎操作!',
'system.orgTemplate.stateDetail': '{name} 状态',
'system.orgTemplate.addState': '添加状态',
'system.orgTemplate.example': '示例',
'system.orgTemplate.startState': '开始状态',
'system.orgTemplate.endState': '结束状态',
'system.orgTemplate.iconTip': '图标可调整状态顺序',
'system.orgTemplate.anyStateToAll': '任何状态可转换到改状态',
'system.orgTemplate.enableAnyStateToAll': '开启',
'system.orgTemplate.enableNotAnyStateToAll': '未开启',
'system.orgTemplate.createFlowStep': '创建流转步骤',
'system.orgTemplate.deleteSteps': '删除步骤',
'system.orgTemplate.updateFlowStep': '更新流转步骤',
'system.orgTemplate.details': '详情',
'system.orgTemplate.stateTip': '开启: 已有的状态会流转到该状态,仅在新建状态时设置',
'system.orgTemplate.createSuccess': '创建成功',
};