feat(infra): 新增状态机管理功能模块

- 新增状态机模型和状态流转的完整 CRUD 功能
- 添加字典编码 OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE 用于对象类型下拉选择
- 实现状态机列表页、搜索组件、操作对话框和状态流转管理
- 新增 infra API 接口封装和类型定义
- 遵循项目规范:使用 TableSearchFields 搜索组件、BusinessTableActionCell 操作列、统一的状态标签展示

涉及文件:
- src/constants/dict.ts: 新增对象类型字典编码
- src/service/api/infra.ts: 新增状态机和状态流转相关 API
- src/typings/api/infra.d.ts: 新增状态机相关类型定义
- src/views/infra/state-machine/: 新增状态机管理页面及子组件
This commit is contained in:
caozehui
2026-05-15 09:31:00 +08:00
parent 960fe805ec
commit 3a064eb09f
11 changed files with 1891 additions and 1 deletions

View File

@@ -76,6 +76,14 @@ export const RDMS_PROJECT_TYPE_DICT_CODE = 'rdms_project_type';
*/
export const RDMS_PROJECT_EXECUTION_TYPE_DICT_CODE = 'rdms_project_execution_type';
/**
* 状态机对象类型字典编码
*
* 对应业务字段:状态机管理中的 objectType / 对象类型
* 来源口径:用户明确指定对象类型下拉来自运行时字典 object_status_model_object_type
*/
export const OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE = 'object_status_model_object_type';
/**
* 需求允许删除的状态字典编码
*

View File

@@ -1,6 +1,7 @@
export * from './auth';
export * from './dict';
export * from './file';
export * from './infra';
export * from './object-context';
export * from './product';
export * from './project';

208
src/service/api/infra.ts Normal file
View File

@@ -0,0 +1,208 @@
import { WEB_SERVICE_PREFIX } from '@/constants/service';
import { request } from '../request';
import { type ServiceRequestResult, mapServiceResult, normalizeStringId, safeJsonRequestConfig } from './shared';
const OBJECT_STATUS_MODEL_PREFIX = `${WEB_SERVICE_PREFIX}/project/status/model`;
const OBJECT_STATUS_TRANSITION_PREFIX = `${WEB_SERVICE_PREFIX}/project/status/transition`;
type ObjectStatusModelResponse = Omit<
Api.Infra.ObjectStatusModel,
| 'id'
| 'initialFlag'
| 'terminalFlag'
| 'allowEdit'
| 'progressExcludedFlag'
| 'allowCreateProject'
| 'allowCreateRequirement'
> & {
id: string | number;
initialFlag: boolean | number | string | null | undefined;
terminalFlag: boolean | number | string | null | undefined;
allowEdit: boolean | number | string | null | undefined;
progressExcludedFlag: boolean | number | string | null | undefined;
allowCreateProject: boolean | number | string | null | undefined;
allowCreateRequirement: boolean | number | string | null | undefined;
};
type ObjectStatusTransitionResponse = Omit<Api.Infra.ObjectStatusTransition, 'id' | 'needReason'> & {
id: string | number;
needReason: boolean | number | string | null | undefined;
};
type ObjectStatusModelPageResponse = Api.Infra.PageResult<ObjectStatusModelResponse>;
type ObjectStatusTransitionPageResponse = Api.Infra.PageResult<ObjectStatusTransitionResponse>;
function createBatchDeleteQuery(ids: string[]) {
const query = new URLSearchParams();
ids.forEach(id => {
query.append('ids', id);
});
return query.toString();
}
function normalizeBooleanFlag(value: boolean | number | string | null | undefined) {
if (typeof value === 'boolean') {
return value;
}
if (typeof value === 'number') {
return value === 1;
}
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase();
if (!normalized || normalized === '0' || normalized === 'false' || normalized === 'n') {
return false;
}
return true;
}
return false;
}
function normalizeObjectStatusModel(model: ObjectStatusModelResponse): Api.Infra.ObjectStatusModel {
return {
...model,
id: normalizeStringId(model.id),
initialFlag: normalizeBooleanFlag(model.initialFlag),
terminalFlag: normalizeBooleanFlag(model.terminalFlag),
allowEdit: normalizeBooleanFlag(model.allowEdit),
progressExcludedFlag: normalizeBooleanFlag(model.progressExcludedFlag),
allowCreateProject: normalizeBooleanFlag(model.allowCreateProject),
allowCreateRequirement: normalizeBooleanFlag(model.allowCreateRequirement)
};
}
function normalizeObjectStatusTransition(transition: ObjectStatusTransitionResponse): Api.Infra.ObjectStatusTransition {
return {
...transition,
id: normalizeStringId(transition.id),
needReason: normalizeBooleanFlag(transition.needReason)
};
}
export async function fetchGetObjectStatusModelPage(params?: Api.Infra.ObjectStatusModelSearchParams) {
const result = await request<ObjectStatusModelPageResponse>({
...safeJsonRequestConfig,
url: `${OBJECT_STATUS_MODEL_PREFIX}/page`,
method: 'get',
params
});
return mapServiceResult(result as ServiceRequestResult<ObjectStatusModelPageResponse>, data => ({
...data,
list: data.list.map(normalizeObjectStatusModel)
}));
}
export async function fetchGetObjectStatusModel(id: string) {
const result = await request<ObjectStatusModelResponse>({
...safeJsonRequestConfig,
url: `${OBJECT_STATUS_MODEL_PREFIX}/get`,
method: 'get',
params: { id }
});
return mapServiceResult(result as ServiceRequestResult<ObjectStatusModelResponse>, normalizeObjectStatusModel);
}
export async function fetchCreateObjectStatusModel(data: Api.Infra.SaveObjectStatusModelParams) {
const result = await request<string | number>({
...safeJsonRequestConfig,
url: `${OBJECT_STATUS_MODEL_PREFIX}/create`,
method: 'post',
data
});
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
}
export function fetchUpdateObjectStatusModel(data: { id: string } & Api.Infra.SaveObjectStatusModelParams) {
return request<boolean>({
url: `${OBJECT_STATUS_MODEL_PREFIX}/update`,
method: 'put',
data
});
}
export function fetchDeleteObjectStatusModel(id: string) {
return request<boolean>({
url: `${OBJECT_STATUS_MODEL_PREFIX}/delete`,
method: 'delete',
params: { id }
});
}
export function fetchBatchDeleteObjectStatusModel(ids: string[]) {
return request<boolean>({
url: `${OBJECT_STATUS_MODEL_PREFIX}/delete-list?${createBatchDeleteQuery(ids)}`,
method: 'delete'
});
}
export async function fetchGetObjectStatusTransitionPage(params?: Api.Infra.ObjectStatusTransitionSearchParams) {
const result = await request<ObjectStatusTransitionPageResponse>({
...safeJsonRequestConfig,
url: `${OBJECT_STATUS_TRANSITION_PREFIX}/page`,
method: 'get',
params
});
return mapServiceResult(result as ServiceRequestResult<ObjectStatusTransitionPageResponse>, data => ({
...data,
list: data.list.map(normalizeObjectStatusTransition)
}));
}
export async function fetchGetObjectStatusTransition(id: string) {
const result = await request<ObjectStatusTransitionResponse>({
...safeJsonRequestConfig,
url: `${OBJECT_STATUS_TRANSITION_PREFIX}/get`,
method: 'get',
params: { id }
});
return mapServiceResult(
result as ServiceRequestResult<ObjectStatusTransitionResponse>,
normalizeObjectStatusTransition
);
}
export async function fetchCreateObjectStatusTransition(data: Api.Infra.SaveObjectStatusTransitionParams) {
const result = await request<string | number>({
...safeJsonRequestConfig,
url: `${OBJECT_STATUS_TRANSITION_PREFIX}/create`,
method: 'post',
data
});
return mapServiceResult(result as ServiceRequestResult<string | number>, normalizeStringId);
}
export function fetchUpdateObjectStatusTransition(data: { id: string } & Api.Infra.SaveObjectStatusTransitionParams) {
return request<boolean>({
url: `${OBJECT_STATUS_TRANSITION_PREFIX}/update`,
method: 'put',
data
});
}
export function fetchDeleteObjectStatusTransition(id: string) {
return request<boolean>({
url: `${OBJECT_STATUS_TRANSITION_PREFIX}/delete`,
method: 'delete',
params: { id }
});
}
export function fetchBatchDeleteObjectStatusTransition(ids: string[]) {
return request<boolean>({
url: `${OBJECT_STATUS_TRANSITION_PREFIX}/delete-list?${createBatchDeleteQuery(ids)}`,
method: 'delete'
});
}

101
src/typings/api/infra.d.ts vendored Normal file
View File

@@ -0,0 +1,101 @@
declare namespace Api {
/**
* namespace Infra
*
* backend api module: "project/status/*"
*/
namespace Infra {
type CommonStatus = 0 | 1;
interface PageParams {
pageNo: number;
pageSize: number;
}
interface PageResult<T = any> {
total: number;
list: T[];
}
interface ObjectStatusModel {
id: string;
objectType: string;
statusCode: string;
statusName: string;
sort: number;
status: CommonStatus;
initialFlag: boolean;
terminalFlag: boolean;
allowEdit: boolean;
progressExcludedFlag: boolean;
allowCreateProject: boolean;
allowCreateRequirement: boolean;
remark?: string | null;
creator?: string | null;
createTime: string;
updater?: string | null;
updateTime: string;
}
type ObjectStatusModelSearchParams = CommonType.RecordNullable<
Pick<PageParams, 'pageNo' | 'pageSize'> &
Pick<ObjectStatusModel, 'objectType' | 'status' | 'initialFlag' | 'terminalFlag'> & {
keyword?: string;
}
>;
type SaveObjectStatusModelParams = Pick<
ObjectStatusModel,
| 'objectType'
| 'statusCode'
| 'statusName'
| 'sort'
| 'status'
| 'initialFlag'
| 'terminalFlag'
| 'allowEdit'
| 'progressExcludedFlag'
| 'allowCreateProject'
| 'allowCreateRequirement'
> & {
remark?: string | null;
};
type ObjectStatusModelList = PageResult<ObjectStatusModel>;
interface ObjectStatusTransition {
id: string;
objectType: string;
actionCode: string;
actionName: string;
fromStatusCode: string;
fromStatusName?: string | null;
toStatusCode: string;
toStatusName?: string | null;
needReason: boolean;
status: CommonStatus;
remark?: string | null;
creator?: string | null;
createTime: string;
updater?: string | null;
updateTime: string;
}
type ObjectStatusTransitionSearchParams = CommonType.RecordNullable<
Pick<PageParams, 'pageNo' | 'pageSize'> &
Pick<
ObjectStatusTransition,
'objectType' | 'fromStatusCode' | 'toStatusCode' | 'status' | 'actionCode' | 'actionName'
>
>;
type SaveObjectStatusTransitionParams = Pick<
ObjectStatusTransition,
'objectType' | 'actionCode' | 'actionName' | 'fromStatusCode' | 'toStatusCode' | 'needReason' | 'status'
> & {
remark?: string | null;
};
type ObjectStatusTransitionList = PageResult<ObjectStatusTransition>;
}
}

