mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-22 05:02:57 +00:00
refactor: 重构查询参数实现,替换旧的__init__初始化方式为dataclass+post_init
feat: 新增岗位编码字段与校验 fix: 修复批量删除提示、请求缓存、任务状态等逻辑 perf: 优化表格列展示,移除冗余ID列 build: 降级fastapi版本至0.115.2,更新依赖锁文件 docs: 补充部分代码注释与说明
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user