first commit

This commit is contained in:
long2ice
2020-04-06 18:21:35 +08:00
commit 82a26ed9c4
134 changed files with 19430 additions and 0 deletions

View File

@@ -0,0 +1,263 @@
<template>
<div class="avatar-cropper">
<div v-if="dataUrl" class="avatar-cropper-overlay">
<div class="avatar-cropper-mark"><a href="javascript:;" class="avatar-cropper-close" @click="destroy">&times;</a></div>
<div class="avatar-cropper-container">
<div class="avatar-cropper-image-container">
<img ref="img" :src="dataUrl" alt="" @load.stop="createCropper" />
</div>
<div class="avatar-cropper-footer">
<button class="avatar-cropper-btn" type="button" @click="destroy" v-text="labels.cancel">Cancel</button>
<button class="avatar-cropper-btn" type="button" @click="submit" v-text="labels.submit">Submit</button>
</div>
</div>
</div>
<input ref="input" type="file" class="avatar-cropper-img-input" :accept="mimes">
</div>
</template>
<script>
import 'cropperjs/dist/cropper.css'
import Cropper from 'cropperjs'
export default {
props: {
trigger: {
type: [String, Element],
required: true
},
uploadHandler: {
type: Function,
},
uploadUrl: {
type: String,
},
uploadHeaders: {
type: Object,
},
uploadFormName: {
type: String,
default: 'file'
},
uploadFormData: {
type: Object,
default() {
return {}
}
},
cropperOptions: {
type: Object,
default() {
return {
aspectRatio: 1,
autoCropArea: 1,
viewMode: 1,
movable: false,
zoomable: false,
}
}
},
outputOptions: {
type: Object,
default() {
return {
width: 512,
height: 512
}
}
},
outputMime: {
type: String,
default: 'image/jpeg'
},
outputQuality: {
type: Number,
default: 0.9
},
mimes: {
type: String,
default: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'
},
labels: {
type: Object,
default() {
return {
submit: "提交",
cancel: "取消"
}
}
}
},
data() {
return {
cropper: undefined,
dataUrl: undefined,
filename: undefined
}
},
methods: {
destroy() {
this.cropper.destroy()
this.$refs.input.value = '';
this.dataUrl = undefined
},
submit() {
this.$emit('submit')
if (this.uploadUrl) {
this.uploadImage()
} else if (this.uploadHandler) {
this.uploadHandler(this.cropper)
} else {
this.$emit('error', 'No upload handler found.', 'user')
}
this.destroy()
},
pickImage() {
this.$refs.input.click()
},
createCropper() {
this.cropper = new Cropper(this.$refs.img, this.cropperOptions)
},
uploadImage() {
this.cropper.getCroppedCanvas(this.outputOptions).toBlob((blob) => {
let form = new FormData()
let xhr = new XMLHttpRequest()
let data = Object.assign({}, this.uploadFormData)
for (let key in data) {
form.append(key, data[key])
}
form.append(this.uploadFormName, blob, this.filename)
this.$emit('uploading', form, xhr)
xhr.open('POST', this.uploadUrl, true)
for (let header in this.uploadHeaders) {
xhr.setRequestHeader(header, this.uploadHeaders[header])
}
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
let response = ''
try {
response = JSON.parse(xhr.responseText)
} catch (err) {
response = xhr.responseText
}
this.$emit('completed', response, form, xhr)
if ([200, 201, 204].indexOf(xhr.status) > -1) {
this.$emit('uploaded', response, form, xhr)
} else {
this.$emit('error', 'Image upload fail.', 'upload', xhr)
}
}
}
xhr.send(form);
}, this.outputMime, this.outputQuality)
}
},
mounted() {
// listen for click event on trigger
let trigger = typeof this.trigger == 'object' ? this.trigger : document.querySelector(this.trigger)
if (!trigger) {
this.$emit('error', 'No avatar make trigger found.', 'user')
} else {
trigger.addEventListener('click', this.pickImage)
}
// listen for input file changes
let fileInput = this.$refs.input
fileInput.addEventListener('change', () => {
if (fileInput.files != null && fileInput.files[0] != null) {
let reader = new FileReader()
reader.onload = (e) => {
this.dataUrl = e.target.result
}
reader.readAsDataURL(fileInput.files[0])
this.filename = fileInput.files[0].name || 'unknown'
this.$emit('changed', fileInput.files[0], reader)
}
})
}
}
</script>
<style lang="scss">
.avatar-cropper {
.avatar-cropper-overlay {
text-align: center;
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top:0;
left:0;
right:0;
bottom:0;
z-index: 99999;
}
.avatar-cropper-img-input {
display: none;
}
.avatar-cropper-close {
float: right;
padding: 20px;
font-size: 3rem;
color: #fff;
font-weight: 100;
text-shadow: 0px 1px rgba(40, 40, 40,.3);
}
.avatar-cropper-mark {
position: fixed;
top:0;
left:0;
right:0;
bottom:0;
background: rgba(0, 0, 0, .10);
}
.avatar-cropper-container {
background: #fff;
z-index: 999;
box-shadow: 1px 1px 5px rgba(100, 100, 100, .14);
.avatar-cropper-image-container {
position: relative;
max-width: 400px;
height: 300px;
}
img {
max-width: 100%;
height: 100%;
}
.avatar-cropper-footer {
display: flex;
align-items: stretch;
align-content: stretch;
justify-content: space-between;
.avatar-cropper-btn {
width: 50%;
padding: 15px 0;
cursor: pointer;
border: none;
background: transparent;
outline: none;
&:hover {
background-color: #2aabd2;
color: #fff;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,14 @@
<template>
<b-table>
</b-table>
</template>
<script>
export default {
data(){
return {}
}
}
</script>

View File

@@ -0,0 +1,70 @@
<template>
<div>
<component :is="component" v-if="component"></component>
</div>
</template>
<script>
export default {
props: {
config: {
required: true
}
},
data(){
return {
component: null
}
},
mounted() {
},
watch: {
config: 'setComponent'
},
methods: {
setComponent() {
if (!this.config) {
return this.component = null;
}
if (typeof this.config === 'string') {
return this.component = {
template: this.config
}
}
const component = {};
const config = this.config;
const rawData = Object.assign({}, config.data);
component.data = rawData;
component.name = config.name
if (typeof component.data !== "function") {
component.data = function() {
return Object.assign({}, rawData);
};
}
const wrapFunction = v => {
if (Array.isArray(v)) {
return new Function(...v);
}
return v;
};
const mapValues = (obj = {}) => {
return Object.entries(obj).reduce((acc, [k, v]) => {
acc[k] = wrapFunction(v);
return acc;
}, {});
};
component.methods = mapValues(config.methods, wrapFunction);
component.computed = mapValues(config.computed, wrapFunction);
component.created = config.created ? wrapFunction(config.created) : null;
component.mounted = config.mounted ? wrapFunction(config.mounted) : null;
component.watch = config.watch ? wrapFunction(config.watch) : null;
component.template = config.template
component.render = wrapFunction(config.render)
this.component = Object.assign({}, component, {});
}
}
};
</script>

View File

@@ -0,0 +1,43 @@
<script>
// import Chart from "vue-chartjs";
export default {
// extends: Chart.Pie,
props: {
resource: {
type: String,
required: true
},
group: {
type: String,
required: true
},
},
data() {
return {
options: {}
};
},
computed: {
resourceUri() {
return this.resource + "/stat";
}
},
methods: {
fetch() {
this.$http.get(this.resourceUri, {
params: {group: this.group}
}).then(({ data }) => {
this.options = data;
this.renderChart(this.options)
});
},
},
mounted() {
// Overwriting base render method with actual data.
this.fetch()
}
};
</script>

View File

@@ -0,0 +1,165 @@
<template>
<!-- <component :is="tag" :header="header"> -->
<div class="data-form">
<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>
</div>
<div class="col col-md-4 text-right hidden-sm-down">
<b-btn @click="$router.go(-1)">{{$t('actions.back')}}</b-btn>
<b-btn variant="primary" @click="$refs.form.submitForm()">{{$t('actions.save')}}</b-btn>
</div>
</div>
<b-form-builder
:languages="$store.state.site.languages"
group-by="group"
v-if="loaded"
:auth="auth"
:layout="layout"
:fields="fields"
ref="form"
v-model="model"
:action="resourceUri"
:method="method"
@success="onSuccess"
></b-form-builder>
</div>
<!-- </component> -->
</template>
<script>
import { mapState, mapGetters } from "vuex";
import _ from "lodash";
export default {
components: {},
props: {
resource: {
type: String,
required: true
},
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: `
${_.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;
},
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"])
},
methods: {
fetch() {
if (this.isNew) {
this.model = {};
this.loaded = true;
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.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);
}
}
},
mounted() {
this.site.fetched && this.fetchForm()
},
created() {
// this.fetchForm();
}
};
</script>

View File

@@ -0,0 +1,541 @@
<template>
<div class="data-table">
<div v-if="site.grid_style === 1">
<div class="pb-3">
<b-btn
class="mr-2"
:to="'/rest/' + resourceUri + '/create'"
variant="secondary"
v-if="_.get(actions,'toolbar.create') !== false"
>
<i class="icon-plus"></i>
{{$t('actions.create')}}
</b-btn>
<b-btn
class="mr-2"
@click="fetchGrid"
variant="success"
v-if="_.get(actions, 'toolbar.reload') !== false"
>
<i class="icon-reload"></i>
{{$t('actions.reload')}}
</b-btn>
<b-btn
class="mr-2"
v-for="button in _.get(actions, 'toolbar.extra', [])"
:key="button.label"
v-bind="button"
>{{button.label}}</b-btn>
<b-btn
@click="removeAll"
class="pull-right"
variant="second"
v-if="_.get(actions, 'toolbar.delete_all') === true"
>
<i class="icon-trash"></i>
{{$t('actions.delete_all')}}
</b-btn>
</div>
<div class="mb-2 data-table-search" v-if="!_.isEmpty(searchFields)">
<b-form-builder
:inline="true"
:fields="searchFields"
:action="searchUri"
v-model="searchModel"
:submitText="$t('actions.search')"
backText
method="get"
:on-submit="onSearch"
>
<div slot="extra-buttons" class="ml-2">
<b-button
type="button"
@click="searchAndExport"
variant="success"
v-if="_.get(actions, 'export')"
>{{$t('actions.search_and_export')}}</b-button>
<iframe :src="iframeSrc" style="width:0;height:0;border:none;"></iframe>
</div>
</b-form-builder>
</div>
<div class v-if="description" v-html="description"></div>
<b-row>
<b-col cols="8">
<b-pagination
:limit="limitPages"
:total-rows="totalRows"
:per-page="perPage"
v-model="page"
/>
</b-col>
<b-col cols="4" class="text-right">
<p>{{$t('messages.paginate', {total: totalRows})}}</p>
</b-col>
</b-row>
</div>
<div v-else class="table-toolbar style-2">
<div class="search-box">
<div class="flex">
<b-form-builder
:inline="true"
v-if="!_.isEmpty(searchFields)"
:fields="searchFields"
:action="searchUri"
v-model="searchModel"
:submitText="$t('actions.search')"
backText
method="get"
:on-submit="onSearch"
>
<div slot="extra-buttons" class="ml-2">
<b-button
type="button"
@click="searchAndExport"
variant="success"
v-if="_.get(actions, 'export')"
>{{$t('actions.search_and_export')}}</b-button>
<iframe :src="iframeSrc" style="width:0;height:0;border:none;"></iframe>
</div>
</b-form-builder>
<b-btn
:to="'/page/' + resourceUri + '.create'"
variant="link"
v-if="_.get(actions,'toolbar.create') !== false"
>
<i class="iconfont icon-xinjianshiti"></i>
{{$t('actions.create')}}
</b-btn>
<b-btn
@click="fetchGrid"
variant="link"
v-if="_.get(actions, 'toolbar.reload') !== false"
>
<i class="iconfont icon-shuaxin"></i>
{{$t('actions.reload')}}
</b-btn>
</div>
<!-- <b-pagination :limit="limitPages" :total-rows="totalRows" :per-page="perPage" v-model="page" /> -->
<div class="flex">
<div class="result">共{{totalRows}}个结果</div>
<div class="pagination">
<span class="left-arrow iconfont icon-tuichu" @click="previousPage"></span>
<div class="go-page flex">
<input type="number" v-model="inputPage">
<div>/{{totalRows == 0 ? 1 : Math.ceil(totalRows/limitPages)}}</div>
<button @click="goPage">GO</button>
</div>
<span class="right-arrow iconfont icon-jinru" @click="nextPage"></span>
</div>
</div>
</div>
</div>
<b-table
class="data-table bg-white"
:sort-by.sync="sortBy"
:sort-desc.sync="sortDesc"
:no-local-sorting="true"
:fields="fields"
:items="items"
>
<template
v-for="(field, key) in _.omit(fields, '_actions')"
:slot="`HEAD_${key}`"
>
<div
:key="key"
:class="{'text-right': ['number'].includes(field.type)}"
>{{field.label || key}}</div>
</template>
<template v-for="(field, key) in fields" :slot="key" slot-scope="row">
<b-data-value
:field="field"
:key="key"
:lang="currentLanguage"
:name="key"
:model="row.item"
short-id
/>
</template>
<template slot="_actions" slot-scope="row">
<b-btn
size="sm"
class="mr-2"
v-for="(button, key) in _.get(actions, 'addon')"
:key="key"
v-bind="button"
>{{button.label}}</b-btn>
<b-btn
size="sm"
class="mr-2"
variant="success"
@click.stop="show(row.item)"
v-if="_.get(actions, 'buttons.show') !== false"
>
<i class="icon-eye"></i>
{{$t('actions.view')}}
</b-btn>
<b-btn
size="sm"
class="mr-2"
variant="primary"
@click.stop="edit(row.item)"
v-if="_.get(actions, 'buttons.edit') !== false"
>
<i class="icon-pencil"></i>
{{$t('actions.edit')}}
</b-btn>
<b-btn
size="sm"
class="mr-2"
variant="second"
@click.stop="remove(row.item)"
v-if="_.get(actions, 'buttons.delete') !== false"
>
<i class="icon-trash"></i>
{{$t('actions.delete')}}
</b-btn>
</template>
</b-table>
<div v-if="site.grid_style === 1">
<b-row>
<b-col cols="8">
<b-pagination
:limit="limitPages"
:total-rows="totalRows"
:per-page="perPage"
v-model="page"
/>
</b-col>
<b-col cols="4" class="text-right">
<p>{{$t('messages.paginate', {total: totalRows})}}</p>
</b-col>
</b-row>
</div>
<div v-else>
<div class="footer-pagination">
<div class="flex">
<div class="result">共{{totalRows}}个结果</div>
<div class="pagination">
<span class="left-arrow iconfont icon-tuichu" @click="previousPage"></span>
<div class="go-page flex">
<input type="number" v-model="inputPage">
<div>/{{totalRows == 0 ? 1 : Math.ceil(totalRows/limitPages)}}</div>
<button @click="goPage">GO</button>
</div>
<span class="right-arrow iconfont icon-jinru" @click="nextPage"></span>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import _ from "lodash";
import BFormBuilder from "./FormBuilder";
import BDataValue from "./DataValue";
import { mapState, mapGetters } from "vuex";
export default {
components: {
BFormBuilder,
BDataValue
},
props: {
resource: {
type: String,
required: true
},
gridPath: {
type: String,
default: "grid",
required: false
}
},
data() {
return {
iframeSrc: null,
pause: true, //修复切换页面时page等参数的自动变更会导致多次fetch的问题
page: 1,
inputPage: 1,
perPage: 6,
sortBy: this.$config.primaryKey,
sortDesc: true,
description: "",
fields: {},
filter: {},
choices: {},
totalRows: 0,
items: [],
searchFields: {},
searchModel: {},
where: {},
title: "",
limitPages: 10
};
},
computed: {
...mapState(["site", "i18n"]),
...mapGetters(["currentMenu", "currentLanguage"]),
actions() {
return _.get(this.fields, "_actions");
},
header() {
return `
${this.currentMenu.name || ""}
<small> ${this.resource.toUpperCase()} </small>
`;
},
with() {
return _.filter(
_.map(
this.fields,
(v, k) =>
v.ref &&
v.ref
.split(".")
.slice(0, -1)
.join(".")
)
//.concat(_.map(this.fields, (v, k) => (v.ref ? k : null)))
);
},
searchUri() {
return this.resource;
},
resourceUri() {
return [this.site.resource_prefix, this.resource]
.map(v => v.trim("/"))
.filter(v => v)
.join("/");
},
gridUri() {
return [this.site.resource_prefix, this.resource, this.gridPath]
.filter(v => v)
.join("/");
},
sort: {
get() {
if (!this.sortBy) {
return null;
}
return {
[this.sortBy]: this.sortDesc ? -1 : 1
};
},
set(val) {
for (let k in val) {
this.sortBy = k;
this.sortDesc = val === -1;
}
}
},
query: {
get() {
return {
sort: this.sort,
page: this.page,
perPage: this.perPage,
with: this.with,
where: this.where
};
},
set(val) {
this.sort = val.sort;
this.page = val.page;
this.perPage = val.perPage;
this.where = val.where;
}
}
},
watch: {
"site.fetched"(val) {
if (val) {
this.fetchGrid(true);
}
},
"$route.params.resource"() {
this.inputPage = 1;
this.pause = true;
this.fetchGrid(true);
},
page: "fetch",
sort: "fetch",
where: "fetch",
"$route.query.query"() {
this.applyQuery();
this.fetch();
}
// query(val) {
// this.$emit("change query", val);
// // this.$router.push({
// // query: {
// // query: JSON.stringify(val)
// // }
// // });
// }
},
methods: {
goPage() {
const totalPages = Math.ceil(this.totalRows / this.limitPages);
if (this.inputPage <= 0 || this.inputPage > totalPages) {
this.$snotify.warning("请输入正确页码");
this.inputPage = 1;
this.page = 1;
return;
}
this.page = this.inputPage;
},
previousPage() {
// const totalPages = Math.ceil(this.totalRows/this.limitPages)
if (this.inputPage > 1) {
this.inputPage--;
this.page = this.inputPage;
} else {
this.$snotify.warning("已是第一页");
this.inputPage = 1;
this.page = 1;
}
},
nextPage() {
const totalPages = Math.ceil(this.totalRows / this.limitPages);
if (this.inputPage < totalPages) {
this.inputPage++;
this.page = this.inputPage;
} else {
this.$snotify.warning("后面没有了");
this.inputPage = 1;
this.page = 1;
}
},
fetch() {
if (this.pause) {
return;
}
this.pause = true;
this.$http
.get(this.resourceUri, {
params: {
query: JSON.stringify(this.query)
}
})
.then(({ data }) => {
const {
data: items,
total,
perPage,
fields,
searchFields,
searchModel
} = data;
fields && (this.fields = fields);
searchFields && (this.searchFields = searchFields);
searchModel && (this.searchModel = searchModel);
this.items = items;
this.description = data.description;
this.totalRows = total;
this.perPage = perPage;
this.pause = false;
});
},
searchAndExport() {
const query = JSON.stringify({
where: _.clone(this.searchModel),
with: _.clone(this.with)
});
this.iframeSrc = "";
setTimeout(() => {
this.iframeSrc = `${global.API_URI}${
this.resourceUri
}/export?query=${query}&token=${this.$store.state.auth.token}`;
}, 50);
},
fetchGrid(fetchData = false) {
// if (this.$store.state.site.use_field_apis === false) {
// this.pause = false;
// this.applyQuery();
// this.fetch();
// return
// }
this.query = {};
this.$http.get(this.gridUri).then(({ data }) => {
this.fields = data.fields;
if (!this.fields._actions && this.fields._actions !== false) {
this.fields._actions = {};
}
if (this.fields._actions) {
if (!this.fields._actions.label) {
this.fields._actions.label = "";
}
}
this.searchFields = data.searchFields;
this.searchModel = data.searchModel;
this.pause = false;
if (fetchData) {
this.applyQuery();
this.fetch();
}
});
},
applyQuery() {
const query = this.$route.query.query;
if (!query) {
this.query = {};
return;
}
this.query = _.isString(query) ? JSON.parse(query) : query;
// this.searchModel = this.where
},
get(item, path) {
const [model, field] = path.split(".");
return item[model][field];
},
show(item) {
this.$router.push({
path: this.resource + "/" + item[this.$config.primaryKey]
});
},
edit(item) {
if (this.site.grid_style == 2) {
return this.$router.push({
path: `/page/${this.resource}.${item[this.$config.primaryKey]}.edit`
});
}
this.$router.push({
path: this.resource + "/" + item[this.$config.primaryKey] + "/edit"
});
},
remove(item) {
if (window.confirm(this.$t("messages.confirm_delete"))) {
this.$http
.delete(this.resourceUri + "/" + item[this.$config.primaryKey])
.then(() => {
this.$snotify.success(this.$t("messages.deleted"));
this.fetch();
});
}
},
removeAll() {
if (window.confirm(this.$t("messages.confirm_delete_all"))) {
this.$http.delete(this.resourceUri).then(() => {
this.$snotify.success(this.$t("messages.deleted_all"));
this.fetch();
});
}
},
onSearch() {
this.where = this.searchModel;
this.fetch();
}
},
mounted() {
this.site.fetched && this.fetchGrid(true);
// console.log('mounted');
},
created() {
// this.applyQuery();
}
};
</script>

View File

@@ -0,0 +1,192 @@
<template>
<div class="break-all data-value">
<template v-if="['html'].includes(field.type)">
<div v-html="value" class=" data-value-html"></div>
</template>
<template v-else-if="['number'].includes(field.type)">
<div class=" text-right">{{value}}</div>
</template>
<template v-else-if="['image'].includes(field.type)">
<template v-if="field.multiple">
<b-img class="type-image" :key="v" v-for="v in value" :src="preview(v)" v-bind="field" @click.stop="previewInModal(v)"/>
</template>
<b-img class="type-image" v-else :src="preview(value)" v-bind="field" fluid @click.stop="previewInModal(value)"/>
</template>
<template v-else-if="['audio', 'video'].includes(field.type)">
<component :is="field.type" :src="value" controls />
</template>
<template v-else-if="['file'].includes(field.type)">
<a target="blank" :href="value" v-if="value">
<i class="fa fa-file"></i> {{String(value).split('/').pop()}}
</a>
<div v-else class="text-muted">
</div>
</template>
<template v-else-if="['link'].includes(field.type)">
<a :class="field.classes" :href="value" :target="field.target">
<i :class="field.icon" v-if="field.icon"></i>
{{field.text || field.label}}
</a>
</template>
<template v-else-if="['switch', 'boolean', 'checkbox'].includes(field.type)">
<b-badge :variant="value ? 'success' : 'danger'">
{{value ? 'Yes' : 'No'}}
</b-badge>
</template>
<template v-else-if="field.refLabel">
<template>
{{_.get(model || {}, [name, ...field.refLabel.split('.')])}}
</template>
</template>
<template v-else-if="field.ref">
<template v-if="field.multiple">
{{_.map(_.get(model || {}, field.ref.split('.')[0]), field.ref.split('.')[1]).join(',')}}
</template>
<template v-else-if="isIntl">
{{_.get(model || {}, field.ref.split('.').concat([lang]))}}
</template>
<template v-else>
{{_.get(model || {}, field.ref)}}
</template>
</template>
<template v-else-if="name === $config.primaryKey">
<span v-b-tooltip.hover.top.d100 :title="value" v-if="value">
{{String(shortId ? String(value).substr(-4) : value).toUpperCase()}}
</span>
</template>
<template v-else-if="['date', 'datetime'].includes(field.type)">
<span v-b-tooltip.hover.top.d100 :title="value" v-if="value">
<template v-if="_.isString(value)">
{{$d(new Date(value), timeFormat)}}
</template>
<template v-else-if="_.isArray(value)">
{{$d(new Date(value[0]), timeFormat)}} -
{{$d(new Date(value[1]), timeFormat)}}
</template>
</span>
</template>
<template v-else>
{{value}}
</template>
<b-modal :title="field.label" v-model="showModal">
<b-img :src="preview(previewValue)" class="my-2" center fluid />
</b-modal>
</div>
</template>
<script>
import _ from "lodash";
export default {
data() {
return {
previewValue: null,
showModal: false,
}
},
props: {
field: {
required: true,
type: Object
},
name: {
required: true,
type: String
},
model: {
required: true,
type: Object
},
lang: {
},
shortId: {
required: false,
type: Boolean,
default: false
}
},
computed: {
timeFormat() {
return 'long'
},
isIntl(){
return this.field.intl || this.field.multilingual
},
value() {
// const path = this.name.replace(/\]/g, '').replace(/\[/g, '.').split('.').pop()
// console.log(this.name)
let value = _.get(this.model || {}, this.field.refLabel || this.field.ref || this.name);
if (!value) {
return value
}
if (['select', 'select2', 'radiolist', 'checkboxlist'].includes(this.field.type)) {
const options = _.mapValues(
_.keyBy(this.field.options, "value"),
"text"
);
return options[value];
}
if (this.lang && this.isIntl) {
return _.get(value, this.lang, null)
}
return value;
}
},
methods: {
previewInModal(value) {
this.showModal = true
this.previewValue = value
},
preview(file) {
if (!file) {
return;
}
if (typeof file === "string") {
return file;
}
if (file instanceof File) {
return URL.createObjectURL(file);
}
return ''
}
}
};
</script>
<style lang="scss">
.data-table td .modal-body img{
max-height: inherit;
max-width: 100%;
}
.data-value-html {
max-height: 500px;
max-width: 420px;
overflow-y: scroll;
border: 1px solid #eee;
p {
img {
max-width: 100%;
}
}
}
.data-value {
.type-image {
max-width: 100%;
max-height: 200px;
}
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<div class="page-view">
<div class="data-view">
<legend v-if="model[$config.primaryKey]">{{$t('actions.view')}}: {{model[$config.primaryKey]}}</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>
</tbody>
</table>
</div>
<div slot="footer">
<b-btn @click="$router.go(-1)">{{$t('actions.back')}}</b-btn>
</div>
</div>
</template>
<script>
import BDataValue from "./DataValue";
import _ from 'lodash'
import { mapState, mapGetters } from 'vuex'
export default {
components: {
BDataValue
},
props: {
resource: {
type: String,
required: true
},
id: {
default: '',
required: true
},
viewPath: {
type: String,
default: "view",
required: false
},
},
data() {
return {
choices: {},
fields: {},
model: {},
errors: [],
items: []
};
},
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 `
''
<small> ${this.resource.toUpperCase()} </small>
`
},
},
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;
delete this.fields._actions
this.fetch();
});
},
},
watch: {
id: "fetchForm",
"site.fetched"(val){
if (val) {
this.fetchView(true)
}
},
},
mounted() {
},
created() {
this.site.fetched && this.fetchView();
}
};
</script>

View File

@@ -0,0 +1,42 @@
<template>
<b-modal id="file-manager" title="文件管理器">
<b-table :items="items" :fields="fields">
<b-data-value slot-scope="data" slot="url" name="url" :model="data.item" :field="fields.url"></b-data-value>
</b-table>
</b-modal>
</template>
<script>
import { mapState } from "vuex";
export default {
data() {
return {
fields: {
originalname: { label: "原文件名" },
url: { type: "image", style: { height: "3em" } }
},
items: []
};
},
computed: {
...mapState(["site"]),
fileUrl() {
return [this.site.resource_prefix, "files"].filter(v => v).join("/");
}
},
watch: {
"site.fetched": "fetch"
},
methods: {
fetch() {
this.$http.get(this.fileUrl).then(({ data }) => {
this.items = data.data;
});
}
},
created() {
this.site.fetched && this.fetch();
}
};
</script>

View File

@@ -0,0 +1,35 @@
<template>
<div class="app-footer">
<footer class="bg-light" v-html="site.footer" v-if="site.footer"></footer>
<footer class="bg-light" v-else>
<span class="ml-auto">
GitHub
<a href="https://github.com/wxs77577/rest-admin">REST-ADMIN</a>
</span>
</footer>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
computed: {
...mapState(["site"])
}
};
</script>
<style lang="scss">
.app-footer {
// position: fixed;
bottom: 0;
right: 0;
width: 100%;
z-index: 9;
letter-spacing: 1px;
& > footer {
// margin-left: 215px;
padding: 0.5em;
text-align: right;
}
}
</style>

View File

@@ -0,0 +1,373 @@
<template>
<div v-if="model">
<component
:is="tag"
ref="form"
:action="actionUrl"
:method="method"
class="form form-inline"
@submit.prevent="handleSubmit"
v-if="inline"
enctype="multipart/form-data"
>
<input type="hidden" name="token" :value="auth.token" />
<template v-for="(field, name) in fields">
<label
:for="'input_' + name"
class="m-1"
:key="name"
v-if="field.label !== false"
>{{field.label || $inflection.titleize(name)}}</label>
<b-form-field
:languages="languages"
:parent="parent || model"
class="m-1 mr-4"
@input="setValue(name, arguments[0], arguments[1])"
:value="model[name]"
:id="getFieldId(name)"
:name="name"
:field="field"
:state="!hasError(name)"
:key="id + '_' +name"
/>
</template>
<slot name="actions">
<b-button type="submit" variant="primary" ref="submitButton" class="mr-1">{{submitText}}</b-button>
<b-button
type="button"
variant="secondary"
@click="$router.go(-1)"
v-if="backText"
>{{backText}}</b-button>
<slot name="extra-buttons"></slot>
</slot>
</component>
<component
:is="tag"
ref="form"
:action="actionUrl"
:method="method"
class="form"
@submit.prevent="handleSubmit"
enctype="multipart/form-data"
v-else
>
<input type="hidden" name="token" :value="auth.token" />
<b-tabs
class="my-3"
v-if="groupBy"
:class="{'hide-group-name': _.get(layout, 'hideGroupName')}"
>
<b-tab
v-for="(subFields, tabName) in groupedFields"
:title="_.get(layout, `tabs.${tabName}.name`) || tabName || $t('messages.default')"
:key="tabName"
>
<div class="row form-cols">
<b-col :md="_.get(layout, `tabs.${tabName}.cols`, 12)">
<b-row class="mt-4">
<template v-for="(field, name) in subFields">
<b-form-group
:class="getClass(field)"
v-if="isShowField(field) && model"
:state="!hasError(name)"
:key="id + '_' +name"
v-bind="field"
:label-for="'input_' + name"
:label="field.label !== false ? (field.label || $inflection.titleize(name)) : ''"
>
<div class>
<b-form-field
:languages="languages"
:class="getInputClass(field)"
:parent="parent || model"
@input="setValue(name, arguments[0], arguments[1])"
:value="model[name]"
:name="name"
:field="field"
:state="!hasError(name)"
:id="'input_' + name"
/>
</div>
</b-form-group>
</template>
</b-row>
</b-col>
<b-col
:md="12 - _.get(layout, `tabs.${tabName}.cols`, 0)"
v-if="!!_.get(layout, `tabs.${tabName}.right`)"
>
<div v-html="_.get(layout, `tabs.${tabName}.right`)"></div>
</b-col>
</div>
</b-tab>
</b-tabs>
<div class="row" v-else>
<template v-for="(field, name) in fields">
<b-form-group
:class="getClass(field)"
v-if="isShowField(field) && model"
:state="!hasError(name)"
:key="[id,subForm,name].join()"
v-bind="field"
:label-for="getFieldId(name)"
:label="field.label !== false ? (field.label || $inflection.titleize(name)) : ''"
>
<div class>
<b-form-field
:languages="languages"
:class="getInputClass(field)"
:parent="parent || model"
@input="setValue(name, arguments[0], arguments[1])"
:value="model[name]"
:name="getFieldName(name)"
:field="field"
:state="!hasError(name)"
:id="getFieldId(name)"
/>
</div>
</b-form-group>
</template>
</div>
<slot name="actions" v-if="!subForm">
<b-button type="submit" variant="primary" class="mr-1" ref="submitButton">{{submitText}}</b-button>
<b-button
type="button"
variant="secondary"
@click="$router.go(-1)"
v-if="backText"
>{{backText}}</b-button>
</slot>
</component>
</div>
</template>
<script>
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();
// console.log(this.$refs.form);
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,635 @@
<template>
<div>
<div class="languages mb-1" v-if="isIntl">
<span
class="badge mr-1 mb-0 pointer"
:class="`badge-${currentLanguage === key ? 'primary' : 'secondary'}`"
v-for="(lang, key) in languages"
:key="key"
@click="changeLanguage(key, name)"
>{{lang}}</span>
</div>
<b-form-select
v-if="['select'].includes(field.type)"
:formatter="getFormatter(field, value)"
:id="id"
:options="options"
v-bind="field"
v-model="model"
:name="name"
></b-form-select>
<div v-else-if="['select2'].includes(field.type)">
<b-select v-model="model" v-bind="field" :parent="parent"></b-select>
</div>
<b-tree-select
:normalizer="treeSelectNormalizer"
value-consists-of="LEAF_PRIORITY"
v-else-if="['tree', 'treeselect'].includes(field.type)"
v-bind="field"
v-model="model"
/>
<!-- <b-select v-if="['select', 'select2'].includes(field.type)" track-by="value" label="text" @input="model = arguments[0]" :id="id" v-bind="field" :title="value" /> -->
<b-date-picker
v-else-if="['date', 'datetime'].includes(field.type)"
:name="name"
v-bind="field"
v-model="model"
/>
<b-form-radio-group
v-else-if="['radiolist'].includes(field.type)"
:name="name"
v-bind="field"
v-model="model"
>
<!-- <b-form-radio :key="choice.value" :value="choice.value" v-for="choice in field.options">{{choice.text}}</b-form-radio> -->
</b-form-radio-group>
<b-form-checkbox-group
:name="name"
v-else-if="['checkboxlist'].includes(field.type)"
v-bind="field"
v-model="model"
>
<!-- <b-form-checkbox :key="choice.value" :value="choice.value" v-for="choice in field.options">{{choice.text}}</b-form-checkbox> -->
</b-form-checkbox-group>
<div v-else-if="['checkboxtable'].includes(field.type)" class="checkboxtable">
<div v-for="(options, group) in groupedOptions" :key="group" class="mt-1">
<b-form-checkbox-group :name="name" :options="options" v-bind="field" v-model="model"></b-form-checkbox-group>
</div>
</div>
<b-form-textarea
:name="name"
v-else-if="['textarea'].includes(field.type)"
:id="id"
v-model="model"
v-bind="field"
:rows="field.rows || 3"
/>
<div v-else-if="['file-picker'].includes(field.type)">
<b-btn v-b-modal.file-manager>选择文件</b-btn>
</div>
<!-- <b-uploader v-else-if="['image', 'file', 'audio'].includes(field.type)" :id="id" v-model="model" v-bind="field" /> -->
<component
:is="field.autoUpload === false ? 'b-form-file' : 'b-form-uploader'"
v-else-if="['image', 'file', 'audio', 'video'].includes(field.type)"
:field="field"
v-model="model"
:id="id"
:name="name"
:parent="parent"
/>
<div v-else-if="['switch', 'checkbox'].includes(field.type)">
<b-form-checkbox
variant="success"
v-bind="field"
size="lg"
pill
type="3d"
:id="id"
v-model="model"
/>
</div>
<!-- <b-ueditor :state="state" v-else-if="['wysiwyg', 'html'].includes(field.type)" :id="id" v-bind="field" v-model="model" /> -->
<div v-else-if="['wysiwyg', 'html'].includes(field.type)">
<!-- <b-btn :id="`cropper_${id}`" variant="success">图片裁剪助手</b-btn> -->
<avatar-cropper
v-if="field.cropper"
:trigger="`#cropper_${id}`"
v-bind="cropperOptions"
ref="cropper"
@uploaded="cropperUploaded"
@completed="cropperUploadComplete"
/>
<b-html-editor
ref="editor"
:state="state"
:id="id"
v-bind="field"
v-model="model"
:content="model"
@open-cropper="$refs.cropper && $refs.cropper.pickImage()"
@change="htmlEditorInput"
@keyup.native.enter="wrapFirstLine"
/>
<!-- <b-cropper v-if="field.showCropper" ref="cropper" ></b-cropper> -->
</div>
<div v-else-if="['json'].includes(field.type)">
<b-form-textarea :id="id" v-model="model" v-bind="field" :rows="field.rows || 5"/>
<!-- <v-jsoneditor v-if="model" :value="JSON.parse(model)"
@input="model = JSON.stringify(arguments[0])"
:options="{}" />-->
</div>
<div v-else-if="field.fields">
<div v-if="['array'].includes(field.type) || field.is_array || parent.is_array">
<b-table
hover
bordered
:items="model"
:fields="myFields"
v-if="field.is_table || parent.is_table"
>
<template v-for="(child, k) in myFields" :slot="k" slot-scope="row">
<b-form-field
:parent="parent"
v-model="model[row.index][k]"
:field="child"
:name="`input_${row.index}_${k}`"
:key="`input_${row.index}_${k}`"
:id="`input_${row.index}_${k}`"
/>
</template>
<template slot="HEAD__actions">
<b-btn size="sm" @click="addRow">
<i class="icon-plus"></i>
{{$t('actions.add')}}
</b-btn>
</template>
<template slot="_actions" slot-scope="row">
<b-btn size="sm" @click="model.splice(row.index + 1, 0, {});">
<i class="icon-plus"></i>
{{$t('actions.add')}}
</b-btn>
<b-btn size="sm" @click="model.splice(row.index, 1);">
<i class="icon-trash"></i>
{{$t('actions.delete')}}
</b-btn>
</template>
</b-table>
<b-draggable v-model="model" v-else>
<transition-group tag="div" class="row">
<b-col
v-for="(item, i) in model"
:key="`draggable-${name}-${i}`"
cols
:lg="field.item_cols || 6"
>
<b-card class="mb-4">
<b-row slot="header" class="justify-content-between">
<b-col>No. {{i + 1}}</b-col>
<b-col right class="text-right">
<b-btn size="sm" @click="model.splice(i, 1)">
<i class="icon-trash"></i>
{{$t('actions.delete')}}
</b-btn>
</b-col>
</b-row>
<b-form-group
v-for="(child, key) in myFields"
:key="key"
v-bind="child"
:label-for="`input_${name}_${i}_${key}`"
>
<b-form-field
v-model="model[i][key]"
:parent="parent"
:name="`${name}.${i}.${key}`"
:field="child"
:id="`input_${name}_${i}_${key}`"
/>
</b-form-group>
</b-card>
</b-col>
<b-col
cols
:lg="field.item_cols || 6"
:key="-1"
class="d-flex align-items-center justify-content-center"
>
<b-btn
size="lg"
class="p-5"
block
@click="model = !model? [] : model; model.push({})"
>
<i class="fa fa-plus"></i>
</b-btn>
</b-col>
</transition-group>
</b-draggable>
</div>
<div v-else-if="['object'].includes(field.type)">
<b-card>
<b-form-builder
:sub-form="name || ''"
v-model="model"
:languages="languages"
:fields="myFields"
:parent="parent"
ref="subForm"
></b-form-builder>
<!-- <b-form-group v-for="(child, key) in myFields" :key="key" v-bind="child" :label-for="`input_${name}_${key}`">
<b-form-field :value="_.get(model, `${key}`)"
@input="model[key] = arguments[0]"
:parent="parent" :name="`${name}_${key}`" :field="child" :id="`input_${name}_${key}`" />
</b-form-group>-->
</b-card>
</div>
<div v-else>
<b-form-group
v-for="(child, key) in myFields"
:key="key"
v-bind="child"
:label-for="`input_${name}_${key}`"
>
<b-form-field
v-model="model[key]"
:parent="parent"
:name="key"
:field="child"
:id="`input_${name}_${key}`"
/>
</b-form-group>
</div>
</div>
<b-input-group v-else>
<b-input-group-prepend is-text v-if="field.prependIcon || field.prepend">
<i :class="field.prependIcon" v-if="field.prependIcon"></i>
<span v-else v-html="field.prepend"></span>
</b-input-group-prepend>
<b-form-input
:state="state"
:id="id"
:name="name"
v-bind="field"
v-model="model"
:formatter="getFormatter(field, value)"
/>
<b-input-group-append is-text v-if="field.appendIcon || field.append">
<i :class="field.appendIcon" v-if="field.appendIcon"></i>
<span v-else v-html="field.append"></span>
</b-input-group-append>
</b-input-group>
</div>
</template>
<style>
.checkboxtable .btn-group > .btn:first-child {
text-align: center;
width: 10em;
margin-right: 2px;
}
.vue-html5-editor .content {
max-height: 500px;
}
</style>
<script>
import Vue from "vue";
import BDraggable from "vuedraggable";
import BTreeSelect from "@riophae/vue-treeselect";
import "@riophae/vue-treeselect/dist/vue-treeselect.min.css";
// import BSelect from "vue-multiselect"
import BSelect from "./FormSelect2";
// import BSelect from "@alfsnd/vue-bootstrap-select";
// import "vue-multiselect/dist/vue-multiselect.min.css"
import BDatePicker from "vue2-datepicker";
// import BUeditor from "./UEditor"
import BFormUploader from "./FormUploader";
import VueHtml5Editor from "vue-html5-editor";
// import BJsonEditor from "./JsonEditor"
// import BJsonEditor from "vue-jsoneditor"
import _ from "lodash";
// import "jsoneditor/dist/jsoneditor.min.css"
// Vue.use(BJsonEditor);
export default {
components: {
// BUeditor,
BDatePicker,
BSelect,
BFormUploader,
// BJsonEditor,
BDraggable,
BTreeSelect
},
props: {
languages: {},
id: {
required: true
},
parent: {},
value: {},
field: {},
state: {},
name: {}
},
computed: {
cropperOptions() {
return {
"upload-url": global.API_URI + "upload",
"upload-headers": {
Authorization: "Bearer " + this.$store.state.auth.token
},
"upload-form-name": "file",
"upload-form-data": {
from: "cropper"
},
"cropper-options": {
viewMode: 2,
aspectRatio: _.get(this.field, "cropper.ratio", 1)
},
"output-options": this.field.cropper,
labels: this.field.labels || { submit: "提交", cancel: "取消" }
};
},
isSelect() {
return ["select", "select2"].includes(this.field.type);
},
isSelect2() {
return ["select2"].includes(this.field.type);
},
groupedOptions() {
return _.groupBy(this.options, "group");
},
myFields() {
let fields = this.field.fields;
if (typeof fields == "string") {
const rel = this.parent[fields];
if (!rel) {
return {};
}
try {
fields = JSON.parse(rel);
} catch (e) {
fields = {};
}
}
if (this.parent.is_table) {
fields._actions = { label: this.$t("actions.actions") };
}
return fields;
},
description() {
if (this.field.limit) {
const { width, height } = this.field.limit;
if (width && height) {
return `尺寸:${width}x${height}`;
}
return;
}
return this.field.description;
},
filteredValue() {
let defaultValue = this.value;
if (!this.defaultValue) {
if (["object", "json"].includes(this.field.type)) {
defaultValue = {};
}
if (["array"].includes(this.field.type) || this.field.multiple) {
defaultValue = [];
}
}
// console.log(defaultValue);
return defaultValue;
},
isArrayValue() {
return (
this.field.multiple ||
this.field.is_array ||
this.field.type == "array" ||
this.field.is_table
);
},
isIntl() {
return this.field.intl || this.field.multilingual;
},
selectedValue1() {
let value = this.initSelectedValue
if (this.isArrayValue) {
value = _.filter(
this.options,
v => this.value && this.value.includes(v.value)
);
} else {
value = _.find(this.options, v => this.value == v.value);
}
return value;
},
model: {
get() {
const isArray =
this.field.multiple ||
this.field.is_array ||
this.field.type == "array" ||
this.field.is_table;
const isObject = this.field.type == "object";
let ret = this.value;
if (!this.value) {
if (isArray) {
ret = [];
} else if (isObject) {
ret = {};
}
}
if (this.isIntl) {
// console.log(this.name, ret, this.currentLanguage);
return _.get(ret, this.currentLanguage, "");
}
return ret;
},
set(value) {
this.$emit("input", value, this.currentLanguage);
}
}
},
data() {
const isArray =
this.field.multiple ||
this.field.is_array ||
this.field.type == "array" ||
this.field.is_table;
return {
currentLanguage: this.field.currentLanguage || "en",
options: this.field.options || [],
initSelectedValue: isArray && !this.value ? [] : this.value,
selectedValue: isArray && !this.value ? [] : this.value,
};
},
methods: {
addRow() {
if (!this.parent[this.name]) {
this.$set(this.parent, this.name, []);
}
this.$nextTick(() => {
this.model.push({});
});
},
initEditor() {
const language = "zh-cn";
window.document.execCommand("defaultParagraphSeparator", false, "p");
const visibleModules = this.field.modules || [
"text",
// "color",
// "font",
"heading",
"align",
"list",
"link",
"unlink",
"tabulation",
"image",
// this.field.cropper ? "cropper" : "image",
"hr",
"eraser",
"undo",
"full-screen",
"cropper"
// "info",
];
const cropperClass = _.get(
this.field,
"cropper.icon",
"fa fa-crop text-danger"
);
Vue.use(VueHtml5Editor, {
name: "b-html-editor",
language,
showModuleName: false,
modules: [
{
name: "cropper",
icon: cropperClass,
i18n: "cropper",
show: true,
handler: function(editor) {
editor.$emit("open-cropper");
}
},
{
name: "heading",
icon: "fa fa-header",
i18n: "heading",
show: true,
dashboard: {
template: `
<div>
<button v-for="h in 6" type="button" @click="setHeading(h)">H{{h}}</button>
</div>
`,
methods: {
setHeading(heading) {
this.$parent.execCommand("formatBlock", `h${heading}`);
}
}
}
}
],
i18n: {
"zh-cn": {
cropper: "图片裁剪",
heading: "标题"
}
},
image: {
sizeLimit: 10 * 1024 * 1024,
upload: {
url: global.API_URI + "upload",
headers: {
Authorization: "Bearer " + this.$store.state.auth.token
},
fieldName: "file"
},
// compress: {
// width: 1600,
// height: 1600,
// quality: 80
// },
uploadHandler(res) {
let data;
try {
data = JSON.parse(res);
} catch (e) {
this.$notify.error("上传失败");
}
return data.url;
}
},
visibleModules: visibleModules
});
},
cropperUploaded(res) {
this.$refs.editor.execCommand("insertHTML", `<img src="${res.url}" />`);
},
cropperUploadComplete(data) {
if (data.message) {
this.$snotify.error(data.message);
}
},
changeLanguage(lang) {
this.currentLanguage = lang;
// this.$emit('change-language', lang, name)
// global.console.log(lang, name)
},
htmlEditorInput(value) {
this.$emit("input", value, this.currentLanguage);
},
wrapFirstLine() {
// const value = String(el.target.innerHTML).replace(/^\s*(.+?)(<?)/i, '<p> $1 </p>$2')
// this.$emit('input', value)
},
treeSelectNormalizer(row) {
return {
id: row.value,
label: row.text
};
},
getFormatter(field) {
if (field.format) {
return eval(field.format);
}
return v => v;
},
fetchAjaxOptions(query = {}) {
const params = this.field.ajaxOptions;
const { url, resource, where = {}, text, depends } = params;
params.where = Object.assign({}, where, query);
if (this.q) {
params.q = this.q;
}
const apiUrl = url
? _.template(url)({ item: this.parent })
: resource + "/options";
this.$http.get(apiUrl, { params }).then(({ data }) => {
this.options = data;
});
}
},
mounted() {
if (this.field.type == "html") {
// window.onscroll = () => {
// const editor = this.$refs.editor.$el
// const offsetTop = editor.getClientRects()[0].top
// // const scrollTop = document.documentElement.scrollTop
// if (offsetTop <= 0) {
// editor.classList.toggle()
// }
// // global.console.log(scrollTop, offsetTop)
// }
}
},
created() {
if (this.field.type == "html") {
this.initEditor();
}
if (this.field.ajaxOptions && this.field.ajaxOptions.search !== true) {
this.fetchAjaxOptions();
}
if (this.isSelect2) {
// this.initOptionsForSelect2();
// this.$watch("options", () => {
// });
}
}
};
</script>

View File

@@ -0,0 +1,93 @@
<template>
<b-dropdown split variant="primary" no-caret1 class="selectx">
<template slot="button-content">
<span>
{{model}}
<!-- <b-btn-close></b-btn-close> -->
</span>
<!-- <b-form-input class="border-0 w-auto d-inline-block" v-model="q"></b-form-input> -->
</template>
<b-dropdown-item-button
@click="choose(item)"
v-for="item in options"
:key="item.value"
:active="getIsActive(item)"
>{{item.text}}</b-dropdown-item-button>
<!-- <b-dropdown-item>Second Action</b-dropdown-item>
<b-dropdown-item>Third Action</b-dropdown-item>
<b-dropdown-divider></b-dropdown-divider>
<b-dropdown-item active>Active action</b-dropdown-item>
<b-dropdown-item disabled>Disabled action</b-dropdown-item>-->
</b-dropdown>
</template>
<script>
export default {
props: {
value: {},
options: {},
multiple: { type: Boolean }
},
data() {
return {
q: ""
};
},
methods: {
getIsActive(item) {
if (this.multiple) {
return (this.model || []).includes(item.value);
} else {
return this.model === item.value;
}
},
choose(item) {
const val = item.value;
if (this.multiple) {
let values = this.model.slice(0);
const i = values.findIndex(val);
// console.log(i);
if (i) {
values.splice(i, 1);
} else {
values.push(val);
}
return this.$emit("input", values);
}
this.$emit("input", val);
}
},
computed: {
values() {},
model: {
get() {
if (this.multiple && !this.value) {
return [];
}
const selected = this.options.find(v => v.value === this.value)
return selected ? selected.text : null;
},
set(val) {
if (this.multiple) {
let values = this.model.slice(0);
const i = values.findIndex(val);
if (i) {
values.splice(i, 1);
} else {
values.push(val);
}
return this.$emit("input", values);
}
this.$emit("input", val);
}
}
}
};
</script>
<style>
.selectx{
/* min-width:15em; */
/* text-align: left; */
}
</style>

View File

@@ -0,0 +1,123 @@
<template>
<v-select
v-model="model"
@search="getAjaxOptions"
label="text"
:options="newOptions"
:multiple="multiple"
>
<div slot="no-options">{{noOptionsText}}</div>
</v-select>
</template>
<script>
import VSelect from "vue-select";
export default {
components: {
VSelect
},
data() {
return {
newOptions: this.options.slice(0),
q: ""
};
},
props: {
value: {
deafult() {
return this.multiple ? [] : null;
}
},
options: {
default: () => []
},
name: {},
multiple: {},
field: {},
parent: {},
ajaxOptions: {},
noOptionsText: {
default() {
return this.$t("messages.no_options_text");
}
}
},
computed: {
model: {
get() {
if (this.multiple) {
return this.newOptions.filter(v => this.value.includes(v.value));
} else {
return this.newOptions.find(v => this.value === v.value);
}
},
set(val) {
if (val === null || val.value === null) {
return this.$emit("input", null);
}
let ret;
if (this.multiple) {
const isChanged = JSON.stringify(val) !== JSON.stringify(this.value);
if (isChanged) {
ret = val.map(v => v.value);
}
} else {
ret = val.value;
}
if (ret !== null) {
this.$emit("input", ret);
}
}
}
},
methods: {
getAjaxOptions(q) {
this.q = q;
this.fetchOptions();
},
initOptionsForSelect2() {
const parentOptions = this.parent[this.name + "_data"];
if (parentOptions) {
this.newOptions = this.options.concat(parentOptions);
}
},
fetchOptions(query = {}) {
const params = this.ajaxOptions;
const { url, resource, where = {}, text, depends } = params;
params.where = Object.assign({}, where, query);
if (this.q) {
params.q = this.q;
}
const apiUrl = url
? _.template(url)({ item: this.parent })
: resource + "/options";
this.$http.get(apiUrl, { params }).then(({ data }) => {
this.model = { value: null };
this.newOptions = data;
});
}
},
mounted() {
if (this.ajaxOptions) {
const { url, resource, where, depends } = this.ajaxOptions;
if (depends) {
this.$watch(
`parent.${depends}`,
val => {
// console.log(val);
this.fetchOptions({ [depends]: val });
},
{
// deep: true,
immediate: true
}
);
} else {
this.fetchOptions();
}
}
}
};
</script>

View File

@@ -0,0 +1,74 @@
<template>
<div>
<div class="preview">
<div class="thumbnail" v-if="field.multiple">
<b-draggable v-model="model" @input="update" class="row">
<b-form-uploader-item class="col-4" :name="name" :id="`${id}_${k}`" :key="k"
v-for="(v, k) in model" :field="field" :parent="parent" v-model="model[k]"
@input="update"
@remove="model.splice(k, 1) ; update()"
@add="model.splice(k + 1,0, null) ; update()" allow-add />
</b-draggable>
</div>
<div v-else>
<div class="row">
<b-form-uploader-item class="col-12" :id="id" v-model="model" @open-file-browser="openFileBrowser(name)"
@input="update" :name="name" :field="field" :parent="parent" @remove="model = null; update()" />
</div>
</div>
</div>
</div>
</template>
<script>
import BDraggable from "vuedraggable";
import BFormUploaderItem from "./FormUploaderItem";
import types from "../store/types";
export default {
components: {
BDraggable,
BFormUploaderItem
},
props: {
value: {},
field: {},
id: {},
parent: {},
name: {}
},
data() {
return {
model: this.value || [],
file: null,
current: this.value
};
},
watch: {
value(val) {
this.model = val;
}
},
computed: {
tag() {
return this.field.type == "image" ? "img" : this.field.type;
}
},
methods: {
openFileBrowser(name) {
this.$store.commit(types.SHOW_FILE_BROWSER, { name });
},
update() {
this.$emit("input", this.model);
}
}
};
</script>
<style>
.current {
border: 1px solid #c30;
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<div>
<b-card>
<b-data-value class="text-center" :field="field" :name="name" :model="parent"/>
<div
class="text-center text-muted mt-2"
v-if="field.limit && field.limit.width"
>{{$t('messages.image_size', field.limit)}}</div>
<div slot="footer" class="text-center">
<b-form-file
ref="file"
:id="fileName"
:name="name"
v-model="file"
v-bind="field"
@input="upload"
class="d-none"
:multiple="false"
/>
<b-form-input
:value="String(file || '')"
@input="$emit('input', arguments[0])"
v-if="field.showInput"
class="mb-2"
></b-form-input>
<b-btn
v-if="field.showBrowse"
@click="$emit('open-file-browser')"
class="mr-2"
size="sm"
>{{$t('actions.file_browser')}}</b-btn>
<label
:for="`file_${id}`"
class="btn btn-primary m-0 mr-2 btn-sm"
>{{file ? $t('actions.change') : $t('actions.choose')}}</label>
<b-btn @click="$emit('remove')" class="mr-2" size="sm">{{$t('actions.delete')}}</b-btn>
<b-btn @click="$emit('add')" v-if="allowAdd" class="mr-2" size="sm">{{$t('actions.add')}}</b-btn>
<b-btn
v-if="field.showCopy"
ref="copy_btn"
:data-clipboard-text="value"
class="mr-2"
size="sm"
>{{$t('actions.copy')}}</b-btn>
</div>
</b-card>
</div>
</template>
<script>
// import ClipboardJS from 'clipboard'
import BDataValue from "./DataValue";
import Vue from "vue";
export default {
components: {
BDataValue
},
props: {
value: {},
field: {},
id: {},
parent: {},
name: {},
allowAdd: {
type: Boolean,
default: false
}
},
data() {
return {
model: this.value,
file: this.value,
current: this.value,
fileData: {}
};
},
watch: {
value(val) {
this.file = val;
}
},
computed: {
text() {
return String(this.value)
.split("/")
.pop();
},
modalName() {
return `modal_${this.id}`;
},
fileName() {
return `file_${this.id}`;
},
tag() {
const tags = {
image: "img",
audio: "audio",
video: "video"
};
return tags[this.field.type] ? tags[this.field.type] : "div";
}
},
mounted() {
Vue.nextTick(() => {
// new ClipboardJS(this.$refs.copy_btn)
});
},
methods: {
reset(error) {
if (error) {
this.$snotify.error(error);
}
this.model = this.oldValue;
return false;
},
upload() {
if (!this.file) {
return;
}
const fd = new FormData();
fd.append("type", this.name);
fd.append("file", this.file);
this.fileData = this.file;
const src = URL.createObjectURL(this.file);
const doUpload = () => {
this.$http.post("upload", fd).then(({ data }) => {
this.file = data.url;
this.$emit("input", this.file);
this.$snotify.success(this.$t("messages.uploaded"));
});
};
const { width, height, size } = this.field.limit || {};
if (this.file.size > size) {
return this.reset(
this.$t("errors.too_large", { limit: parseInt(size / 1024) })
);
}
if (this.file.type.match(/^image/)) {
let file = new Image();
file.src = src;
file.onload = () => {
if (this.field.limit) {
if (file.naturalHeight != height || file.naturalWidth != width) {
return this.reset(
this.$t("errors.wrong_size", { width, height })
);
}
}
doUpload();
};
} else {
doUpload();
}
}
}
};
</script>
<style scoped>
.current {
border: 1px solid #c30;
}
.item {
max-width: 100%;
max-height: 200px;
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<header class="app-header navbar">
<button class="navbar-toggler mobile-sidebar-toggler d-lg-none" type="button" @click="mobileSidebarToggle">
<span class="navbar-toggler-icon"></span>
</button>
<b-link class="navbar-brand" to="/">
</b-link>
<button class="navbar-toggler sidebar-toggler d-md-down-none" type="button" @click="sidebarToggle">
<span class="navbar-toggler-icon"></span>
</button>
<b-navbar-nav is-nav-bar class="d-md-down-none">
<b-nav-item class="px-3">Dashboard</b-nav-item>
<b-nav-item class="px-3">Users</b-nav-item>
<b-nav-item class="px-3">Settings</b-nav-item>
</b-navbar-nav>
<b-navbar-nav is-nav-bar class="ml-auto">
<b-nav-item class="d-md-down-none">
<i class="icon-bell"></i>
<b-badge pill variant="danger">5</b-badge>
</b-nav-item>
<b-nav-item class="d-md-down-none">
<i class="icon-list"></i>
</b-nav-item>
<b-nav-item class="d-md-down-none">
<i class="icon-location-pin"></i>
</b-nav-item>
<b-nav-item-dropdown right no-caret>
<template slot="button-content">
<img src="static/img/avatars/6.jpg" class="img-avatar" alt="admin@bootstrapmaster.com">
</template>
<b-dropdown-header tag="div" class="text-center"><strong>Account</strong></b-dropdown-header>
<b-dropdown-item><i class="fa fa-bell-o"></i> Updates<b-badge variant="info">{{itemsCount}}</b-badge></b-dropdown-item>
<b-dropdown-item><i class="fa fa-envelope-o"></i> Messages<b-badge variant="success">{{itemsCount}}</b-badge></b-dropdown-item>
<b-dropdown-item><i class="fa fa-tasks"></i> Tasks<b-badge variant="danger">{{itemsCount}}</b-badge></b-dropdown-item>
<b-dropdown-item><i class="fa fa-comments"></i> Comments<b-badge variant="warning">{{itemsCount}}</b-badge></b-dropdown-item>
<b-dropdown-header tag="div" class="text-center"><strong>Settings</strong></b-dropdown-header>
<b-dropdown-item><i class="fa fa-user"></i> Profile</b-dropdown-item>
<b-dropdown-item><i class="fa fa-wrench"></i> Settings</b-dropdown-item>
<b-dropdown-item><i class="fa fa-usd"></i> Payments<b-badge variant="default">{{itemsCount}}</b-badge></b-dropdown-item>
<b-dropdown-item><i class="fa fa-file"></i> Projects<b-badge variant="primary">{{itemsCount}}</b-badge></b-dropdown-item>
<b-dropdown-divider></b-dropdown-divider>
<b-dropdown-item><i class="fa fa-shield"></i> Lock Account</b-dropdown-item>
<b-dropdown-item><i class="fa fa-lock"></i> Logout</b-dropdown-item>
</b-nav-item-dropdown>
</b-navbar-nav>
<button class="navbar-toggler aside-menu-toggler d-md-down-none" type="button" @click="asideToggle">
<span class="navbar-toggler-icon"></span>
</button>
</header>
</template>
<script>
export default {
name: 'header',
components: {
},
data(){
return {
itemsCount: 3
}
},
methods: {
sidebarToggle (e) {
e.preventDefault()
document.body.classList.toggle('sidebar-hidden')
},
sidebarMinimize (e) {
e.preventDefault()
document.body.classList.toggle('sidebar-minimized')
},
mobileSidebarToggle (e) {
e.preventDefault()
document.body.classList.toggle('sidebar-mobile-show')
},
asideToggle (e) {
e.preventDefault()
document.body.classList.toggle('aside-menu-hidden')
}
}
}
</script>

View File

@@ -0,0 +1,30 @@
<template>
<div ref="editor"></div>
</template>
<script>
import 'jsoneditor/dist/jsoneditor.min.css'
import JSONEditor from "jsoneditor";
export default {
name: "b-json-editor",
props: {
value: null,
options: {}
},
data() {
return {
editor: null
};
},
mounted() {
this.editor = new JSONEditor(this.$refs.editor, this.options);
if (typeof this.value === "string") {
this.value = JSON.parse(this.value);
}
this.editor.set(this.value || {});
this.editor.onChange = v => {
this.$emit("input", JSON.stringify(v));
};
},
created() {}
};
</script>

View File

@@ -0,0 +1,32 @@
<template>
<div class="py-2" v-if="site.locale_switcher">
<b-button
size="sm"
class="mr-1"
@click="changeLocale(lang)"
:key="lang"
v-for="lang in Object.keys($i18n.messages)"
>{{lang | ucfirst}}</b-button>
</div>
</template>
<script>
import types from "../store/types";
import { mapState } from "vuex";
export default {
computed: {
...mapState(["site"])
},
filters: {
ucfirst(val) {
return String(val)[0].toUpperCase() + String(val).slice(1);
}
},
methods: {
changeLocale(locale) {
this.$store.commit(types.SET_LOCALE, locale);
}
}
};
</script>

View File

@@ -0,0 +1,457 @@
<template>
<header class="app-header navbar">
<button display="md" mobile="true" type="button" class="d-lg-none navbar-toggler">
<span class="navbar-toggler-icon"></span>
</button>
<a href="#/dashboard#" class="navbar-brand" target="_self">
<img
src="img/brand/logo.svg"
width="89"
height="25"
alt="CoreUI Logo"
class="navbar-brand-full"
>
<img
src="img/brand/sygnet.svg"
width="30"
height="30"
alt="CoreUI Logo"
class="navbar-brand-minimized"
>
</a>
<button display="lg" type="button" class="d-md-down-none navbar-toggler">
<span class="navbar-toggler-icon"></span>
</button>
<ul class="navbar-nav d-md-down-none">
<li class="nav-item px-3">
<a href="#/dashboard" class="nav-link active" target="_self">Dashboard</a>
</li>
<li class="nav-item px-3">
<a href="#/users" class="nav-link" target="_self">Users</a>
</li>
<li class="nav-item px-3">
<a href="#" target="_self" class="nav-link">Settings</a>
</li>
</ul>
<ul class="navbar-nav ml-auto">
<li class="nav-item d-md-down-none">
<a href="#" target="_self" class="nav-link">
<li id="__BVID__11" class="nav-item b-nav-dropdown dropdown">
<a
href="#"
id="__BVID__11__BV_button_"
aria-haspopup="true"
aria-expanded="false"
class="nav-link"
>
<i class="icon-bell"></i>
<span class="badge badge-danger badge-pill">5</span>
</a>
<div aria-labelledby="__BVID__11__BV_button_" class="dropdown-menu dropdown-menu-right">
<div class="dropdown-header dropdown-menu-lg text-center">
<strong>You have 5 notifications</strong>
</div>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="icon-user-follow text-success"></i> New user registered
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="icon-user-unfollow text-danger"></i> User deleted
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="icon-chart text-info"></i> Sales report is ready
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="icon-basket-loaded text-primary"></i> New client
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="icon-speedometer text-warning"></i> Server overloaded
</a>
<div class="dropdown-header text-center">
<strong>Server</strong>
</div>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<div class="text-uppercase mb-1">
<small>
<b>CPU Usage</b>
</small>
</div>
<div class="progress-xs progress">
<div
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="25"
class="progress-bar bg-info"
style="width: 25%;"
>
<!---->
</div>
</div>
<small class="text-muted">348 Processes. 1/4 Cores.</small>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<div class="text-uppercase mb-1">
<small>
<b>Memory Usage</b>
</small>
</div>
<div class="progress-xs progress">
<div
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="70"
class="progress-bar bg-warning"
style="width: 70%;"
>
<!---->
</div>
</div>
<small class="text-muted">11444GB/16384MB</small>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<div class="text-uppercase mb-1">
<small>
<b>SSD 1 Usage</b>
</small>
</div>
<div class="progress-xs progress">
<div
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="90"
class="progress-bar bg-danger"
style="width: 90%;"
>
<!---->
</div>
</div>
<small class="text-muted">243GB/256GB</small>
</a>
</div>
</li>
</a>
</li>
<li class="nav-item d-md-down-none">
<a href="#" target="_self" class="nav-link">
<li id="__BVID__20" class="nav-item b-nav-dropdown dropdown">
<a
href="#"
id="__BVID__20__BV_button_"
aria-haspopup="true"
aria-expanded="false"
class="nav-link"
>
<i class="icon-list"></i>
<span class="badge badge-warning badge-pill">15</span>
</a>
<div aria-labelledby="__BVID__20__BV_button_" class="dropdown-menu dropdown-menu-right">
<div class="dropdown-header dropdown-menu-lg text-center">
<strong>You have 15 pending tasks</strong>
</div>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<div class="small mb-1">Upgrade NPM &amp; Bower
<span class="float-right">
<strong>0%</strong>
</span>
</div>
<div class="progress-xs progress">
<div
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="0"
class="progress-bar bg-info"
style="width: 0%;"
>
<!---->
</div>
</div>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<div class="small mb-1">ReactJS Version
<span class="float-right">
<strong>25%</strong>
</span>
</div>
<div class="progress-xs progress">
<div
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="25"
class="progress-bar bg-danger"
style="width: 25%;"
>
<!---->
</div>
</div>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<div class="small mb-1">VueJS Version
<span class="float-right">
<strong>50%</strong>
</span>
</div>
<div class="progress-xs progress">
<div
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="50"
class="progress-bar bg-warning"
style="width: 50%;"
>
<!---->
</div>
</div>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<div class="small mb-1">Add new layouts
<span class="float-right">
<strong>75%</strong>
</span>
</div>
<div class="progress-xs progress">
<div
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="75"
class="progress-bar bg-info"
style="width: 75%;"
>
<!---->
</div>
</div>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<div class="small mb-1">Angular 2 Cli Version
<span class="float-right">
<strong>100%</strong>
</span>
</div>
<div class="progress-xs progress">
<div
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="100"
class="progress-bar bg-success"
style="width: 100%;"
>
<!---->
</div>
</div>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item text-center">
<strong>View all tasks</strong>
</a>
</div>
</li>
</a>
</li>
<li class="nav-item d-md-down-none">
<a href="#" target="_self" class="nav-link">
<li id="__BVID__33" class="nav-item b-nav-dropdown dropdown">
<a
href="#"
id="__BVID__33__BV_button_"
aria-haspopup="true"
aria-expanded="false"
class="nav-link"
>
<i class="icon-envelope-letter"></i>
<span class="badge badge-info badge-pill">7</span>
</a>
<div aria-labelledby="__BVID__33__BV_button_" class="dropdown-menu dropdown-menu-right">
<div class="dropdown-menu-lg">
<div class="dropdown-header text-center">
<strong>You have 7 messages</strong>
</div>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<div class="message">
<div class="pt-3 mr-3 float-left">
<div class="avatar">
<img
src="img/avatars/7.jpg"
alt="admin@bootstrapmaster.com"
class="img-avatar"
>
<span class="avatar-status bg-success"></span>
</div>
</div>
<div>
<small class="text-muted">John Doe</small>
<small class="text-muted float-right mt-1">Just now</small>
</div>
<div class="text-truncate font-weight-bold">
<span class="fa fa-exclamation text-danger"></span> Important message
</div>
<div
class="small text-muted text-truncate"
>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt...</div>
</div>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<div class="message">
<div class="pt-3 mr-3 float-left">
<div class="avatar">
<img
src="img/avatars/6.jpg"
alt="admin@bootstrapmaster.com"
class="img-avatar"
>
<span class="avatar-status bg-warning"></span>
</div>
</div>
<div>
<small class="text-muted">Jane Doe</small>
<small class="text-muted float-right mt-1">5 minutes ago</small>
</div>
<div class="text-truncate font-weight-bold">Lorem ipsum dolor sit amet</div>
<div
class="small text-muted text-truncate"
>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt...</div>
</div>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<div class="message">
<div class="pt-3 mr-3 float-left">
<div class="avatar">
<img
src="img/avatars/5.jpg"
alt="admin@bootstrapmaster.com"
class="img-avatar"
>
<span class="avatar-status bg-danger"></span>
</div>
</div>
<div>
<small class="text-muted">Janet Doe</small>
<small class="text-muted float-right mt-1">1:52 PM</small>
</div>
<div class="text-truncate font-weight-bold">Lorem ipsum dolor sit amet</div>
<div
class="small text-muted text-truncate"
>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt...</div>
</div>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<div class="message">
<div class="pt-3 mr-3 float-left">
<div class="avatar">
<img
src="img/avatars/4.jpg"
alt="admin@bootstrapmaster.com"
class="img-avatar"
>
<span class="avatar-status bg-info"></span>
</div>
</div>
<div>
<small class="text-muted">Joe Doe</small>
<small class="text-muted float-right mt-1">4:03 AM</small>
</div>
<div class="text-truncate font-weight-bold">Lorem ipsum dolor sit amet</div>
<div
class="small text-muted text-truncate"
>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt...</div>
</div>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item text-center">
<strong>View all messages</strong>
</a>
</div>
</div>
</li>
</a>
</li>
<li class="nav-item d-md-down-none">
<a href="#" target="_self" class="nav-link">
<li id="__BVID__36" class="nav-item b-nav-dropdown dropdown">
<a
href="#"
id="__BVID__36__BV_button_"
aria-haspopup="true"
aria-expanded="false"
class="nav-link"
>
<i class="icon-location-pin"></i>
</a>
<div aria-labelledby="__BVID__36__BV_button_" class="dropdown-menu dropdown-menu-right">
<div>
<h6 class="text-capitalize text-center">your dropdown</h6>
<ol>
<li>one</li>
<li>two</li>
<li>three</li>
</ol>
</div>
</div>
</li>
</a>
</li>
<li id="__BVID__39" class="nav-item b-nav-dropdown dropdown">
<a
href="#"
id="__BVID__39__BV_button_"
aria-haspopup="true"
aria-expanded="false"
class="nav-link"
>
<img src="img/avatars/6.jpg" alt="admin@bootstrapmaster.com" class="img-avatar">
</a>
<div aria-labelledby="__BVID__39__BV_button_" class="dropdown-menu dropdown-menu-right">
<div class="dropdown-header text-center">
<strong>Account</strong>
</div>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="fa fa-bell-o"></i> Updates
<span class="badge badge-info">42</span>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="fa fa-envelope-o"></i> Messages
<span class="badge badge-success">42</span>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="fa fa-tasks"></i> Tasks
<span class="badge badge-danger">42</span>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="fa fa-comments"></i> Comments
<span class="badge badge-warning">42</span>
</a>
<div class="dropdown-header text-center">
<strong>Settings</strong>
</div>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="fa fa-user"></i> Profile
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="fa fa-wrench"></i> Settings
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="fa fa-usd"></i> Payments
<span class="badge badge-secondary">42</span>
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="fa fa-file"></i> Projects
<span class="badge badge-primary">42</span>
</a>
<div role="separator" class="dropdown-divider"></div>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="fa fa-shield"></i> Lock Account
</a>
<a role="menuitem" href="#" target="_self" class="dropdown-item">
<i class="fa fa-lock"></i> Logout
</a>
</div>
</li>
</ul>
<button display type="button" class="d-none d-lg-block navbar-toggler">
<span class="navbar-toggler-icon"></span>
</button>
</header>
</template>

View File

@@ -0,0 +1,97 @@
<template>
<b-aside class="sidebar position-fixed">
<div class="text-center top">
<a :href="site.url" target="_blank" v-if="site.logo">
<img class="site-logo img-fluid" :src="site.logo">
</a>
<!-- <b-img class="site-logo" :src="require('../assets/img/gengyi-logo.svg')" fluid style="border-radius: 5px;" /> -->
<!-- <b-img rounded="circle" :src="auth.user.avatar" height="70" blank-color="#777" alt="avatar" class="m-2" /> -->
<div class="my-3" v-if="site.sidebar_userinfo !== false">
<h5 style="letter-spacing:2px">{{site.name}}</h5>
<template v-if="auth.user">
<b-badge class="text-uppercase mr-1" v-if="auth.user.badge">{{auth.user.badge}}</b-badge>
<span>{{auth.user.username}}</span>
</template>
</div>
<div v-else></div>
<!-- <locale-switcher></locale-switcher> -->
<!-- <theme-switcher></theme-switcher> -->
</div>
<el-menu router>
<template v-for="(item, index) in site.menu" >
<el-submenu :index="item.url" :key="index" v-if="item.children">
<template slot="title">
<i :class="item.icon"></i>
<span>{{item.name}}</span>
</template>
</el-submenu>
<el-menu-item :index="item.url" :key="index" v-else>
<i :class="item.icon"></i>
<span class="">{{item.name}}</span>
</el-menu-item>
</template>
</el-menu>
</b-aside>
</template>
<script>
import ThemeSwitcher from "./ThemeSwitcher";
import LocaleSwitcher from "./LocaleSwitcher";
import { mapState } from "vuex";
export default {
name: "sidebar",
computed: {
...mapState(["auth", "site"])
},
components: { LocaleSwitcher, ThemeSwitcher },
methods: {
toggle(item) {
this.$set(item, "open", !item.open);
}
}
};
</script>
<style lang="scss">
// .sidebar {
// z-index: 999;
// box-shadow: 1px 0 20px rgba(0, 0, 0, 0.1);
// width: 200px;
// height: 100vh;
// overflow: auto;
// letter-spacing: 1px;
// padding: 1rem;
// .site-logo {
// // border-radius: 1rem;
// // min-height:3em;
// }
// .nav-link {
// color: #666;
// display: flex;
// align-items: center;
// padding: 0.7rem 1rem;
// // border-radius: 2rem;
// // font-weight: 400;
// &:hover,
// &.active {
// // color: #333;
// // background: #eee;
// }
// i {
// margin-right: 1rem;
// }
// }
// .nav-title {
// font-size: 0.7rem;
// font-weight: bold;
// text-transform: uppercase;
// color: #ced4da;
// padding: 1rem 1rem 0.5rem 0;
// }
// }
</style>

View File

@@ -0,0 +1,107 @@
<template>
<div class="pt-3 pl-3">
<div class="text-center top">
<a :href="site.url" target="_blank" v-if="site.logo" v-show="!collapsed">
<b-img class="site-logo" :src="site.logo" fluid/>
</a>
<!-- <b-img class="site-logo" :src="require('../assets/img/gengyi-logo.svg')" fluid style="border-radius: 5px;" /> -->
<!-- <b-img rounded="circle" :src="auth.user.avatar" height="70" blank-color="#777" alt="avatar" class="m-2" /> -->
<div class="my-3" v-if="site.sidebar_userinfo !== false" v-show="!collapsed">
<h5 style="letter-spacing:2px">{{site.name}}</h5>
<template v-if="auth.user">
<b-badge class="text-uppercase mr-1" v-if="auth.user.badge">{{auth.user.badge}}</b-badge>
<span>{{auth.user.username}}</span>
</template>
</div>
<div v-else></div>
<locale-switcher v-show="!collapsed"></locale-switcher>
<theme-switcher v-show="!collapsed"></theme-switcher>
</div>
<div slot="header"></div>
<b-nav pills class="sidebar-nav" vertical>
<template v-for="(item, index) in site.menu">
<b-nav-text v-if="item.title" :key="index">
<small class="text-muted">
<b>{{item.name}}</b>
</small>
</b-nav-text>
<b-nav vertical v-else-if="item.children" :key="index">
<b-nav-item :to="child.url" :key="child.name" v-for="child in item.children">
<i class="mr-1" :class="child.icon"></i>
<span>{{child.name}}</span>
<b-badge v-bind="child.badge" v-if="child.badge">{{child.badge.text}}</b-badge>
</b-nav-item>
</b-nav>
<b-nav-item :active="$route.path === item.url" :to="item.url" v-else :key="index">
<i :class="{'mr-2': !collapsed, [item.icon]: true}"></i>
<span v-show="!collapsed">{{item.name}}</span>
<b-badge v-bind="item.badge" v-if="item.badge">{{item.badge.text}}</b-badge>
</b-nav-item>
</template>
<slot></slot>
</b-nav>
<p></p>
</div>
</template>
<script>
import ThemeSwitcher from "./ThemeSwitcher";
import LocaleSwitcher from "./LocaleSwitcher";
import { mapState } from "vuex";
export default {
name: "sidebar",
props: {
collapsed: {
type: Boolean,
default: false
}
},
data() {
return {};
},
computed: {
...mapState(["auth", "site"]),
menu() {
const menu = [];
const titleIndices = [];
this.site.menu.forEach((v, k) => {
v.title && titleIndices.push(parseInt(k));
});
for (let i in titleIndices) {
menu.push({
name: this.site.menu[titleIndices[i]].name,
children: this.site.menu.slice(
titleIndices[i] + 1,
titleIndices[parseInt(i) + 1]
)
});
}
return menu;
}
},
components: { LocaleSwitcher, ThemeSwitcher },
methods: {
toggle(item) {
this.$set(item, "open", !item.open);
}
}
};
</script>
<style lang="scss">
.site-logo {
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
}
.sidebar-nav {
.nav-item a {
color: #666;
padding: 0.7rem 1rem;
}
}
.sidebar-nav .nav-item:hover {
background: #f6f6f6;
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<label :class="classList">
<input type="checkbox"
class="switch-input"
:value="value"
:checked="isChecked"
@change="handleChange">
<template v-if="isOn">
<span class="switch-label" :data-on="on" :data-off="off"></span>
</template>
<template v-else>
<span class="switch-label"></span>
</template>
<span class="switch-handle"></span>
</label>
</template>
<script>
export default {
model: {
prop: 'checked',
event: 'change'
},
props: {
value: {
default: true
},
uncheckedValue: {
default: false
},
checked: {
default: false
},
type: {
type: String,
default: 'default'
},
variant: {
type: String,
default: ''
},
pill: {
type: Boolean,
default: false
},
on: {
type: String,
default: null
},
off: {
type: String,
default: null
},
size: {
type: String,
default: null
}
},
computed: {
classList () {
return [
'switch',
this.switchType,
this.switchVariant,
this.switchPill,
this.switchSize
]
},
switchType () {
return this.type ? `switch-${this.type}` : `switch-default`
},
switchVariant () {
return this.variant ? `switch-${this.variant}` : `switch-secondary`
},
switchPill () {
return !this.pill ? null : `switch-pill`
},
switchSize () {
return this.size ? `switch-${this.size}` : ''
},
isChecked () {
return this.checked === this.value
},
isOn () {
return !this.on ? null : true
}
},
methods: {
handleChange ({ target: { checked } }) {
this.$emit('change', checked ? this.value : this.uncheckedValue)
}
}
}
</script>

View File

@@ -0,0 +1,32 @@
<template>
<b-table class="table-builder" :fields="fields" :items="items">
<template v-for="(field, key) in _.omit(fields, '_actions')" :slot="`HEAD_${key}`">
<div :key="key" :class="{'text-right': ['number'].includes(field.type)}">
{{field.label || key}}
</div>
</template>
<template v-for="(field, key) in fields" :slot="key" slot-scope="row">
<b-data-value :field="field" :key="key" :lang="currentLanguage" :name="key" :model="row.item" short-id />
</template>
</b-table>
</template>
<script>
import BDataValue from "./DataValue";
import { mapState, mapGetters } from "vuex";
export default {
components: {
BDataValue
},
props: {
fields: {},
items: Array
},
computed: {
...mapState(["site", "i18n"]),
...mapGetters(["currentMenu", "currentLanguage"]),
}
}
</script>

View File

@@ -0,0 +1,35 @@
<template>
<div class="m-4" v-if="$store.state.site.theme_switcher">
<b-select :options="themeOptions" v-model="theme"></b-select>
</div>
</template>
<script>
import types from "../store/types.js";
export default {
computed: {
theme: {
get() {
return this.$store.state.site.theme;
},
set(val) {
this.$store.commit(types.SET_THEME, val);
}
}
},
data() {
return {
themeOptions: `
cerulean darkly litera materia sandstone slate superhero
cosmo flatly lumen minty simplex solar united
cyborg journal lux pulse sketchy spacelab yeti
`.split(/\s+/)
};
},
created() {
this.theme = this.$storage.get("theme") || 'cosmo';
}
};
</script>