View File

@@ -1,3 +1,389 @@
<script setup lang="tsx">
import { computed, nextTick, onActivated, reactive, ref } from 'vue';
import type { TableInstance } from 'element-plus';
import { ElTag } from 'element-plus';
import { useBoolean } from '@sa/hooks';
import { OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE } from '@/constants/dict';
import {
fetchBatchDeleteObjectStatusModel,
fetchDeleteObjectStatusModel,
fetchGetObjectStatusModelPage
} from '@/service/api';
import { useAuth } from '@/hooks/business/auth';
import { useDict } from '@/hooks/business/dict';
import { useUIPaginatedTable } from '@/hooks/common/table';
import BusinessTableActionCell, { type BusinessTableAction } from '@/components/custom/business-table-action-cell';
import StateMachineOperateDialog from './modules/state-machine-operate-dialog.vue';
import StateMachineSearch from './modules/state-machine-search.vue';
import StateTransitionDialog from './modules/state-transition-dialog.vue';
import { formatDateTime, getBooleanLabel, getBooleanTagType, getStatusLabel, getStatusTagType } from './shared';
defineOptions({ name: 'StateMachineManage' });
function getInitSearchParams(): Api.Infra.ObjectStatusModelSearchParams {
return {
pageNo: 1,
pageSize: 10,
keyword: undefined,
objectType: undefined,
status: undefined,
initialFlag: undefined,
terminalFlag: undefined
};
}
function transformPageResult(
response: Awaited<ReturnType<typeof fetchGetObjectStatusModelPage>>,
pageNo: number,
pageSize: number
) {
if (!response.error) {
return {
data: response.data.list,
pageNum: pageNo,
pageSize,
total: response.data.total
};
}
return {
data: [],
pageNum: pageNo,
pageSize,
total: 0
};
}
const searchParams = reactive(getInitSearchParams());
const stateTableRef = ref<TableInstance>();
const checkedRowKeys = ref<string[]>([]);
const { getLabel: getObjectTypeLabel } = useDict(OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE);
const { hasAuth } = useAuth();
const canDeleteStateMachine = computed(() => hasAuth('infra:state-machine:delete'));
const canUpdateStateMachine = computed(() => hasAuth('infra:state-machine:update'));
const canManageStateTransition = computed(() => hasAuth('infra:state-transition:manage'));
function getStatusModelActions(row: Api.Infra.ObjectStatusModel): BusinessTableAction[] {
const actions: BusinessTableAction[] = [];
if (canManageStateTransition.value) {
actions.push({
key: 'transition',
label: '状态流转',
buttonType: 'primary',
onClick: () => openTransitionDialog(row)
});
}
if (canUpdateStateMachine.value) {
actions.push({
key: 'edit',
label: '编辑',
buttonType: 'primary',
onClick: () => openEdit(row)
});
}
if (canDeleteStateMachine.value) {
actions.push({
key: 'delete',
label: '删除',
buttonType: 'danger',
onClick: () => handleDeleteAction(row)
});
}
return actions;
}
const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagination } = useUIPaginatedTable({
paginationProps: {
currentPage: searchParams.pageNo,
pageSize: searchParams.pageSize
},
api: () => fetchGetObjectStatusModelPage(searchParams),
transform: response => transformPageResult(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
onPaginationParamsChange: params => {
searchParams.pageNo = params.currentPage ?? 1;
searchParams.pageSize = params.pageSize ?? 10;
},
columns: () => [
{ prop: 'selection', type: 'selection', width: 48 },
{ prop: 'index', type: 'index', label: '序号', width: 64 },
{
prop: 'objectType',
label: '对象类型',
minWidth: 130,
formatter: row => getObjectTypeLabel(row.objectType)
},
{ prop: 'statusName', label: '状态名称', minWidth: 140, showOverflowTooltip: true },
{ prop: 'statusCode', label: '状态编码', minWidth: 160, showOverflowTooltip: true },
{
prop: 'status',
label: '配置状态',
width: 110,
align: 'center',
formatter: row => <ElTag type={getStatusTagType(row.status)}>{getStatusLabel(row.status)}</ElTag>
},
{
prop: 'initialFlag',
label: '初始状态',
width: 110,
align: 'center',
formatter: row => <ElTag type={getBooleanTagType(row.initialFlag)}>{getBooleanLabel(row.initialFlag)}</ElTag>
},
{
prop: 'terminalFlag',
label: '终态',
width: 90,
align: 'center',
formatter: row => <ElTag type={getBooleanTagType(row.terminalFlag)}>{getBooleanLabel(row.terminalFlag)}</ElTag>
},
{
prop: 'allowEdit',
label: '允许编辑主数据',
width: 140,
align: 'center',
formatter: row => <ElTag type={getBooleanTagType(row.allowEdit)}>{getBooleanLabel(row.allowEdit)}</ElTag>
},
{
prop: 'progressExcludedFlag',
label: '不参与上层进度统计',
width: 160,
align: 'center',
formatter: row => (
<ElTag type={getBooleanTagType(row.progressExcludedFlag)}>{getBooleanLabel(row.progressExcludedFlag)}</ElTag>
)
},
{
prop: 'allowCreateProject',
label: '允许新建项目',
width: 130,
align: 'center',
formatter: row => (
<ElTag type={getBooleanTagType(row.allowCreateProject)}>{getBooleanLabel(row.allowCreateProject)}</ElTag>
)
},
{
prop: 'allowCreateRequirement',
label: '允许新增需求',
width: 130,
align: 'center',
formatter: row => (
<ElTag type={getBooleanTagType(row.allowCreateRequirement)}>
{getBooleanLabel(row.allowCreateRequirement)}
</ElTag>
)
},
{ prop: 'sort', label: '排序', width: 90, align: 'center' },
{
prop: 'remark',
label: '备注',
minWidth: 180,
showOverflowTooltip: true,
formatter: row => row.remark || '--'
},
{
prop: 'createTime',
label: '创建时间',
minWidth: 170,
formatter: row => formatDateTime(row.createTime)
},
{
prop: 'operate',
label: '操作',
width: 220,
align: 'center',
fixed: 'right',
formatter: row => {
const actions = getStatusModelActions(row);
if (!actions.length) {
return <span>--</span>;
}
return <BusinessTableActionCell actions={actions} />;
}
}
]
});
const { bool: operateVisible, setTrue: openOperateModal, setFalse: closeOperateModal } = useBoolean();
const operateType = ref<UI.TableOperateType>('add');
const editingData = ref<Api.Infra.ObjectStatusModel | null>(null);
const { bool: transitionVisible, setTrue: openTransitionModal, setFalse: closeTransitionModal } = useBoolean();
const transitionRow = ref<Api.Infra.ObjectStatusModel | null>(null);
function openAdd() {
operateType.value = 'add';
editingData.value = null;
openOperateModal();
}
function openEdit(item: Api.Infra.ObjectStatusModel) {
operateType.value = 'edit';
editingData.value = item;
openOperateModal();
}
function openTransitionDialog(item: Api.Infra.ObjectStatusModel) {
transitionRow.value = item;
openTransitionModal();
}
async function handleDelete(item: Api.Infra.ObjectStatusModel) {
const { error } = await fetchDeleteObjectStatusModel(item.id);
if (error) {
return;
}
window.$message?.success('删除成功');
await reloadStatusTable();
}
async function handleDeleteAction(row: Api.Infra.ObjectStatusModel) {
try {
await window.$messageBox?.confirm('确认删除当前状态模型吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
} catch {
return;
}
await handleDelete(row);
}
async function handleBatchDelete() {
if (!checkedRowKeys.value.length) {
return;
}
const { error } = await fetchBatchDeleteObjectStatusModel(checkedRowKeys.value);
if (error) {
return;
}
window.$message?.success('删除成功');
await reloadStatusTable();
}
function handleSelectionChange(rows: Api.Infra.ObjectStatusModel[]) {
checkedRowKeys.value = rows.map(item => item.id);
}
async function reloadStatusTable(page = searchParams.pageNo) {
checkedRowKeys.value = [];
await getDataByPage(page);
await nextTick();
stateTableRef.value?.clearSelection();
}
function resetSearchParams() {
Object.assign(searchParams, getInitSearchParams());
reloadStatusTable(1);
}
function handleSearch() {
reloadStatusTable(1);
}
function handleSubmitted() {
closeOperateModal();
reloadStatusTable();
}
onActivated(() => {
resetSearchParams();
});
</script>
<template>
<LookForward title="状态机管理" subtitle="功能建设中,敬请期待" />
<div class="flex-col-stretch gap-16px overflow-hidden">
<StateMachineSearch v-model:model="searchParams" @reset="resetSearchParams" @search="handleSearch" />
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
<template #header>
<div class="flex items-center justify-between gap-12px">
<div class="flex items-center gap-10px">
<p>状态模型列表</p>
<ElTag effect="plain">{{ mobilePagination.total || data.length }}</ElTag>
</div>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
@refresh="getData"
>
<template #default>
<ElButton v-auth="'infra:state-machine:create'" plain type="primary" @click="openAdd">
<template #icon>
<icon-ic-round-plus class="text-icon" />
</template>
新增
</ElButton>
<ElPopconfirm
v-if="canDeleteStateMachine"
title="确认删除选中的状态模型吗?"
@confirm="handleBatchDelete"
>
<template #reference>
<ElButton type="danger" plain :disabled="checkedRowKeys.length === 0">
<template #icon>
<icon-ic-round-delete class="text-icon" />
</template>
批量删除
</ElButton>
</template>
</ElPopconfirm>
</template>
</TableHeaderOperation>
</div>
</template>
<div class="flex-1">
<ElTable
ref="stateTableRef"
v-loading="loading"
height="100%"
border
row-key="id"
:data="data"
@selection-change="handleSelectionChange"
>
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col" />
</ElTable>
</div>
<div class="mt-20px flex justify-end">
<ElPagination
v-if="mobilePagination.total"
layout="total,prev,pager,next,sizes"
v-bind="mobilePagination"
@current-change="mobilePagination['current-change']"
@size-change="mobilePagination['size-change']"
/>
</div>
</ElCard>
<StateMachineOperateDialog
v-model:visible="operateVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="handleSubmitted"
/>
<StateTransitionDialog
v-model:visible="transitionVisible"
:current-status="transitionRow"
@update:visible="value => !value && closeTransitionModal()"
/>
</div>
</template>
<style scoped></style>

View File

@@ -0,0 +1,269 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE } from '@/constants/dict';
import { fetchCreateObjectStatusModel, fetchGetObjectStatusModel, fetchUpdateObjectStatusModel } from '@/service/api';
import { useDict } from '@/hooks/business/dict';
import { useForm, useFormRules } from '@/hooks/common/form';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import { statusOptions } from '../shared';
defineOptions({ name: 'StateMachineOperateDialog' });
interface Props {
operateType: UI.TableOperateType;
rowData?: Api.Infra.ObjectStatusModel | null;
}
const props = defineProps<Props>();
const emit = defineEmits<{
submitted: [statusModelId: string];
}>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate } = useForm();
const { createRequiredRule } = useFormRules();
const { dictOptions: objectTypeOptions } = useDict(OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE);
const detailLoading = ref(false);
const submitting = ref(false);
const isEdit = computed(() => props.operateType === 'edit');
const title = computed(() => {
const titleMap: Record<UI.TableOperateType, string> = {
add: '新增状态模型',
edit: '编辑状态模型'
};
return titleMap[props.operateType];
});
type Model = Api.Infra.SaveObjectStatusModelParams;
const model = ref(createDefaultModel());
function createDefaultModel(): Model {
return {
objectType: 'product',
statusCode: '',
statusName: '',
sort: 0,
status: 0,
initialFlag: false,
terminalFlag: false,
allowEdit: false,
progressExcludedFlag: false,
allowCreateProject: false,
allowCreateRequirement: false,
remark: ''
};
}
const rules = {
objectType: createRequiredRule('请选择对象类型'),
statusCode: createRequiredRule('请输入状态编码'),
statusName: createRequiredRule('请输入状态名称'),
sort: createRequiredRule('请输入排序值'),
status: createRequiredRule('请选择配置状态')
} satisfies Record<string, App.Global.FormRule>;
function closeModal() {
visible.value = false;
}
async function initModel() {
model.value = createDefaultModel();
if (!isEdit.value || !props.rowData) {
await nextTick();
formRef.value?.clearValidate();
return;
}
detailLoading.value = true;
const { error, data } = await fetchGetObjectStatusModel(props.rowData.id);
detailLoading.value = false;
if (!error) {
model.value = {
objectType: data.objectType,
statusCode: data.statusCode,
statusName: data.statusName,
sort: data.sort ?? 0,
status: data.status,
initialFlag: data.initialFlag,
terminalFlag: data.terminalFlag,
allowEdit: data.allowEdit,
progressExcludedFlag: data.progressExcludedFlag,
allowCreateProject: data.allowCreateProject,
allowCreateRequirement: data.allowCreateRequirement,
remark: data.remark ?? ''
};
}
await nextTick();
formRef.value?.clearValidate();
}
async function handleSubmit() {
await validate();
submitting.value = true;
const submitData: Api.Infra.SaveObjectStatusModelParams = {
...model.value,
statusCode: model.value.statusCode.trim(),
statusName: model.value.statusName.trim(),
remark: model.value.remark?.trim() || null
};
let statusModelId = props.rowData?.id ?? '';
if (isEdit.value && props.rowData) {
const { error } = await fetchUpdateObjectStatusModel({ id: props.rowData.id, ...submitData });
submitting.value = false;
if (error) {
return;
}
} else {
const { error, data } = await fetchCreateObjectStatusModel(submitData);
submitting.value = false;
if (error) {
return;
}
statusModelId = data;
}
window.$message?.success(isEdit.value ? '修改成功' : '新增成功');
closeModal();
emit('submitted', statusModelId);
}
watch(visible, value => {
if (value) {
initModel();
}
});
</script>
<template>
<BusinessFormDialog
v-model="visible"
:title="title"
preset="lg"
:loading="detailLoading"
:confirm-loading="submitting"
@confirm="handleSubmit"
>
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
<ElRow :gutter="16">
<ElCol :span="12">
<ElFormItem label="对象类型" prop="objectType">
<ElSelect
v-model="model.objectType"
class="w-full"
placeholder="请选择或输入对象类型"
filterable
allow-create
default-first-option
clearable
:reserve-keyword="false"
>
<ElOption v-for="item in objectTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="状态编码" prop="statusCode">
<ElInput v-model="model.statusCode" placeholder="请输入状态编码" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="状态名称" prop="statusName">
<ElInput v-model="model.statusName" placeholder="请输入状态名称" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="排序值" prop="sort">
<ElInputNumber v-model="model.sort" class="w-full" :min="0" placeholder="请输入排序值" />
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="配置状态" prop="status">
<ElRadioGroup v-model="model.status" class="business-form-radio-group">
<ElRadio v-for="{ label, value } in statusOptions" :key="value" :value="value">
{{ label }}
</ElRadio>
</ElRadioGroup>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="是否初始状态" prop="initialFlag">
<div class="business-form-switch-field">
<ElSwitch v-model="model.initialFlag" />
<span class="ml-8px text-12px text-[#606266]">{{ model.initialFlag ? '是' : '否' }}</span>
</div>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="是否终态" prop="terminalFlag">
<div class="business-form-switch-field">
<ElSwitch v-model="model.terminalFlag" />
<span class="ml-8px text-12px text-[#606266]">{{ model.terminalFlag ? '是' : '否' }}</span>
</div>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="允许编辑主数据" prop="allowEdit">
<div class="business-form-switch-field">
<ElSwitch v-model="model.allowEdit" />
<span class="ml-8px text-12px text-[#606266]">{{ model.allowEdit ? '是' : '否' }}</span>
</div>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="不参与上层进度统计" prop="progressExcludedFlag">
<div class="business-form-switch-field">
<ElSwitch v-model="model.progressExcludedFlag" />
<span class="ml-8px text-12px text-[#606266]">{{ model.progressExcludedFlag ? '是' : '否' }}</span>
</div>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="允许新建项目" prop="allowCreateProject">
<div class="business-form-switch-field">
<ElSwitch v-model="model.allowCreateProject" />
<span class="ml-8px text-12px text-[#606266]">{{ model.allowCreateProject ? '是' : '否' }}</span>
</div>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="允许新增需求" prop="allowCreateRequirement">
<div class="business-form-switch-field">
<ElSwitch v-model="model.allowCreateRequirement" />
<span class="ml-8px text-12px text-[#606266]">{{ model.allowCreateRequirement ? '是' : '否' }}</span>
</div>
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="备注" prop="remark">
<ElInput v-model="model.remark" type="textarea" :rows="4" placeholder="请输入备注" />
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
</BusinessFormDialog>
</template>
<style scoped></style>

