refactor: 重构查询参数实现,替换旧的__init__初始化方式为dataclass+post_init

feat: 新增岗位编码字段与校验
fix: 修复批量删除提示、请求缓存、任务状态等逻辑
perf: 优化表格列展示,移除冗余ID列
build: 降级fastapi版本至0.115.2,更新依赖锁文件
docs: 补充部分代码注释与说明
This commit is contained in:
zhangtao
2026-06-22 00:27:23 +08:00
parent 8949f6dc46
commit afdd805e11
44 changed files with 728 additions and 639 deletions
+1 -1
View File
@@ -476,7 +476,7 @@ function useTableImpl<TApiFn extends (params: any) => Promise<any>>(
globalListNetworkInflight.set(dedupeKey, networkPromise as Promise<ApiResponse<unknown>>);
networkPromise.finally(() => {
globalListNetworkInflight.delete(dedupeKey);
});
}).catch(() => {}); // 忽略取消/reject,仅用于清理全局 Map
inFlightDedupePromise = networkPromise;
+1 -1
View File
@@ -185,7 +185,7 @@ function onRefreshFailed() {
export const request: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: Number(import.meta.env.VITE_TIMEOUT) || 15000,
timeout: Number(import.meta.env.VITE_API_TIMEOUT) || 15000,
headers: { "Content-Type": "application/json;charset=utf-8" },
paramsSerializer: (params) => qs.stringify(params, { indices: false }),
});
@@ -155,7 +155,6 @@
</template>
<script setup lang="ts">
import { h } from "vue";
import { useAuth } from "@/hooks/core/useAuth";
import { renderTableOperationCell, type TableOperationAction } from "@/utils/table";
import { useTable } from "@/hooks/core/useTable";
@@ -279,9 +278,7 @@ const {
columnsFactory: (): ColumnOption<DemoTable>[] => [
{ type: "globalIndex", width: 56, label: "序号" },
{ type: "selection", width: 48, fixed: "left" },
{ prop: "id", label: "ID", width: 72 },
{ prop: "name", label: "名称", minWidth: 120, showOverflowTooltip: true },
{ prop: "uuid", label: "UUID", minWidth: 168, showOverflowTooltip: true },
{
prop: "status",
label: "状态",
@@ -307,15 +304,6 @@ const {
{ prop: "time_val", label: "时间", minWidth: 96, showOverflowTooltip: true },
{ prop: "datetime_val", label: "日期时间", minWidth: 168, showOverflowTooltip: true },
{ prop: "text_val", label: "长文本", minWidth: 120, showOverflowTooltip: true },
{
prop: "json_val",
label: "元数据",
minWidth: 160,
formatter: (row: DemoTable) => {
if (row.json_val == null) return h("span", { class: "text-g-500" }, "—");
return h(FaJsonPretty, { value: row.json_val, height: "120px" });
},
},
{ prop: "description", label: "描述", minWidth: 120, showOverflowTooltip: true },
{ prop: "created_time", label: "创建时间", width: 168, showOverflowTooltip: true },
{ prop: "updated_time", label: "更新时间", width: 168, showOverflowTooltip: true },
@@ -184,7 +184,6 @@ const {
page_size: 50,
},
columnsFactory: resolveStatusColumns<InvoiceTable>(() => [
{ prop: "id", label: "ID", width: 60 },
{ prop: "tenant_id", label: "租户ID", width: 80 },
{ prop: "invoice_no", label: "发票号", width: 180, showOverflowTooltip: true },
{ prop: "title", label: "抬头", minWidth: 180, showOverflowTooltip: true },
@@ -276,7 +275,6 @@ const {
page_size: 50,
},
columnsFactory: resolveStatusColumns<InvoiceTable>(() => [
{ prop: "id", label: "ID", width: 60 },
{ prop: "invoice_no", label: "发票号", width: 180, showOverflowTooltip: true },
{ prop: "title", label: "抬头", minWidth: 180, showOverflowTooltip: true },
{
@@ -256,7 +256,6 @@ const {
page_size: 20,
},
columnsFactory: resolveStatusColumns<OrderTable>(() => [
{ prop: "id", label: "ID", width: 60 },
{ prop: "order_no", label: "订单号", minWidth: 180, showOverflowTooltip: true },
{ prop: "tenant_id", label: "租户ID", width: 80 },
{ prop: "package_id", label: "套餐ID", width: 80 },
@@ -406,7 +405,6 @@ const {
page_size: 20,
},
columnsFactory: resolveStatusColumns<PaymentRecordTable>(() => [
{ prop: "id", label: "ID", width: 60 },
{ prop: "order_id", label: "订单ID", width: 80 },
{
prop: "transaction_id",
@@ -513,7 +511,6 @@ const {
page_size: 20,
},
columnsFactory: resolveStatusColumns<RefundTable>(() => [
{ prop: "id", label: "ID", width: 60 },
{ prop: "refund_no", label: "退款单号", minWidth: 180, showOverflowTooltip: true },
{ prop: "order_id", label: "订单ID", width: 80 },
{
@@ -485,6 +485,7 @@ const positionDetailItems: import("@/components/others/fa-descriptions/index.vue
const formData = ref<PositionForm>({
id: undefined,
name: undefined,
code: undefined,
order: 1,
status: 0,
description: undefined,
@@ -494,6 +495,7 @@ const { dialogVisible } = useCrudDialog();
const rules = reactive({
name: [{ required: true, message: "请输入岗位名称", trigger: "blur" }],
code: [{ required: true, message: "请输入岗位编码", trigger: "blur" }],
order: [{ required: true, message: "请输入岗位排序", trigger: "blur" }],
status: [{ required: true, message: "请选择岗位状态", trigger: "blur" }],
});
@@ -501,6 +503,7 @@ const rules = reactive({
const initialFormData: PositionForm = {
id: undefined,
name: undefined,
code: undefined,
order: 1,
status: 0,
description: undefined,
@@ -550,6 +553,13 @@ const positionDialogFormItems = computed<FormItem[]>(() => [
span: 24,
props: { placeholder: "请输入岗位名称", maxlength: 50 },
},
{
label: "岗位编码",
key: "code",
type: "input",
span: 24,
props: { placeholder: "请输入岗位编码", maxlength: 64 },
},
{
label: "排序",
key: "order",
@@ -946,7 +946,7 @@ async function handleBatchDelete() {
if (userStore.basicInfo.id && ids.includes(userStore.basicInfo.id)) {
userStore.clearUserInfo();
} else {
ElMessage.success("删除成功");
console.info(`删除 ${ids.length} 条数据`);
}
selectedRows.value = [];
await refreshRemove();
@@ -1,4 +1,4 @@
<!-- 工作流节点类型Art + useTable -->
<!-- 工作流节点类型ElSplitter + Codemirror -->
<template>
<div class="fa-full-height">
<FaSearchBar
@@ -82,27 +82,92 @@
<FaDialog
v-model="dialogVisible"
:title="dialogTitle"
width="720px"
width="1000px"
destroy-on-close
@close="handleCloseDialog"
@opened="handleDialogOpened"
>
<FaForm
:key="nodeTypeFormRenderKey"
ref="formRef"
v-model="formData"
:items="nodeTypeDialogFormItems"
:rules="rules"
label-width="100px"
label-position="right"
:span="24"
:gutter="16"
:show-reset="false"
:show-submit="false"
class="crud-dialog-art-form"
/>
<ElSplitter direction="horizontal" :style="'height: 500px'">
<ElSplitterPanel size="320px" :min="240" :max="420">
<ElScrollbar :style="'height: 100%'">
<FaForm
:key="nodeTypeFormRenderKey"
ref="formRef"
v-model="formData"
:items="nodeTypeDialogFormItems"
:rules="rules"
label-suffix=":"
label-width="85px"
:span="24"
:gutter="16"
:show-reset="false"
:show-submit="false"
class="crud-dialog-art-form node-splitter-art-form"
>
<template #args>
<div class="dynamic-params">
<div v-for="(_item, index) in argsList" :key="index" class="param-item">
<ElInput v-model="argsList[index]" placeholder="参数值" />
<ElButton
type="danger"
icon="Delete"
circle
@click="argsList.splice(index, 1)"
/>
</div>
<ElButton type="primary" icon="Plus" @click="argsList.push('')">
添加位置参数
</ElButton>
</div>
</template>
<template #kwargs>
<div class="dynamic-params">
<div v-for="(item, index) in kwargsList" :key="index" class="param-item">
<ElInput v-model="item.key" placeholder="键" />
<ElInput v-model="item.value" placeholder="值" />
<ElButton
type="danger"
icon="Delete"
circle
@click="kwargsList.splice(index, 1)"
/>
</div>
<ElButton
type="primary"
icon="Plus"
@click="kwargsList.push({ key: '', value: '' })"
>
添加关键词参数
</ElButton>
</div>
</template>
</FaForm>
</ElScrollbar>
</ElSplitterPanel>
<ElSplitterPanel>
<div class="code-editor-container">
<div class="code-editor-header">
<span class="code-editor-title">handler 代码</span>
<span class="code-editor-tip">须定义 handler(*args, **kwargs) 函数</span>
</div>
<Codemirror
ref="codeEditorRef"
v-model:value="formData.func"
:options="codeEditorOptions"
border
height="calc(100% - 40px)"
width="100%"
/>
</div>
</ElSplitterPanel>
</ElSplitter>
<template #footer>
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" :loading="submitting" @click="submitForm">保存</ElButton>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" :loading="submitting" @click="submitForm">保存</ElButton>
</div>
</template>
</FaDialog>
</div>
@@ -126,7 +191,11 @@ import { useTable } from "@/hooks/core/useTable";
import type { ColumnOption } from "@/types/component";
import { ElMessage, ElMessageBox } from "element-plus";
import type { FormRules } from "element-plus";
import { computed, ref } from "vue";
import { computed, nextTick, ref } from "vue";
import Codemirror, { CmComponentRef } from "codemirror-editor-vue3";
import type { EditorConfiguration } from "codemirror";
import "codemirror/mode/python/python.js";
import "codemirror/theme/dracula.css";
const BATCH_DELETE_MSG = "确认删除选中的编排节点类型吗?";
@@ -346,6 +415,23 @@ async function onResetSearch() {
await resetSearchParams();
}
// ─── Codemirror ──────────────────────────────────────────────────
const codeEditorOptions: EditorConfiguration = {
mode: "python",
lineNumbers: true,
smartIndent: true,
indentUnit: 4,
tabSize: 4,
theme: "dracula",
lineWrapping: true,
autofocus: false,
};
const codeEditorRef = ref<CmComponentRef>();
// ─── 表单 ────────────────────────────────────────────────────────
const dialogVisible = ref(false);
const dialogTitle = ref("新增节点类型");
const editingId = ref<number | null>(null);
@@ -353,6 +439,30 @@ const submitting = ref(false);
const formRef = ref<InstanceType<typeof FaForm> | null>(null);
const nodeTypeFormRenderKey = ref(0);
const defaultForm = (): WorkflowNodeTypeForm => ({
name: "",
code: "",
category: "action",
func: `def handler(*args, **kwargs):
"""
编写你的处理逻辑
可访问:
- args: 位置参数
- kwargs: 关键字参数
"""
print("handler executed")
return {"status": "success"}
`,
args: "",
kwargs: "{}",
sort_order: 0,
is_active: true,
});
const formData = ref<WorkflowNodeTypeForm>(defaultForm());
const argsList = ref<string[]>([]);
const kwargsList = ref<{ key: string; value: string }[]>([]);
const nodeTypeDialogFormItems = computed<FormItem[]>(() => [
{
label: "名称",
@@ -383,34 +493,19 @@ const nodeTypeDialogFormItems = computed<FormItem[]>(() => [
],
},
},
{
label: "代码块",
key: "func",
type: "input",
span: 24,
props: {
type: "textarea",
rows: 12,
placeholder: "须定义 handler(*args, **kwargs),可接收 upstream、variables",
},
},
{
label: "位置参数",
key: "args",
type: "input",
span: 24,
props: { placeholder: "逗号分隔,如 a, b" },
placeholder: "",
},
{
label: "关键字参数",
key: "kwargs",
type: "input",
span: 24,
props: {
type: "textarea",
rows: 3,
placeholder: 'JSON,如 {"key": "v"}',
},
placeholder: "",
},
{
label: "排序",
@@ -427,29 +522,17 @@ const nodeTypeDialogFormItems = computed<FormItem[]>(() => [
},
]);
const defaultForm = (): WorkflowNodeTypeForm => ({
name: "",
code: "",
category: "action",
func: "",
args: "",
kwargs: "{}",
sort_order: 0,
is_active: true,
});
const formData = ref<WorkflowNodeTypeForm>(defaultForm());
const rules: FormRules = {
name: [{ required: true, message: "请输入名称", trigger: "blur" }],
code: [{ required: true, message: "请输入编码", trigger: "blur" }],
category: [{ required: true, message: "请选择分类", trigger: "change" }],
func: [{ required: true, message: "请输入代码块", trigger: "blur" }],
};
function resetForm() {
Object.assign(formData.value, defaultForm());
editingId.value = null;
argsList.value = [];
kwargsList.value = [];
formRef.value?.resetFields();
formRef.value?.clearValidate();
}
@@ -458,6 +541,14 @@ function handleCloseDialog() {
resetForm();
}
function handleDialogOpened() {
nextTick(() => {
setTimeout(() => {
codeEditorRef.value?.refresh?.();
}, 100);
});
}
async function handleAdd() {
createLoading.value = true;
try {
@@ -480,10 +571,15 @@ async function openDialog(id?: number) {
formData.value.code = d.code || "";
formData.value.category = (d.category as WorkflowNodeTypeForm["category"]) || "action";
formData.value.func = d.func || "";
formData.value.args = d.args || "";
formData.value.kwargs = d.kwargs || "{}";
formData.value.sort_order = d.sort_order ?? 0;
formData.value.is_active = d.is_active ?? true;
argsList.value = d.args ? d.args.split(",").map((v: string) => v.trim()) : [];
kwargsList.value = d.kwargs
? Object.entries(JSON.parse(d.kwargs)).map(([key, value]) => ({
key,
value: String(value),
}))
: [];
}
} catch {
ElMessage.error("加载详情失败");
@@ -497,22 +593,32 @@ async function openDialog(id?: number) {
async function submitForm() {
if (!formRef.value) return;
await formRef.value.validate();
if (formData.value.kwargs?.trim()) {
try {
JSON.parse(formData.value.kwargs);
} catch {
ElMessage.error("关键字参数须为合法 JSON");
return;
}
if (!formData.value.func || !formData.value.func.trim()) {
ElMessage.error("请输入 handler 代码");
return;
}
submitting.value = true;
try {
const submitData = {
...formData.value,
args: argsList.value.filter((v) => v.trim()).join(",") || undefined,
kwargs:
kwargsList.value.filter((v) => v.key.trim()).length > 0
? JSON.stringify(
Object.fromEntries(
kwargsList.value.filter((v) => v.key.trim()).map((v) => [v.key, v.value])
)
)
: undefined,
};
if (editingId.value) {
await WorkflowNodeTypeAPI.updateWorkflowNodeType(editingId.value, formData.value);
await WorkflowNodeTypeAPI.updateWorkflowNodeType(editingId.value, submitData);
dialogVisible.value = false;
await refreshUpdate();
} else {
await WorkflowNodeTypeAPI.createWorkflowNodeType(formData.value);
await WorkflowNodeTypeAPI.createWorkflowNodeType(submitData);
dialogVisible.value = false;
await refreshCreate();
}
@@ -524,4 +630,46 @@ async function submitForm() {
}
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.code-editor-container {
display: flex;
flex-direction: column;
height: 100%;
}
.code-editor-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: var(--el-fill-color-light);
border-bottom: 1px solid var(--el-border-color-lighter);
flex-shrink: 0;
}
.code-editor-title {
font-weight: 600;
font-size: 14px;
}
.code-editor-tip {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.dynamic-params {
display: flex;
flex-direction: column;
gap: 8px;
}
.param-item {
display: flex;
align-items: center;
gap: 8px;
.el-input {
flex: 1;
}
}
</style>