mirror of
https://github.com/fastapi-admin/fastapi-admin.git
synced 2025-08-26 10:40:18 +08:00
add auto pk field mapping
This commit is contained in:
@ -85,7 +85,10 @@ def create_app():
|
||||
Menu(
|
||||
name='在线参数',
|
||||
url='/rest/Config',
|
||||
icon='fa fa-cog'
|
||||
icon='fa fa-cog',
|
||||
actions={
|
||||
'delete': False
|
||||
}
|
||||
),
|
||||
Menu(
|
||||
name='基本信息',
|
||||
|
@ -75,7 +75,7 @@ class AdminApp(FastAPI):
|
||||
return field_type
|
||||
|
||||
async def _build_resource_from_model_describe(self, resource: str, model: Type[Model], model_describe: dict,
|
||||
exclude_pk: bool, exclude_m2m_field=True):
|
||||
exclude_pk: bool, exclude_m2m_field=True, exclude_actions=False):
|
||||
"""
|
||||
build resource
|
||||
:param resource:
|
||||
@ -94,7 +94,7 @@ class AdminApp(FastAPI):
|
||||
search_fields = menu.search_fields
|
||||
sort_fields = menu.sort_fields
|
||||
fields = {}
|
||||
name = pk_field.get('name')
|
||||
pk = name = pk_field.get('name')
|
||||
if not exclude_pk and not self._exclude_field(resource, name):
|
||||
fields = {
|
||||
name: Field(
|
||||
@ -104,8 +104,8 @@ class AdminApp(FastAPI):
|
||||
sortable=name in sort_fields
|
||||
)
|
||||
}
|
||||
|
||||
fields['_actions'] = menu.actions
|
||||
if not exclude_actions:
|
||||
fields['_actions'] = menu.actions
|
||||
|
||||
for data_field in data_fields:
|
||||
readonly = data_field.get('constraints').get('readOnly')
|
||||
@ -166,18 +166,20 @@ class AdminApp(FastAPI):
|
||||
options=options,
|
||||
multiple=True,
|
||||
)
|
||||
return fields, search_fields_ret
|
||||
return pk, fields, search_fields_ret
|
||||
|
||||
async def get_resource(self, resource: str, exclude_pk=False, exclude_m2m_field=True):
|
||||
async def get_resource(self, resource: str, exclude_pk=False, exclude_m2m_field=True, exclude_actions=False):
|
||||
assert self._inited, 'must call init() first!'
|
||||
model = getattr(self.models, resource) # type:Type[Model]
|
||||
model_describe = Tortoise.describe_model(model)
|
||||
fields, search_fields = await self._build_resource_from_model_describe(resource, model, model_describe,
|
||||
exclude_pk, exclude_m2m_field)
|
||||
pk, fields, search_fields = await self._build_resource_from_model_describe(resource, model, model_describe,
|
||||
exclude_pk, exclude_m2m_field,
|
||||
exclude_actions)
|
||||
return Resource(
|
||||
title=model_describe.get('description') or resource.title(),
|
||||
fields=fields,
|
||||
searchFields=search_fields
|
||||
searchFields=search_fields,
|
||||
pk=pk
|
||||
)
|
||||
|
||||
|
||||
|
@ -49,7 +49,7 @@ async def get_resource(
|
||||
async def form(
|
||||
resource: str,
|
||||
):
|
||||
resource = await app.get_resource(resource, exclude_pk=True, exclude_m2m_field=False)
|
||||
resource = await app.get_resource(resource, exclude_pk=True, exclude_m2m_field=False, exclude_actions=True)
|
||||
return resource.dict(by_alias=True, exclude_unset=True)
|
||||
|
||||
|
||||
@ -142,4 +142,5 @@ async def get_one(
|
||||
relate_model = getattr(obj, m2m_field) # type:ManyToManyRelation
|
||||
ids = await relate_model.all().values_list(relate_model.model._meta.pk_attr)
|
||||
ret[m2m_field] = list(map(lambda x: x[0], ids))
|
||||
ret['__str__'] = str(obj)
|
||||
return ret
|
||||
|
@ -69,6 +69,7 @@ class Field(BaseModel):
|
||||
|
||||
class Resource(BaseModel):
|
||||
title: str
|
||||
pk: str
|
||||
resource_fields: Dict[str, Union[Field, Dict]]
|
||||
searchFields: Optional[Dict[str, Field]]
|
||||
|
||||
|
@ -3,13 +3,11 @@
|
||||
# Change if you put the built files to a sub directory. e.g. /admin/
|
||||
PRODUCTION_BASE_URL=/
|
||||
|
||||
# Your backend restful api base url, end with slash `/`
|
||||
# Your backend restful api base url, end with slash `/`
|
||||
# for example
|
||||
# VUE_APP_API_URL=http://localhost:3333/admin/api/
|
||||
|
||||
# this is the built-in test api server, use `npm run test-api` to run it.
|
||||
VUE_APP_API_URL=http://localhost:8088/admin/api/
|
||||
|
||||
# The primary key field name in you database, e.g: mongodb use `_id`
|
||||
VUE_APP_PRIMARY_KEY=_id
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
# Change if you put the built files to a sub directory. e.g. /admin/
|
||||
BASE_URL=/admin/
|
||||
|
||||
# Your backend restful api base url, end with slash `/`
|
||||
# Your backend restful api base url, end with slash `/`
|
||||
VUE_APP_API_URL=/admin/api/
|
||||
|
||||
# The primary key field name in you database, e.g: mongodb use `_id`
|
||||
VUE_APP_PRIMARY_KEY=_id
|
||||
|
||||
|
@ -4,8 +4,9 @@
|
||||
<div class="row d-none">
|
||||
<div class="col col-md-8">
|
||||
<legend
|
||||
v-if="model[$config.primaryKey] && false"
|
||||
>{{$t('actions.edit')}}: {{model[$config.primaryKey]}}</legend>
|
||||
v-if="model[pk] && false"
|
||||
>{{$t('actions.edit')}}: {{model[pk]}}
|
||||
</legend>
|
||||
</div>
|
||||
<div class="col col-md-4 text-right hidden-sm-down">
|
||||
<b-btn @click="$router.go(-1)">{{$t('actions.back')}}</b-btn>
|
||||
@ -30,136 +31,138 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from "vuex";
|
||||
import _ from "lodash";
|
||||
import {mapState, mapGetters} from "vuex";
|
||||
import _ from "lodash";
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
resource: {
|
||||
type: String,
|
||||
required: true
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
resource: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
id: {
|
||||
default: "",
|
||||
required: true
|
||||
},
|
||||
formPath: {
|
||||
type: String,
|
||||
default: "form",
|
||||
required: false
|
||||
}
|
||||
},
|
||||
id: {
|
||||
default: "",
|
||||
required: true
|
||||
},
|
||||
formPath: {
|
||||
type: String,
|
||||
default: "form",
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
redirect: -1,
|
||||
loaded: false,
|
||||
choices: {},
|
||||
fields: {},
|
||||
model: {},
|
||||
errors: [],
|
||||
tag: "b-card",
|
||||
header: `
|
||||
data() {
|
||||
return {
|
||||
redirect: -1,
|
||||
loaded: false,
|
||||
choices: {},
|
||||
fields: {},
|
||||
model: {},
|
||||
errors: [],
|
||||
pk: null,
|
||||
tag: "b-card",
|
||||
header: `
|
||||
${_.get(this.currentMenu, "name", "") || ""}
|
||||
<small> ${this.resource.toUpperCase()} </small>
|
||||
`
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
id: "fetchForm",
|
||||
"site.fetched"(val){
|
||||
if (val) {
|
||||
this.fetchForm(true)
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
resourceUri() {
|
||||
let url = [this.site.resource_prefix, this.resource, this.id]
|
||||
.filter(v => v)
|
||||
.join("/");
|
||||
let group = this.$route.params.group;
|
||||
if (group) {
|
||||
url += "?group=" + group;
|
||||
}
|
||||
return url;
|
||||
watch: {
|
||||
id: "fetchForm",
|
||||
"site.fetched"(val) {
|
||||
if (val) {
|
||||
this.fetchForm(true)
|
||||
}
|
||||
},
|
||||
},
|
||||
formUri() {
|
||||
let url = [this.site.resource_prefix, this.resource, this.formPath]
|
||||
.filter(v => v)
|
||||
.join("/");
|
||||
url += "?id=" + (this.id || "");
|
||||
return url;
|
||||
computed: {
|
||||
resourceUri() {
|
||||
let url = [this.site.resource_prefix, this.resource, this.id]
|
||||
.filter(v => v)
|
||||
.join("/");
|
||||
let group = this.$route.params.group;
|
||||
if (group) {
|
||||
url += "?group=" + group;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
formUri() {
|
||||
let url = [this.site.resource_prefix, this.resource, this.formPath]
|
||||
.filter(v => v)
|
||||
.join("/");
|
||||
url += "?id=" + (this.id || "");
|
||||
return url;
|
||||
},
|
||||
isNew() {
|
||||
return !this.id;
|
||||
},
|
||||
method() {
|
||||
return this.isNew ? "post" : "put";
|
||||
},
|
||||
with() {
|
||||
return _.filter(
|
||||
_.map(this.fields, v => v.ref && v.ref.split(".").shift())
|
||||
);
|
||||
},
|
||||
...mapState(["nav", "auth", "site"]),
|
||||
...mapGetters(["currentMenu"])
|
||||
},
|
||||
isNew() {
|
||||
return !this.id;
|
||||
},
|
||||
method() {
|
||||
return this.isNew ? "post" : "put";
|
||||
},
|
||||
with() {
|
||||
return _.filter(
|
||||
_.map(this.fields, v => v.ref && v.ref.split(".").shift())
|
||||
);
|
||||
},
|
||||
...mapState(["nav", "auth", "site"]),
|
||||
...mapGetters(["currentMenu"])
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
if (this.isNew) {
|
||||
this.model = {};
|
||||
this.loaded = true;
|
||||
return;
|
||||
}
|
||||
this.$http
|
||||
.get(this.resourceUri, {
|
||||
params: {
|
||||
query: {
|
||||
with: this.with
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(({ data }) => {
|
||||
methods: {
|
||||
fetch() {
|
||||
if (this.isNew) {
|
||||
this.model = {};
|
||||
this.loaded = true;
|
||||
this.model = data;
|
||||
});
|
||||
},
|
||||
fetchForm() {
|
||||
this.$http
|
||||
.get(this.formUri, {
|
||||
params: this.$route.params
|
||||
})
|
||||
.then(({ data }) => {
|
||||
this.fields = data.fields;
|
||||
this.layout = data.layout;
|
||||
this.redirect = data.redirect;
|
||||
if (data.header) {
|
||||
this.header = data.header;
|
||||
}
|
||||
if (data.tag) {
|
||||
this.tag = data.tag;
|
||||
}
|
||||
this.fetch();
|
||||
});
|
||||
},
|
||||
return;
|
||||
}
|
||||
this.$http
|
||||
.get(this.resourceUri, {
|
||||
params: {
|
||||
query: {
|
||||
with: this.with
|
||||
}
|
||||
}
|
||||
})
|
||||
.then(({data}) => {
|
||||
this.loaded = true;
|
||||
this.model = data;
|
||||
});
|
||||
},
|
||||
fetchForm() {
|
||||
this.$http
|
||||
.get(this.formUri, {
|
||||
params: this.$route.params
|
||||
})
|
||||
.then(({data}) => {
|
||||
this.fields = data.fields;
|
||||
this.pk = data.pk;
|
||||
this.layout = data.layout;
|
||||
this.redirect = data.redirect;
|
||||
if (data.header) {
|
||||
this.header = data.header;
|
||||
}
|
||||
if (data.tag) {
|
||||
this.tag = data.tag;
|
||||
}
|
||||
this.fetch();
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess() {
|
||||
if (this.redirect === false) {
|
||||
this.fetchForm();
|
||||
} else if (this.redirect === -1 || !this.redirect) {
|
||||
this.$router.go(-1);
|
||||
} else {
|
||||
this.$router.go(this.redirect);
|
||||
onSuccess() {
|
||||
if (this.redirect === false) {
|
||||
this.fetchForm();
|
||||
} else if (this.redirect === -1 || !this.redirect) {
|
||||
this.$router.go(-1);
|
||||
} else {
|
||||
this.$router.go(this.redirect);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.site.fetched && this.fetchForm()
|
||||
},
|
||||
created() {
|
||||
// this.fetchForm();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.site.fetched && this.fetchForm()
|
||||
},
|
||||
created() {
|
||||
// this.fetchForm();
|
||||
}
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<div class="page-view">
|
||||
<div class="data-view">
|
||||
<legend v-if="model[$config.primaryKey]">{{$t('actions.view')}}: {{model[$config.primaryKey]}}</legend>
|
||||
<legend v-if="model['__str__']">{{$t('actions.view')}}: {{model['__str__']}}</legend>
|
||||
<table class="table ">
|
||||
<tbody>
|
||||
<tr v-for="(field, key) in fields" :key="key">
|
||||
<th style="min-width:120px">{{field.label || key}}</th>
|
||||
<td>
|
||||
<div v-if="['array'].includes(field.type)">
|
||||
<b-table :items="model[key]" :fields="field.fields">
|
||||
<template v-for="(child, k) in field.fields" :slot="k" slot-scope="row">
|
||||
<b-data-value :lang="currentLanguage" :field="child" :name="k" :key="k" :model="row.item" />
|
||||
</template>
|
||||
</b-table>
|
||||
</div>
|
||||
<div v-else>
|
||||
<b-data-value :lang="currentLanguage" :field="field" :name="key" :model="model" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-for="(field, key) in fields" :key="key">
|
||||
<th style="min-width:120px">{{field.label || key}}</th>
|
||||
<td>
|
||||
<div v-if="['array'].includes(field.type)">
|
||||
<b-table :items="model[key]" :fields="field.fields">
|
||||
<template v-for="(child, k) in field.fields" :slot="k" slot-scope="row">
|
||||
<b-data-value :lang="currentLanguage" :field="child" :name="k" :key="k" :model="row.item"/>
|
||||
</template>
|
||||
</b-table>
|
||||
</div>
|
||||
<div v-else>
|
||||
<b-data-value :lang="currentLanguage" :field="field" :name="key" :model="model"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@ -29,103 +29,103 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BDataValue from "./DataValue";
|
||||
import _ from 'lodash'
|
||||
import BDataValue from "./DataValue";
|
||||
import _ from 'lodash'
|
||||
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
export default {
|
||||
components: {
|
||||
BDataValue
|
||||
},
|
||||
props: {
|
||||
resource: {
|
||||
type: String,
|
||||
required: true
|
||||
import {mapGetters, mapState} from 'vuex'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BDataValue
|
||||
},
|
||||
id: {
|
||||
default: '',
|
||||
required: true
|
||||
props: {
|
||||
resource: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
id: {
|
||||
default: '',
|
||||
required: true
|
||||
},
|
||||
viewPath: {
|
||||
type: String,
|
||||
default: "view",
|
||||
required: false
|
||||
},
|
||||
|
||||
},
|
||||
viewPath: {
|
||||
type: String,
|
||||
default: "view",
|
||||
required: false
|
||||
data() {
|
||||
return {
|
||||
choices: {},
|
||||
fields: {},
|
||||
model: {},
|
||||
errors: [],
|
||||
items: [],
|
||||
pk: null
|
||||
};
|
||||
},
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
choices: {},
|
||||
fields: {},
|
||||
model: {},
|
||||
errors: [],
|
||||
items: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
resourceUri() {
|
||||
return [this.site.resource_prefix, this.resource, this.id]
|
||||
.filter(v => v)
|
||||
.join("/");
|
||||
},
|
||||
viewUri() {
|
||||
let url = [this.site.resource_prefix, this.resource, this.viewPath]
|
||||
.filter(v => v)
|
||||
.join("/");
|
||||
url += "?id=" + (this.id || "");
|
||||
return url;
|
||||
},
|
||||
with() {
|
||||
return _.filter(
|
||||
_.map(this.fields, (v) => v.ref && v.ref.replace(/\.\w+$/, ''))
|
||||
);
|
||||
},
|
||||
|
||||
computed: {
|
||||
resourceUri() {
|
||||
let url = [this.site.resource_prefix, this.resource, this.id]
|
||||
.filter(v => v)
|
||||
.join("/");
|
||||
return url;
|
||||
},
|
||||
viewUri() {
|
||||
let url = [this.site.resource_prefix, this.resource, this.viewPath]
|
||||
.filter(v => v)
|
||||
.join("/");
|
||||
url += "?id=" + (this.id || "");
|
||||
return url;
|
||||
},
|
||||
with() {
|
||||
return _.filter(
|
||||
_.map(this.fields, (v) => v.ref && v.ref.replace(/\.\w+$/, ''))
|
||||
);
|
||||
},
|
||||
|
||||
...mapState(['site']),
|
||||
...mapGetters(["currentMenu", "currentLanguage"]),
|
||||
header() {
|
||||
return `
|
||||
...mapState(['site']),
|
||||
...mapGetters(["currentMenu", "currentLanguage"]),
|
||||
header() {
|
||||
return `
|
||||
''
|
||||
<small> ${this.resource.toUpperCase()} </small>
|
||||
`
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
this.$http.get(this.resourceUri, {
|
||||
params: {
|
||||
query: { with: this.with }
|
||||
methods: {
|
||||
fetch() {
|
||||
this.$http.get(this.resourceUri, {
|
||||
params: {
|
||||
query: {with: this.with}
|
||||
}
|
||||
}).then(({data}) => {
|
||||
this.model = data;
|
||||
});
|
||||
},
|
||||
fetchView() {
|
||||
this.$http.get(this.viewUri).then(({data}) => {
|
||||
this.fields = data.fields;
|
||||
this.pk = data.pk;
|
||||
delete this.fields._actions
|
||||
this.fetch();
|
||||
});
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
id: "fetchForm",
|
||||
"site.fetched"(val) {
|
||||
if (val) {
|
||||
this.fetchView(true)
|
||||
}
|
||||
}).then(({ data }) => {
|
||||
this.model = data;
|
||||
});
|
||||
},
|
||||
},
|
||||
fetchView() {
|
||||
this.$http.get(this.viewUri).then(({ data }) => {
|
||||
this.fields = data.fields;
|
||||
delete this.fields._actions
|
||||
this.fetch();
|
||||
});
|
||||
mounted() {
|
||||
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
id: "fetchForm",
|
||||
"site.fetched"(val){
|
||||
if (val) {
|
||||
this.fetchView(true)
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
created() {
|
||||
this.site.fetched && this.fetchView();
|
||||
|
||||
|
||||
}
|
||||
};
|
||||
created() {
|
||||
this.site.fetched && this.fetchView();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -21,8 +21,8 @@ import { mapState } from "vuex";
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
|
||||
|
||||
|
||||
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -43,7 +43,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
fetch() {
|
||||
|
||||
|
||||
},
|
||||
fetchForm() {
|
||||
this.$http.get(this.uri).then(({ data }) => {
|
||||
|
@ -120,7 +120,7 @@
|
||||
v-if="actions.edit !== false"
|
||||
variant="success"
|
||||
size="sm"
|
||||
:to="`/rest/${uri}/${row.item[$config.primaryKey]}`"
|
||||
:to="`/rest/${uri}/${row.item[pk]}`"
|
||||
class="mr-1"
|
||||
>{{$t('actions.view')}}
|
||||
</b-btn>
|
||||
@ -128,14 +128,14 @@
|
||||
v-if="actions.edit !== false"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
:to="`/rest/${uri}/${row.item[$config.primaryKey]}/edit`"
|
||||
:to="`/rest/${uri}/${row.item[pk]}/edit`"
|
||||
class="mr-1"
|
||||
>{{$t('actions.edit')}}
|
||||
</b-btn>
|
||||
<b-btn
|
||||
v-if="actions.delete !== false"
|
||||
size="sm"
|
||||
@click.stop="remove(row.item[$config.primaryKey])"
|
||||
@click.stop="remove(row.item[pk])"
|
||||
>{{$t('actions.delete')}}
|
||||
</b-btn>
|
||||
</template>
|
||||
@ -173,12 +173,12 @@
|
||||
total: 0, //total rows
|
||||
pageLimit: 10, //display how many page buttons
|
||||
currentPage: 1,
|
||||
sortBy: this.$config.primaryKey,
|
||||
sortBy: this.pk,
|
||||
sortDesc: true,
|
||||
sortDirection: null,
|
||||
perPage: 10,
|
||||
where: {},
|
||||
iframeSrc: ""
|
||||
pk: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@ -249,7 +249,7 @@
|
||||
sortBy && (this.sortBy = sortBy);
|
||||
|
||||
if (sortDesc) {
|
||||
this.sortDesc = sortDesc === -1 ? true : false;
|
||||
this.sortDesc = sortDesc === -1;
|
||||
}
|
||||
this.total = page * this.perPage;
|
||||
this.currentPage = page;
|
||||
@ -305,6 +305,7 @@
|
||||
});
|
||||
|
||||
this.table = res.data;
|
||||
this.pk = res.data.pk;
|
||||
|
||||
if (_.get(this.table, "fields._actions") !== false) {
|
||||
_.set(
|
||||
|
Reference in New Issue
Block a user