View File

@@ -0,0 +1,142 @@
<script setup lang="ts">
import { computed, reactive, watch } from 'vue';
import { OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE } from '@/constants/dict';
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
import { statusOptions } from '../shared';
defineOptions({ name: 'StateMachineSearch' });
const emit = defineEmits<{
reset: [];
search: [];
}>();
const model = defineModel<Api.Infra.ObjectStatusModelSearchParams>('model', { required: true });
const booleanOptions = [
{ label: '是', value: 1 },
{ label: '否', value: 0 }
];
const searchModel = reactive<{
keyword: string;
objectType?: string;
status?: Api.Infra.CommonStatus;
initialFlag?: number;
terminalFlag?: number;
}>({
keyword: '',
objectType: undefined,
status: undefined,
initialFlag: undefined,
terminalFlag: undefined
});
let syncingFromSource = false;
watch(
() =>
[
model.value.keyword,
model.value.objectType,
model.value.status,
model.value.initialFlag,
model.value.terminalFlag
] as const,
([keyword, objectType, status, initialFlag, terminalFlag]) => {
syncingFromSource = true;
searchModel.keyword = keyword ?? '';
searchModel.objectType = objectType;
searchModel.status = status;
if (initialFlag === undefined) {
searchModel.initialFlag = undefined;
} else {
searchModel.initialFlag = initialFlag ? 1 : 0;
}
if (terminalFlag === undefined) {
searchModel.terminalFlag = undefined;
} else {
searchModel.terminalFlag = terminalFlag ? 1 : 0;
}
syncingFromSource = false;
},
{ immediate: true, flush: 'sync' }
);
watch(
() =>
[
searchModel.keyword,
searchModel.objectType,
searchModel.status,
searchModel.initialFlag,
searchModel.terminalFlag
] as const,
([keywordValue, objectType, status, initialFlag, terminalFlag]) => {
if (syncingFromSource) {
return;
}
model.value.keyword = keywordValue.trim() || undefined;
model.value.objectType = objectType;
model.value.status = status;
model.value.initialFlag = initialFlag === undefined ? undefined : initialFlag === 1;
model.value.terminalFlag = terminalFlag === undefined ? undefined : terminalFlag === 1;
},
{ flush: 'sync' }
);
const fields = computed<SearchField[]>(() => [
{
key: 'objectType',
label: '对象类型',
type: 'dict',
placeholder: '请选择对象类型',
dictCode: OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE
},
{
key: 'keyword',
label: '关键字',
type: 'input',
placeholder: '请输入状态名称或状态编码'
},
{
key: 'status',
label: '配置状态',
type: 'select',
placeholder: '请选择配置状态',
options: statusOptions
},
{
key: 'initialFlag',
label: '初始状态',
type: 'select',
placeholder: '请选择是否初始状态',
options: booleanOptions
},
{
key: 'terminalFlag',
label: '终态',
type: 'select',
placeholder: '请选择是否终态',
options: booleanOptions
}
]);
function reset() {
emit('reset');
}
function search() {
emit('search');
}
</script>
<template>
<TableSearchFields v-model="searchModel" :fields="fields" :columns="4" @reset="reset" @search="search" />
</template>
<style scoped></style>

