json view

add example
update pypi.yml
This commit is contained in:
long2ice
2020-04-27 22:57:58 +08:00
parent 6031fda39a
commit d2572dadb2
12 changed files with 344 additions and 235 deletions

View File

@@ -21,4 +21,15 @@ jobs:
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.pypi_password }}
password: ${{ secrets.pypi_password }}
- name: Deploy
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
port: ${{ secrets.PORT }}
script: |
cd /root/fastapi-admin/
git pull
supervisorctl restart fastapi-admin

View File

@@ -13,3 +13,15 @@ class ProductType(EnumMixin, IntEnum):
cls.article: 'Article',
cls.page: 'Page'
}
class Status(EnumMixin, IntEnum):
on = 1
off = 0
@classmethod
def choices(cls):
return {
cls.on: 'On',
cls.off: 'Off'
}

View File

@@ -307,3 +307,32 @@ VALUES (2, 'admin', '$2b$12$mrRdNt8n5V8Lsmdh8OGCEOh3.xkUzJRbTo0Ew8IcdyNHjRTfJ0pt
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
-- ----------------------------
-- Table structure for config
-- ----------------------------
DROP TABLE IF EXISTS `config`;
CREATE TABLE `config`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`label` varchar(20) NOT NULL,
`key` varchar(50) NOT NULL,
`value` longtext NOT NULL,
`status` tinyint(1) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `key` (`key`)
) ENGINE = InnoDB
AUTO_INCREMENT = 8
DEFAULT CHARSET = utf8mb4;
-- ----------------------------
-- Records of config
-- ----------------------------
BEGIN;
INSERT INTO `config`
VALUES (1, 'test', 'test',
'{"status":200,"error":"","data":[{"news_id":51184,"title":"iPhone X Review: Innovative future with real black technology","source":"Netease phone"},{"news_id":51183,"title":"Traffic paradise: How to design streets for people and unmanned vehicles in the future?","source":"Netease smart"},{"news_id":51182,"title":"Teslamask''s American Business Relations: The government does not pay billions to build factories","source":"AI Finance","members":["Daniel","Mike","John"]}]}',
1);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -100,6 +100,11 @@ def create_app():
url='/rest/Category',
icon='icon-list'
),
Menu(
name='Config',
url='/rest/Config',
icon='fa fa-pencil'
),
Menu(
name='External',
title=True

View File

@@ -3,7 +3,7 @@ import datetime
from tortoise import fields, Model
from fastapi_admin.models import User as AdminUser, Permission, Role
from .enums import ProductType
from .enums import ProductType, Status
class User(AdminUser):
@@ -40,3 +40,13 @@ class Product(Model):
def __str__(self):
return f'{self.pk}#{self.name}'
class Config(Model):
label = fields.CharField(max_length=200)
key = fields.CharField(max_length=20)
value = fields.JSONField()
status: Status = fields.IntEnumField(Status, default=Status.on)
def __str__(self):
return f'{self.pk}#{self.label}'

View File

@@ -178,7 +178,8 @@ class AdminApp(FastAPI):
)
return pk, fields, search_fields_ret
async def get_resource(self, resource: str, exclude_pk=False, exclude_m2m_field=True, exclude_actions=False):
async def get_resource(self, resource: str, exclude_pk=False, exclude_m2m_field=True,
exclude_actions=False) -> Resource:
assert self._inited, 'must call init() first!'
model = getattr(self.models, resource) # type:Type[Model]
model_describe = model.describe(serializable=False)

View File

@@ -30,6 +30,7 @@
"vue-element-loading": "^1.1.5",
"vue-html5-editor": "^1.1.1",
"vue-i18n": "^8.17.3",
"vue-json-pretty": "^1.6.3",
"vue-router": "^3.1.3",
"vue-select": "^3.2.0",
"vue-snotify": "^3.2.1",

View File

@@ -14,7 +14,12 @@
</template>
<b-img class="type-image" v-else :src="preview(value)" v-bind="field" fluid @click.stop="previewInModal(value)"/>
</template>
<template v-else-if="['json'].includes(field.type)">
<b-json-pretty
:value=value
>
</b-json-pretty>
</template>
<template v-else-if="['audio', 'video'].includes(field.type)">
<component :is="field.type" :src="value" controls/>
</template>
@@ -87,8 +92,10 @@
<script>
import _ from "lodash";
import BJsonPretty from "./JsonPretty";
export default {
components: {BJsonPretty},
data() {
return {
previewValue: null,

View File

@@ -10,7 +10,7 @@
v-if="inline"
enctype="multipart/form-data"
>
<input type="hidden" name="token" :value="auth.token" />
<input type="hidden" name="token" :value="auth.token"/>
<template v-for="(field, name) in fields">
<label
@@ -40,7 +40,8 @@
variant="secondary"
@click="$router.go(-1)"
v-if="backText"
>{{backText}}</b-button>
>{{backText}}
</b-button>
<slot name="extra-buttons"></slot>
</slot>
</component>
@@ -55,7 +56,7 @@
enctype="multipart/form-data"
v-else
>
<input type="hidden" name="token" :value="auth.token" />
<input type="hidden" name="token" :value="auth.token"/>
<b-tabs
class="my-3"
v-if="groupBy"
@@ -140,233 +141,236 @@
variant="secondary"
@click="$router.go(-1)"
v-if="backText"
>{{backText}}</b-button>
>{{backText}}
</b-button>
</slot>
</component>
</div>
</template>
<script>
import _ from "lodash";
import _ from "lodash";
export default {
name: "b-form-builder",
components: {},
props: {
parent: {},
subForm: {
type: String,
default: ""
},
id: {
type: String,
default() {
return "form_" + parseInt(Math.random() * 9999);
}
},
auth: {
type: Object,
default() {
return {};
}
},
col: {
type: Number,
default: 12
},
languages: {},
inline: {
type: Boolean,
default: false
},
useFormData: {
type: Boolean,
default: false
},
submitRawForm: {
type: Boolean,
default: false
},
groupBy: {
type: String,
default: null
},
value: {
type: Object,
default() {
return {};
}
},
fields: {
required: true,
default() {
return {};
}
},
layout: {
required: false,
default() {
return {};
}
},
onSubmit: {
type: Function,
required: false
},
beforeSubmit: {
type: Function,
required: false
},
action: {},
method: {
default: "post"
},
submitText: {
default() {
return this.$t ? this.$t("actions.save") : "Submit";
}
},
backText: {
default() {
return this.$t ? this.$t("actions.back") : "Back";
}
},
successMessage: {
default() {
return this.$t ? this.$t("messages.succeed") : "Succeed";
}
}
},
data() {
return {
model: null,
errors: []
};
},
watch: {},
computed: {
tag() {
return this.subForm ? "div" : "form";
},
actionUrl() {
return global.API_URI + this.action;
},
groupedFields() {
const ret = {};
_.keys(_.groupBy(this.fields, this.groupBy)).map(v => {
let tabName = v;
if (v === "undefined") {
v = null;
tabName = this.$t("messages.default");
}
ret[tabName] = _.pickBy(this.fields, field => field.group == v);
});
return ret;
}
},
methods: {
getFieldId(name) {
if (this.subForm) {
return `input_${this.subForm}_${name}`;
}
return `input_${name}`;
},
getFieldName(name) {
if (this.subForm) {
return `${this.subForm}[${name}]`;
}
return name;
},
setValue(name, value, lang) {
const isIntl = this.fields[name].multilingual || this.fields[name].intl;
if (!isIntl) {
this.$set(this.model, name, value);
// _.set(this.model, name, value);
} else if (lang && !_.isObject(this.model[name])) {
this.$set(this.model, name, {});
} else {
this.$set(this.model[name], lang, value);
}
return this.$emit("input", this.model);
},
titlize() {},
isShowField(field) {
return (
!field.showWhen || this.model[field.showWhen] || eval(field.showWhen)
);
},
getInputClass() {
return [];
// const classNames = [];
// classNames.push(`col-lg-${field.input_cols ? field.input_cols : "12"}`);
// return classNames;
},
getClass(field) {
const cols = field.cols ? field.cols : 12;
const classNames = ["col-lg-" + cols, "col-" + Math.min(12, cols * 2)];
return classNames;
},
hasError(name) {
return _.find(this.errors, v => v.field == name);
},
submitForm() {
this.$refs.submitButton.click();
},
handleSubmit() {
if (this.beforeSubmit) {
const ret = this.beforeSubmit(this.model);
if (ret === false) {
return false;
}
}
if (this.submitRawForm) {
this.$refs.form.submit();
return true;
}
if (this.onSubmit) {
return this.onSubmit(this.model);
}
const methodName = String(this.method).toLowerCase();
let formData = this.model;
if (this.useFormData) {
formData = new FormData();
_.mapValues(this.model, (v, k) => formData.append(k, v));
}
this.$http[methodName](this.action, formData)
.then(({ data }) => {
if (this.successMessage) {
this.$snotify.success(this.successMessage);
}
this.errors = [];
this.$emit("success", data);
})
.catch(({ data, status }) => {
if (status == 422) {
this.errors = data.message;
}
});
}
},
mounted() {
this.model = Object.assign({}, this.value);
for (let [k, v] of Object.entries(this.fields)) {
if (v.type === "object" && !this.model[k]) {
this.$set(this.model, k, {});
}
}
this.$watch(
"value",
val => {
this.model = Object.assign({}, val);
export default {
name: "b-form-builder",
components: {},
props: {
parent: {},
subForm: {
type: String,
default: ""
},
{ deep: true }
);
// global.console.log(this.fields, this.model)
},
created() {}
};
id: {
type: String,
default() {
return "form_" + parseInt(Math.random() * 9999);
}
},
auth: {
type: Object,
default() {
return {};
}
},
col: {
type: Number,
default: 12
},
languages: {},
inline: {
type: Boolean,
default: false
},
useFormData: {
type: Boolean,
default: false
},
submitRawForm: {
type: Boolean,
default: false
},
groupBy: {
type: String,
default: null
},
value: {
type: Object,
default() {
return {};
}
},
fields: {
required: true,
default() {
return {};
}
},
layout: {
required: false,
default() {
return {};
}
},
onSubmit: {
type: Function,
required: false
},
beforeSubmit: {
type: Function,
required: false
},
action: {},
method: {
default: "post"
},
submitText: {
default() {
return this.$t ? this.$t("actions.save") : "Submit";
}
},
backText: {
default() {
return this.$t ? this.$t("actions.back") : "Back";
}
},
successMessage: {
default() {
return this.$t ? this.$t("messages.succeed") : "Succeed";
}
}
},
data() {
return {
model: null,
errors: []
};
},
watch: {},
computed: {
tag() {
return this.subForm ? "div" : "form";
},
actionUrl() {
return global.API_URI + this.action;
},
groupedFields() {
const ret = {};
_.keys(_.groupBy(this.fields, this.groupBy)).map(v => {
let tabName = v;
if (v === "undefined") {
v = null;
tabName = this.$t("messages.default");
}
ret[tabName] = _.pickBy(this.fields, field => field.group == v);
});
return ret;
}
},
methods: {
getFieldId(name) {
if (this.subForm) {
return `input_${this.subForm}_${name}`;
}
return `input_${name}`;
},
getFieldName(name) {
if (this.subForm) {
return `${this.subForm}[${name}]`;
}
return name;
},
setValue(name, value, lang) {
const isIntl = this.fields[name].multilingual || this.fields[name].intl;
if (!isIntl) {
this.$set(this.model, name, value);
// _.set(this.model, name, value);
} else if (lang && !_.isObject(this.model[name])) {
this.$set(this.model, name, {});
} else {
this.$set(this.model[name], lang, value);
}
return this.$emit("input", this.model);
},
titlize() {
},
isShowField(field) {
return (
!field.showWhen || this.model[field.showWhen] || eval(field.showWhen)
);
},
getInputClass() {
return [];
// const classNames = [];
// classNames.push(`col-lg-${field.input_cols ? field.input_cols : "12"}`);
// return classNames;
},
getClass(field) {
const cols = field.cols ? field.cols : 12;
const classNames = ["col-lg-" + cols, "col-" + Math.min(12, cols * 2)];
return classNames;
},
hasError(name) {
return _.find(this.errors, v => v.field == name);
},
submitForm() {
this.$refs.submitButton.click();
},
handleSubmit() {
if (this.beforeSubmit) {
const ret = this.beforeSubmit(this.model);
if (ret === false) {
return false;
}
}
if (this.submitRawForm) {
this.$refs.form.submit();
return true;
}
if (this.onSubmit) {
return this.onSubmit(this.model);
}
const methodName = String(this.method).toLowerCase();
let formData = this.model;
if (this.useFormData) {
formData = new FormData();
_.mapValues(this.model, (v, k) => formData.append(k, v));
}
this.$http[methodName](this.action, formData)
.then(({data}) => {
if (this.successMessage) {
this.$snotify.success(this.successMessage);
}
this.errors = [];
this.$emit("success", data);
})
.catch(({data, status}) => {
if (status == 422) {
this.errors = data.message;
}
});
}
},
mounted() {
this.model = Object.assign({}, this.value);
for (let [k, v] of Object.entries(this.fields)) {
if (v.type === "object" && !this.model[k]) {
this.$set(this.model, k, {});
}
}
this.$watch(
"value",
val => {
this.model = Object.assign({}, val);
},
{deep: true}
);
// global.console.log(this.fields, this.model)
},
created() {
}
};
</script>

View File

@@ -0,0 +1,24 @@
<template>
<div>
<vue-json-pretty
:data=value
:highlightMouseoverNode="true"
>
</vue-json-pretty>
</div>
</template>
<script>
import VueJsonPretty from 'vue-json-pretty'
export default {
name: "b-json-pretty",
components: {
VueJsonPretty
},
props: {
value: null,
},
}
</script>

View File

@@ -5,13 +5,14 @@ import _ from 'lodash'
import Vue from 'vue'
import App from './App'
import router from './router'
import store, { types } from './store'
import store, {types} from './store'
import './http'
import i18n from './i18n'
import inflection from 'inflection'
Vue.prototype.$inflection = inflection
import { sync } from 'vuex-router-sync'
import {sync} from 'vuex-router-sync'
import BootstrapVue from 'bootstrap-vue'
import Snotify from 'vue-snotify'
@@ -22,6 +23,7 @@ import 'bootstrap-vue/dist/bootstrap-vue.css'
import './scss/style.scss'
import "vue-snotify/styles/material.css"
Vue.use(Snotify)
Vue.config.productionTip = false
@@ -30,9 +32,11 @@ Vue.use(BootstrapVue)
import './form'
import storage from './storage'
Vue.prototype.$storage = storage
import VueElementLoading from 'vue-element-loading'
Vue.component('BLoading', VueElementLoading)
Vue.prototype._ = _
@@ -47,12 +51,8 @@ new Vue({
router,
store,
i18n,
watch: {
},
methods: {
},
watch: {},
methods: {},
render: h => h(App),
created() {

View File

@@ -8766,6 +8766,11 @@ vue-i18n@^8.17.0, vue-i18n@^8.17.3:
resolved "https://registry.npm.taobao.org/vue-i18n/download/vue-i18n-8.17.3.tgz?cache=0&sync_timestamp=1587630870936&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-i18n%2Fdownload%2Fvue-i18n-8.17.3.tgz#f366082d5784c3c35e8ffda733cb3f3990a3900d"
integrity sha1-82YILVeEw8Nej/2nM8s/OZCjkA0=
vue-json-pretty@^1.6.3:
version "1.6.3"
resolved "https://registry.npm.taobao.org/vue-json-pretty/download/vue-json-pretty-1.6.3.tgz#c7f378f3c9f68977047de28197735bc2cf81b15b"
integrity sha1-x/N488n2iXcEfeKBl3Nbws+BsVs=
vue-loader@^15.9.1:
version "15.9.1"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.1.tgz#bd2ab8f3d281e51d7b81d15390a58424d142243e"