View File

@@ -0,0 +1,406 @@
<script setup lang="tsx">
import { computed, nextTick, reactive, ref, watch } from 'vue';
import type { TableInstance } from 'element-plus';
import { ElButton, ElTag } from 'element-plus';
import { useBoolean } from '@sa/hooks';
import { OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE } from '@/constants/dict';
import {
fetchBatchDeleteObjectStatusTransition,
fetchDeleteObjectStatusTransition,
fetchGetObjectStatusModelPage,
fetchGetObjectStatusTransitionPage
} from '@/service/api';
import { useDict } from '@/hooks/business/dict';
import { useUIPaginatedTable } from '@/hooks/common/table';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import BusinessTableActionCell from '@/components/custom/business-table-action-cell';
import { formatDateTime, getBooleanLabel, getBooleanTagType, getStatusLabel, getStatusTagType } from '../shared';
import StateTransitionOperateDialog from './state-transition-operate-dialog.vue';
import StateTransitionSearch from './state-transition-search.vue';
defineOptions({ name: 'StateTransitionDialog' });
interface Props {
currentStatus?: Api.Infra.ObjectStatusModel | null;
}
const props = defineProps<Props>();
const visible = defineModel<boolean>('visible', {
default: false
});
function getInitSearchParams(): Api.Infra.ObjectStatusTransitionSearchParams {
return {
pageNo: 1,
pageSize: 10,
objectType: props.currentStatus?.objectType,
fromStatusCode: props.currentStatus?.statusCode,
actionCode: undefined,
actionName: undefined,
toStatusCode: undefined,
status: undefined
};
}
function transformPageResult(
response: Awaited<ReturnType<typeof fetchGetObjectStatusTransitionPage>>,
pageNo: number,
pageSize: number
) {
if (!response.error) {
return {
data: response.data.list,
pageNum: pageNo,
pageSize,
total: response.data.total
};
}
return {
data: [],
pageNum: pageNo,
pageSize,
total: 0
};
}
const searchParams = reactive(getInitSearchParams());
const transitionTableRef = ref<TableInstance>();
const checkedRowKeys = ref<string[]>([]);
const statusModelOptions = ref<Api.Infra.ObjectStatusModel[]>([]);
const loadingOptions = ref(false);
const { getLabel: getObjectTypeLabel } = useDict(OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE);
const targetStatusOptions = computed(() =>
statusModelOptions.value.map(item => ({
label: `${item.statusName} (${item.statusCode})`,
value: item.statusCode
}))
);
const currentStatusLabel = computed(() => {
if (!props.currentStatus) {
return '--';
}
return `${props.currentStatus.statusName} (${props.currentStatus.statusCode})`;
});
const currentObjectTypeLabel = computed(() => getObjectTypeLabel(props.currentStatus?.objectType));
const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagination } = useUIPaginatedTable({
paginationProps: {
currentPage: searchParams.pageNo,
pageSize: searchParams.pageSize
},
api: () => fetchGetObjectStatusTransitionPage(searchParams),
transform: response => transformPageResult(response, searchParams.pageNo ?? 1, searchParams.pageSize ?? 10),
onPaginationParamsChange: params => {
searchParams.pageNo = params.currentPage ?? 1;
searchParams.pageSize = params.pageSize ?? 10;
},
columns: () => [
{ prop: 'selection', type: 'selection', width: 48 },
{ prop: 'index', type: 'index', label: '序号', width: 64 },
{ prop: 'actionName', label: '动作名称', minWidth: 150, showOverflowTooltip: true },
{ prop: 'actionCode', label: '动作编码', minWidth: 150, showOverflowTooltip: true },
{
prop: 'toStatusCode',
label: '目标状态',
minWidth: 180,
formatter: row => row.toStatusName?.trim() || row.toStatusCode
},
{
prop: 'needReason',
label: '必须填写原因',
width: 120,
align: 'center',
formatter: row => <ElTag type={getBooleanTagType(row.needReason)}>{getBooleanLabel(row.needReason)}</ElTag>
},
{
prop: 'status',
label: '配置状态',
width: 110,
align: 'center',
formatter: row => <ElTag type={getStatusTagType(row.status)}>{getStatusLabel(row.status)}</ElTag>
},
{
prop: 'remark',
label: '备注',
minWidth: 180,
showOverflowTooltip: true,
formatter: row => row.remark || '--'
},
{
prop: 'createTime',
label: '创建时间',
minWidth: 170,
formatter: row => formatDateTime(row.createTime)
},
{
prop: 'operate',
label: '操作',
width: 180,
align: 'center',
fixed: 'right',
formatter: row => (
<BusinessTableActionCell
actions={[
{
key: 'edit',
label: '编辑',
buttonType: 'primary',
onClick: () => openEdit(row)
},
{
key: 'delete',
label: '删除',
buttonType: 'danger',
onClick: () => handleDeleteAction(row)
}
]}
/>
)
}
]
});
const { bool: operateVisible, setTrue: openOperateModal, setFalse: closeOperateModal } = useBoolean();
const operateType = ref<UI.TableOperateType>('add');
const editingData = ref<Api.Infra.ObjectStatusTransition | null>(null);
function openAdd() {
operateType.value = 'add';
editingData.value = null;
openOperateModal();
}
function openEdit(item: Api.Infra.ObjectStatusTransition) {
operateType.value = 'edit';
editingData.value = item;
openOperateModal();
}
async function handleDelete(item: Api.Infra.ObjectStatusTransition) {
const { error } = await fetchDeleteObjectStatusTransition(item.id);
if (error) {
return;
}
window.$message?.success('删除成功');
await reloadTable();
}
async function handleDeleteAction(row: Api.Infra.ObjectStatusTransition) {
try {
await window.$messageBox?.confirm('确认删除当前状态流转吗?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
} catch {
return;
}
await handleDelete(row);
}
async function handleBatchDelete() {
if (!checkedRowKeys.value.length) {
return;
}
const { error } = await fetchBatchDeleteObjectStatusTransition(checkedRowKeys.value);
if (error) {
return;
}
window.$message?.success('删除成功');
await reloadTable();
}
function handleSelectionChange(rows: Api.Infra.ObjectStatusTransition[]) {
checkedRowKeys.value = rows.map(item => item.id);
}
async function reloadTable(page = searchParams.pageNo) {
checkedRowKeys.value = [];
await getDataByPage(page);
await nextTick();
transitionTableRef.value?.clearSelection();
}
function resetSearchParams() {
Object.assign(searchParams, getInitSearchParams());
reloadTable(1);
}
function handleSearch() {
reloadTable(1);
}
function handleSubmitted() {
closeOperateModal();
reloadTable();
}
async function loadStatusModelOptions() {
if (!props.currentStatus?.objectType) {
statusModelOptions.value = [];
return;
}
loadingOptions.value = true;
const { error, data: page } = await fetchGetObjectStatusModelPage({
pageNo: 1,
pageSize: 200,
keyword: undefined,
objectType: props.currentStatus.objectType,
status: undefined,
initialFlag: undefined,
terminalFlag: undefined
});
loadingOptions.value = false;
statusModelOptions.value = error ? [] : page.list;
}
async function initDialog() {
if (!props.currentStatus) {
return;
}
Object.assign(searchParams, getInitSearchParams(), {
objectType: props.currentStatus.objectType,
fromStatusCode: props.currentStatus.statusCode
});
checkedRowKeys.value = [];
await Promise.all([loadStatusModelOptions(), reloadTable(1)]);
}
watch(
() => [visible.value, props.currentStatus?.id] as const,
([opened]) => {
if (opened) {
initDialog();
}
},
{ immediate: true }
);
</script>
<template>
<BusinessFormDialog
v-model="visible"
title="状态流转配置"
width="1200px"
:loading="loadingOptions"
:show-footer="false"
:scrollbar="false"
>
<div v-if="currentStatus" class="state-transition-dialog">
<StateTransitionSearch
v-model:model="searchParams"
:target-status-options="targetStatusOptions"
@reset="resetSearchParams"
@search="handleSearch"
/>
<ElCard class="flex-1-hidden card-wrapper" body-class="business-table-card-body">
<template #header>
<div class="flex items-center justify-between gap-12px">
<div class="min-w-0 flex flex-wrap items-center gap-8px">
<p>状态流转列表</p>
<ElTag type="primary" effect="light">
{{ currentObjectTypeLabel }}
</ElTag>
<ElTag type="success" effect="light">
{{ currentStatusLabel }}
</ElTag>
<ElTag effect="plain">{{ mobilePagination.total || data.length }}</ElTag>
</div>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
@refresh="getData"
>
<template #default>
<ElButton plain type="primary" @click="openAdd">
<template #icon>
<icon-ic-round-plus class="text-icon" />
</template>
新增
</ElButton>
<ElPopconfirm title="确认删除选中的状态流转吗?" @confirm="handleBatchDelete">
<template #reference>
<ElButton type="danger" plain :disabled="checkedRowKeys.length === 0">
<template #icon>
<icon-ic-round-delete class="text-icon" />
</template>
批量删除
</ElButton>
</template>
</ElPopconfirm>
</template>
</TableHeaderOperation>
</div>
</template>
<div class="flex-1">
<ElTable
ref="transitionTableRef"
v-loading="loading"
height="100%"
border
row-key="id"
:data="data"
@selection-change="handleSelectionChange"
>
<ElTableColumn v-for="col in columns" :key="String(col.prop)" v-bind="col" />
</ElTable>
</div>
<div class="mt-20px flex justify-end">
<ElPagination
v-if="mobilePagination.total"
layout="total,prev,pager,next,sizes"
v-bind="mobilePagination"
@current-change="mobilePagination['current-change']"
@size-change="mobilePagination['size-change']"
/>
</div>
</ElCard>
</div>
<div v-else class="h-full flex items-center justify-center">
<ElEmpty description="请选择状态模型" />
</div>
<StateTransitionOperateDialog
v-model:visible="operateVisible"
:operate-type="operateType"
:row-data="editingData"
:current-status="currentStatus"
:target-status-options="targetStatusOptions"
append-to-body
@submitted="handleSubmitted"
/>
</BusinessFormDialog>
</template>
<style scoped lang="scss">
.state-transition-dialog {
display: flex;
min-height: 560px;
flex-direction: column;
gap: 16px;
}
</style>

View File

@@ -0,0 +1,234 @@
<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue';
import { OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE } from '@/constants/dict';
import {
fetchCreateObjectStatusTransition,
fetchGetObjectStatusTransition,
fetchUpdateObjectStatusTransition
} from '@/service/api';
import { useDict } from '@/hooks/business/dict';
import { useForm, useFormRules } from '@/hooks/common/form';
import BusinessFormDialog from '@/components/custom/business-form-dialog.vue';
import { statusOptions } from '../shared';
defineOptions({ name: 'StateTransitionOperateDialog' });
interface Props {
operateType: UI.TableOperateType;
rowData?: Api.Infra.ObjectStatusTransition | null;
currentStatus?: Api.Infra.ObjectStatusModel | null;
targetStatusOptions: Array<{ label: string; value: string }>;
}
const props = defineProps<Props>();
const emit = defineEmits<{
submitted: [transitionId: string];
}>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate } = useForm();
const { createRequiredRule } = useFormRules();
const { getLabel: getObjectTypeLabel } = useDict(OBJECT_STATUS_MODEL_OBJECT_TYPE_DICT_CODE);
const detailLoading = ref(false);
const submitting = ref(false);
const isEdit = computed(() => props.operateType === 'edit');
const title = computed(() => {
const titleMap: Record<UI.TableOperateType, string> = {
add: '新增状态流转',
edit: '编辑状态流转'
};
return titleMap[props.operateType];
});
type Model = Api.Infra.SaveObjectStatusTransitionParams;
const model = ref(createDefaultModel());
const currentObjectTypeLabel = computed(() => getObjectTypeLabel(model.value.objectType));
const currentFromStatusLabel = computed(() => {
if (!props.currentStatus) {
return model.value.fromStatusCode || '--';
}
return `${props.currentStatus.statusName} (${props.currentStatus.statusCode})`;
});
function createDefaultModel(): Model {
return {
objectType: props.currentStatus?.objectType ?? 'product',
actionCode: '',
actionName: '',
fromStatusCode: props.currentStatus?.statusCode ?? '',
toStatusCode: '',
needReason: false,
status: 0,
remark: ''
};
}
const rules = {
actionCode: createRequiredRule('请输入动作编码'),
actionName: createRequiredRule('请输入动作名称'),
toStatusCode: createRequiredRule('请选择目标状态'),
status: createRequiredRule('请选择配置状态')
} satisfies Record<string, App.Global.FormRule>;
function closeModal() {
visible.value = false;
}
async function initModel() {
model.value = createDefaultModel();
if (!isEdit.value || !props.rowData) {
await nextTick();
formRef.value?.clearValidate();
return;
}
detailLoading.value = true;
const { error, data } = await fetchGetObjectStatusTransition(props.rowData.id);
detailLoading.value = false;
if (!error) {
model.value = {
objectType: data.objectType,
actionCode: data.actionCode,
actionName: data.actionName,
fromStatusCode: data.fromStatusCode,
toStatusCode: data.toStatusCode,
needReason: data.needReason,
status: data.status,
remark: data.remark ?? ''
};
}
await nextTick();
formRef.value?.clearValidate();
}
async function handleSubmit() {
await validate();
submitting.value = true;
const submitData: Api.Infra.SaveObjectStatusTransitionParams = {
...model.value,
objectType: props.currentStatus?.objectType ?? model.value.objectType,
fromStatusCode: props.currentStatus?.statusCode ?? model.value.fromStatusCode,
actionCode: model.value.actionCode.trim(),
actionName: model.value.actionName.trim(),
remark: model.value.remark?.trim() || null
};
let transitionId = props.rowData?.id ?? '';
if (isEdit.value && props.rowData) {
const { error } = await fetchUpdateObjectStatusTransition({ id: props.rowData.id, ...submitData });
submitting.value = false;
if (error) {
return;
}
} else {
const { error, data } = await fetchCreateObjectStatusTransition(submitData);
submitting.value = false;
if (error) {
return;
}
transitionId = data;
}
window.$message?.success(isEdit.value ? '修改成功' : '新增成功');
closeModal();
emit('submitted', transitionId);
}
watch(visible, value => {
if (value) {
initModel();
}
});
</script>
<template>
<BusinessFormDialog
v-model="visible"
:title="title"
preset="md"
:loading="detailLoading"
:confirm-loading="submitting"
@confirm="handleSubmit"
>
<ElForm ref="formRef" :model="model" :rules="rules" label-position="top">
<ElRow :gutter="16">
<ElCol :span="12">
<ElFormItem label="对象类型">
<ElInput :model-value="currentObjectTypeLabel" readonly />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="起始状态">
<ElInput :model-value="currentFromStatusLabel" readonly />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="动作编码" prop="actionCode">
<ElInput v-model="model.actionCode" placeholder="请输入动作编码" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="动作名称" prop="actionName">
<ElInput v-model="model.actionName" placeholder="请输入动作名称" />
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="目标状态" prop="toStatusCode">
<ElSelect v-model="model.toStatusCode" class="w-full" placeholder="请选择目标状态">
<ElOption v-for="{ label, value } in targetStatusOptions" :key="value" :label="label" :value="value" />
</ElSelect>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="配置状态" prop="status">
<ElRadioGroup v-model="model.status" class="business-form-radio-group">
<ElRadio v-for="{ label, value } in statusOptions" :key="value" :value="value">
{{ label }}
</ElRadio>
</ElRadioGroup>
</ElFormItem>
</ElCol>
<ElCol :span="12">
<ElFormItem label="必须填写原因" prop="needReason">
<div class="business-form-switch-field">
<ElSwitch v-model="model.needReason" />
<span class="ml-8px text-12px text-[#606266]">{{ model.needReason ? '是' : '否' }}</span>
</div>
</ElFormItem>
</ElCol>
<ElCol :span="24">
<ElFormItem label="备注" prop="remark">
<ElInput v-model="model.remark" type="textarea" :rows="4" placeholder="请输入备注" />
</ElFormItem>
</ElCol>
</ElRow>
</ElForm>
</BusinessFormDialog>
</template>
<style scoped></style>

View File

@@ -0,0 +1,97 @@
<script setup lang="ts">
import { computed, reactive, watch } from 'vue';
import TableSearchFields, { type SearchField } from '@/components/custom/table-search-fields.vue';
import { statusOptions } from '../shared';
defineOptions({ name: 'StateTransitionSearch' });
interface Props {
targetStatusOptions: Array<{ label: string; value: string }>;
}
const props = defineProps<Props>();
const emit = defineEmits<{
reset: [];
search: [];
}>();
const model = defineModel<Api.Infra.ObjectStatusTransitionSearchParams>('model', { required: true });
const searchModel = reactive<{
keyword: string;
toStatusCode?: string;
status?: Api.Infra.CommonStatus;
}>({
keyword: '',
toStatusCode: undefined,
status: undefined
});
let syncingFromSource = false;
watch(
() => [model.value.actionName, model.value.actionCode, model.value.toStatusCode, model.value.status] as const,
([actionName, actionCode, toStatusCode, status]) => {
syncingFromSource = true;
searchModel.keyword = actionName ?? actionCode ?? '';
searchModel.toStatusCode = toStatusCode;
searchModel.status = status;
syncingFromSource = false;
},
{ immediate: true, flush: 'sync' }
);
watch(
() => [searchModel.keyword, searchModel.toStatusCode, searchModel.status] as const,
([keywordValue, toStatusCode, status]) => {
if (syncingFromSource) {
return;
}
const keywordText = keywordValue.trim() || undefined;
model.value.actionName = keywordText;
model.value.actionCode = keywordText;
model.value.toStatusCode = toStatusCode;
model.value.status = status;
},
{ flush: 'sync' }
);
const fields = computed<SearchField[]>(() => [
{
key: 'keyword',
label: '动作名称',
type: 'input',
placeholder: '请输入动作名称或动作编码'
},
{
key: 'toStatusCode',
label: '目标状态',
type: 'select',
placeholder: '请选择目标状态',
options: props.targetStatusOptions
},
{
key: 'status',
label: '配置状态',
type: 'select',
placeholder: '请选择配置状态',
options: statusOptions
}
]);
function reset() {
emit('reset');
}
function search() {
emit('search');
}
</script>
<template>
<TableSearchFields v-model="searchModel" :fields="fields" :columns="4" @reset="reset" @search="search" />
</template>
<style scoped></style>

View File

@@ -0,0 +1,38 @@
import dayjs from 'dayjs';
export const statusOptions: Array<{ label: string; value: Api.Infra.CommonStatus }> = [
{ label: '启用', value: 0 },
{ label: '停用', value: 1 }
];
export function getStatusLabel(value?: Api.Infra.CommonStatus | null) {
if (value === 0) {
return '启用';
}
if (value === 1) {
return '停用';
}
return '--';
}
export function getStatusTagType(value?: Api.Infra.CommonStatus | null): UI.ThemeColor {
return value === 0 ? 'success' : 'warning';
}
export function getBooleanLabel(value?: boolean | null) {
return value ? '是' : '否';
}
export function getBooleanTagType(value?: boolean | null): UI.ThemeColor {
return value ? 'success' : 'info';
}
export function formatDateTime(value?: string | number | null) {
if (!value) {
return '--';
}
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
}