'init'
This commit is contained in:
61
src/layout/components/AppMain.vue
Normal file
61
src/layout/components/AppMain.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<keep-alive :include="cachedViews">
|
||||
<router-view v-if="!$route.meta.link" :key="key" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
<iframe-toggle />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import iframeToggle from "./IframeToggle/index"
|
||||
|
||||
export default {
|
||||
name: 'AppMain',
|
||||
components: { iframeToggle },
|
||||
computed: {
|
||||
cachedViews() {
|
||||
return this.$store.state.tagsView.cachedViews
|
||||
},
|
||||
key() {
|
||||
return this.$route.path
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-main {
|
||||
/* 50= navbar 50 */
|
||||
min-height: calc(100vh - 56px);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 56px;
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 56 + 34 */
|
||||
min-height: calc(100vh - 128px);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 90px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
// fix css style bug in open el-dialog
|
||||
.el-popup-parent--hidden {
|
||||
.fixed-header {
|
||||
padding-right: 17px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
767
src/layout/components/DialogSimple.vue
Normal file
767
src/layout/components/DialogSimple.vue
Normal file
@@ -0,0 +1,767 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
class="dialog-simple"
|
||||
:visible="dialogVisible"
|
||||
:title="title"
|
||||
:width="configs.dialogWidth ?? '50%'"
|
||||
v-dialogDrag
|
||||
append-to-body>
|
||||
<!-- @close="handleClose"
|
||||
:destroy-on-close="false"
|
||||
:close-on-click-modal="configs.clickModalToClose ?? true" -->
|
||||
|
||||
<!-- form -->
|
||||
<el-form ref="dataForm" :model="dataForm" v-loading="loadingStatus">
|
||||
<el-row v-for="(row, rowIndex) in configs.form.rows" :key="'row_' + rowIndex" :gutter="20">
|
||||
<el-col v-for="(col, colIndex) in row" :key="colIndex" :span="24 / row.length">
|
||||
<!-- 通过多个 col === null 可以控制更灵活的 span 大小 -->
|
||||
<el-form-item v-if="col !== null" :label="col.label" :prop="col.prop" :rules="col.rules || null">
|
||||
<el-input
|
||||
v-if="col.input"
|
||||
v-model="dataForm[col.prop]"
|
||||
clearable
|
||||
:disabled="IsNotAvaliable(col)"
|
||||
v-bind="col.elparams" />
|
||||
<el-input
|
||||
v-if="col.forceDisabled && col.eraseOnSubmit"
|
||||
v-model="shadowDataForm[col.prop]"
|
||||
disabled
|
||||
v-bind="col.elparams" />
|
||||
<el-input
|
||||
v-if="col.forceDisabled && !col.eraseOnSubmit"
|
||||
v-model="dataForm[col.prop]"
|
||||
disabled
|
||||
v-bind="col.elparams" />
|
||||
<el-button
|
||||
type=""
|
||||
plain
|
||||
v-if="col.button"
|
||||
v-bind="col.elparams"
|
||||
style="width: 100%"
|
||||
@click="handleButtonClick(col)">
|
||||
{{ col.label }}
|
||||
</el-button>
|
||||
<el-cascader
|
||||
v-if="col.cascader"
|
||||
v-model="dataForm[col.prop]"
|
||||
:options="col.options"
|
||||
:disabled="IsNotAvaliable(col)"
|
||||
v-bind="col.elparams"></el-cascader>
|
||||
<el-select
|
||||
v-if="col.forceDisabledSelect"
|
||||
disabled
|
||||
v-model="dataForm[col.prop]"
|
||||
clearable
|
||||
v-bind="col.elparams">
|
||||
<el-option
|
||||
v-for="(opt, optIdx) in col.options"
|
||||
:key="'option_' + optIdx"
|
||||
:label="opt.label"
|
||||
:value="opt.value" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-if="col.select"
|
||||
v-model="dataForm[col.prop]"
|
||||
clearable
|
||||
:disabled="IsNotAvaliable(col)"
|
||||
v-bind="col.elparams"
|
||||
@change="handleSelectChange(col, $event)">
|
||||
<el-option
|
||||
v-for="(opt, optIdx) in col.options"
|
||||
:key="'option_' + optIdx"
|
||||
:label="opt.label"
|
||||
:value="opt.value">
|
||||
<span>{{ opt.label }}</span>
|
||||
<span
|
||||
v-if="col.customLabel"
|
||||
style="display: inline-clock; margin-left: 12px; font-size: 0.9em; color: #ccce">
|
||||
{{ opt[col.customLabel] || "-" }}
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-switch
|
||||
v-if="col.switch"
|
||||
v-model="dataForm[col.prop]"
|
||||
:active-value="col.activeValue ?? 1"
|
||||
:inactive-value="col.activeValue ?? 0"
|
||||
@change="handleSwitchChange"
|
||||
:disabled="detailMode" />
|
||||
<el-input
|
||||
v-if="col.textarea"
|
||||
type="textarea"
|
||||
v-model="dataForm[col.prop]"
|
||||
:disabled="IsNotAvaliable(col)"
|
||||
v-bind="col.elparams" />
|
||||
<el-date-picker
|
||||
v-if="col.datetime"
|
||||
v-model="dataForm[col.prop]"
|
||||
:disabled="IsNotAvaliable(col)"
|
||||
v-bind="col.elparams" />
|
||||
<!-- <uploadBtn
|
||||
v-if="col.upload"
|
||||
:key="'upload_' + rowIndex + colIndex"
|
||||
:action="col.actionUrl"
|
||||
:file-list="dataForm['files']"
|
||||
:disabled="IsNotAvaliable(col)"
|
||||
v-bind="col.elparams"
|
||||
@update-file-list="handleFilelistUpdate" />
|
||||
|
||||
<quillEditor
|
||||
v-if="col.richInput"
|
||||
ref="quill-editor"
|
||||
v-model="dataForm[col.prop]"
|
||||
:options="col.quillConfig ?? defaultQuillConfig"
|
||||
style="margin-top: 42px"
|
||||
:disabled="IsNotAvaliable(col)" /> -->
|
||||
|
||||
<div class="" v-if="col.component" style="margin: 42px 0 0">
|
||||
<!-- 下面这个 component 几乎是为 富文本 quill 定制的了... TODO:后续可能会根据业务需求创建新的版本 -->
|
||||
<component
|
||||
:is="col.component"
|
||||
:key="'component_' + col.prop"
|
||||
@update:modelValue="handleComponentModelUpdate(col.prop, $event)"
|
||||
:modelValue="dataForm[col.prop]"
|
||||
:mode="detailMode ? 'detail' : dataForm.id ? 'edit' : 'create'" />
|
||||
</div>
|
||||
<!-- add more... -->
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<!-- footer -->
|
||||
<div slot="footer">
|
||||
<template v-for="(operate, index) in configs.form.operations">
|
||||
<el-button
|
||||
v-if="showButton(operate)"
|
||||
:key="'operation_' + index"
|
||||
:type="operate.type"
|
||||
@click="handleBtnClick(operate)">
|
||||
{{ operate.label }}
|
||||
</el-button>
|
||||
</template>
|
||||
<el-button @click="handleBtnClick({ name: 'cancel' })">取消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import { pick as __pick } from "@/utils/filters";
|
||||
// import Cookies from "js-cookie";
|
||||
// import uploadBtn from "@/components/uploadBtn";
|
||||
// import moment from "moment";
|
||||
|
||||
// import "quill/dist/quill.core.css";
|
||||
// import "quill/dist/quill.snow.css";
|
||||
// import "quill/dist/quill.bubble.css";
|
||||
|
||||
// import { quillEditor } from "vue-quill-editor";
|
||||
|
||||
export default {
|
||||
name: "DialogJustForm",
|
||||
// components: { uploadBtn, quillEditor },
|
||||
props: {
|
||||
configs: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
clickModalToClose: true,
|
||||
forms: null,
|
||||
}),
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: 'Title'
|
||||
},
|
||||
dialogVisible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// extraParams: {
|
||||
// type: Object,
|
||||
// default: () => ({})
|
||||
// }
|
||||
},
|
||||
data() {
|
||||
const dataForm = {};
|
||||
const shadowDataForm = {};
|
||||
const savedDatalist = {};
|
||||
const cachedList = {};
|
||||
const watchList = [];
|
||||
|
||||
this.configs.form.rows.forEach((row) => {
|
||||
row.forEach((col) => {
|
||||
if (!col.eraseOnSubmit && col.prop) dataForm[col.prop] = col.default ?? null;
|
||||
else shadowDataForm[col.prop] = col.default ?? null;
|
||||
|
||||
if ("autoUpdateProp" in col) {
|
||||
savedDatalist[col.prop] = [];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (this.configs.extraFields)
|
||||
this.configs.extraFields.forEach((cnf) => {
|
||||
dataForm[cnf.prop] = cnf.default ?? null;
|
||||
});
|
||||
|
||||
return {
|
||||
loadingStatus: false,
|
||||
dataForm,
|
||||
shadowDataForm,
|
||||
savedDatalist,
|
||||
cachedList,
|
||||
watchList,
|
||||
detailMode: false,
|
||||
editMode: false,
|
||||
baseDialogConfig: null,
|
||||
defaultQuillConfig: {
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ font: [] }],
|
||||
[{ size: ["small", false, "large", "huge"] }], // custom dropdown
|
||||
["bold", "italic", "underline", "strike"], // toggled buttons
|
||||
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
|
||||
["blockquote", "code-block"],
|
||||
[{ header: 1 }, { header: 2 }], // custom button values
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
// [{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript
|
||||
[{ indent: "-1" }, { indent: "+1" }], // outdent/indent
|
||||
// [{ 'direction': 'rtl' }], // text direction
|
||||
// [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
|
||||
// [{ 'align': [] }],
|
||||
// ['clean'] // remove formatting button
|
||||
],
|
||||
},
|
||||
theme: "snow",
|
||||
readOnly: false,
|
||||
placeholder: "在这里输入描述信息...",
|
||||
scrollingContainer: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
/** 临时保存所有发出的请求 */
|
||||
const promiseHistory = {};
|
||||
|
||||
const getData = (col, param) => {
|
||||
console.log("getData: ", col.prop, "/", param ?? "no param!");
|
||||
// 获取数据 - 不需要等待前置条件时
|
||||
promiseHistory[col.prop] = col.fetchData(param).then(({ data: res }) => {
|
||||
if (res.code === 0) {
|
||||
if ("list" in res.data) {
|
||||
console.log(
|
||||
"SdASD ",
|
||||
res.data.list.map((i) => ({
|
||||
label: col.optionLabel ? i[col.optionLabel] : i.name,
|
||||
value: col.optionValue ? i[col.optionValue] : i.id,
|
||||
[col.customLabel]: i[col.customLabel],
|
||||
}))
|
||||
);
|
||||
// 填充 options
|
||||
this.$set(
|
||||
col,
|
||||
"options",
|
||||
col.customLabel
|
||||
? res.data.list.map((i) => ({
|
||||
label: col.optionLabel ? i[col.optionLabel] : i.name,
|
||||
value: col.optionValue ? i[col.optionValue] : i.id,
|
||||
[col.customLabel]: i[col.customLabel],
|
||||
}))
|
||||
: res.data.list.map((i) => ({
|
||||
label: col.optionLabel ? i[col.optionLabel] : i.name,
|
||||
value: col.optionValue ? i[col.optionValue] : i.id,
|
||||
}))
|
||||
);
|
||||
|
||||
// 是否需要缓存数据列表
|
||||
if ("injectTo" in col || col.cached) {
|
||||
this.cachedList[col.prop] = res.data.list;
|
||||
}
|
||||
|
||||
// 设置监听
|
||||
if ("injectTo" in col) {
|
||||
console.log("set watcher: ", col.prop);
|
||||
|
||||
const valueProp = "optionValue" in col ? col.optionValue : "id";
|
||||
const unwatch = this.$watch(
|
||||
() => this.dataForm[col.prop],
|
||||
(val) => {
|
||||
console.log("do watcher: ", col.prop);
|
||||
if (col.disableWatcherOnEdit && this.editMode) return;
|
||||
if (!val) {
|
||||
col.injectTo.map((item) => {
|
||||
this.$set(this.dataForm, item[0], null);
|
||||
this.$forceUpdate();
|
||||
});
|
||||
return;
|
||||
}
|
||||
const chosenObject = this.cachedList[col.prop].find((i) => i[valueProp] === val);
|
||||
if (chosenObject) {
|
||||
col.injectTo.map((item) => {
|
||||
this.$set(this.dataForm, item[0], chosenObject[item[1]]);
|
||||
this.$forceUpdate();
|
||||
});
|
||||
} else {
|
||||
console.log('[x] if ("injectTo" in col) {');
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: false,
|
||||
}
|
||||
);
|
||||
this.watchList.push(unwatch);
|
||||
}
|
||||
|
||||
if ("autoUpdateProp" in col) {
|
||||
this.$set(this.savedDatalist, col.prop, res.data.list);
|
||||
}
|
||||
|
||||
// end branch
|
||||
} else if (Array.isArray(res.data)) {
|
||||
// 填充 options
|
||||
this.$set(
|
||||
col,
|
||||
"options",
|
||||
col.customLabel
|
||||
? res.data.map((i) => ({
|
||||
label: col.optionLabel ? i[col.optionLabel] : i.name,
|
||||
value: col.optionValue ? i[col.optionValue] : i.id,
|
||||
[col.customLabel]: i[col.customLabel],
|
||||
}))
|
||||
: res.data.map((i) => ({
|
||||
label: col.optionLabel ? i[col.optionLabel] : i.name,
|
||||
value: col.optionValue ? i[col.optionValue] : i.id,
|
||||
}))
|
||||
);
|
||||
|
||||
// 是否需要缓存数据列表
|
||||
if ("injectTo" in col || col.cached) {
|
||||
this.cachedList[col.prop] = res.data;
|
||||
}
|
||||
|
||||
// 设置监听
|
||||
if ("injectTo" in col && !col.watcher) {
|
||||
console.log("set watcher: ", col.prop);
|
||||
|
||||
const valueProp = "optionValue" in col ? col.optionValue : "id";
|
||||
const unwatch = this.$watch(
|
||||
() => this.dataForm[col.prop],
|
||||
(val) => {
|
||||
if (col.disableWatcherOnEdit && this.editMode) return;
|
||||
console.log("do watcher: ", col.prop);
|
||||
if (!val) {
|
||||
col.injectTo.map((item) => {
|
||||
this.$set(this.dataForm, item[0], null);
|
||||
this.$forceUpdate();
|
||||
});
|
||||
return;
|
||||
}
|
||||
const chosenObject = this.cachedList[col.prop].find((i) => i[valueProp] === val);
|
||||
if (chosenObject) {
|
||||
col.injectTo.map((item) => {
|
||||
this.$set(this.dataForm, item[0], chosenObject[item[1]]);
|
||||
this.$forceUpdate();
|
||||
});
|
||||
} else {
|
||||
console.log('[x] if ("injectTo" in col) {');
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: false,
|
||||
}
|
||||
);
|
||||
this.$set(col, "watcher", unwatch);
|
||||
this.watchList.push(unwatch);
|
||||
}
|
||||
|
||||
if ("autoUpdateProp" in col) {
|
||||
this.$set(this.savedDatalist, col.prop, res.data);
|
||||
}
|
||||
}
|
||||
// end branch
|
||||
} else {
|
||||
col.options.splice(0);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("after getData: ", promiseHistory);
|
||||
};
|
||||
|
||||
/** 处理函数 */
|
||||
const handleFn = (prevProp, anchorField, anchorValue, targetField, col) => {
|
||||
console.log("[handleFn]: ", prevProp, anchorField, anchorValue, targetField);
|
||||
/** 此时 cachedList 已经确保可用了 */
|
||||
const target = this.cachedList[prevProp].find((i) => i[anchorField] === anchorValue);
|
||||
const param = target ? target[targetField] : "";
|
||||
console.log("((( chosenObject )))", target);
|
||||
|
||||
getData(col, param);
|
||||
};
|
||||
|
||||
this.configs.form.rows.forEach((row) => {
|
||||
row.forEach((col) => {
|
||||
if (col.fetchData && typeof col.fetchData === "function" && !col.hasPrev) {
|
||||
getData(col);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/** 必须要遍历两遍 */
|
||||
this.configs.form.rows.forEach((row) => {
|
||||
row.forEach((col) => {
|
||||
if (col.fetchData && typeof col.fetchData === "function" && col.hasPrev) {
|
||||
console.log("[hasPrev] set watcher: ", col.hasPrev);
|
||||
|
||||
// 获取数据 - 需要等待前置条件时
|
||||
const unwatch = this.$watch(
|
||||
() => this.dataForm[col.hasPrev],
|
||||
(val) => {
|
||||
console.log("[hasPrev] do watcher: ", col.hasPrev);
|
||||
if (!val) {
|
||||
col.injectTo.map((item) => {
|
||||
this.$set(this.dataForm, item[0], null);
|
||||
this.$forceUpdate();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.editMode) this.$set(this.dataForm, col.prop, null);
|
||||
|
||||
const { search, get } = col.fetchDataParam; // 伴随着 hasPrev 出现的
|
||||
|
||||
if (!this.cachedList[col.hasPrev]) {
|
||||
// 如果 prev 还尚不存在,但此时 promiseHistory 一定存在了
|
||||
promiseHistory[col.hasPrev].then(() => {
|
||||
handleFn(col.hasPrev, search, val, get, col);
|
||||
});
|
||||
} else {
|
||||
handleFn(col.hasPrev, search, val, get, col);
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: false,
|
||||
}
|
||||
);
|
||||
this.watchList.push(unwatch);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (this.configs.extraFields)
|
||||
this.configs.extraFields.forEach((cnf) => {
|
||||
if (cnf.listenTo) {
|
||||
console.log("set watcher for: ", cnf.prop);
|
||||
const unwatch = this.$watch(
|
||||
() => this.dataForm[cnf.listenTo.prop],
|
||||
(carId) => {
|
||||
console.log("do watcher for: ", cnf.prop);
|
||||
if (!carId) return;
|
||||
if (cnf.disableWatcherOnEdit && this.editMode) return;
|
||||
cnf.listenTo.handler.call(this, this.cachedList[cnf.listenTo.prop], carId);
|
||||
},
|
||||
{
|
||||
immediate: false,
|
||||
}
|
||||
);
|
||||
this.watchList.push(unwatch);
|
||||
}
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
uploadHeaders() {
|
||||
return {
|
||||
token: Cookies.get("token") || "",
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
IsNotAvaliable(col) {
|
||||
return this.detailMode || (col.disableOnEdit && this.editMode);
|
||||
},
|
||||
handleFilelistUpdate(newFilelist) {
|
||||
// TODO: 直接访问 .files 可能不太合适
|
||||
this.dataForm.files = newFilelist.map((file) => ({
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
url: file.url,
|
||||
typeCode: file.typeCode,
|
||||
}));
|
||||
// 更新请求
|
||||
if ("id" in this.dataForm && this.dataForm.id !== null && this.dataForm.id !== undefined) this.addOrUpdate("PUT");
|
||||
else
|
||||
this.$notify({
|
||||
title: "等待保存",
|
||||
message: "已添加文件,请手动点击保存!",
|
||||
type: "warning",
|
||||
duration: 2500,
|
||||
});
|
||||
},
|
||||
|
||||
/** utitilities */
|
||||
showButton(operate) {
|
||||
const notDetailMode = !this.detailMode;
|
||||
const showAlways = operate.showAlways ?? false;
|
||||
const editMode = operate.showOnEdit && this.dataForm.id;
|
||||
const addMode = !operate.showOnEdit && !this.dataForm.id;
|
||||
const permission = operate.permission ? this.$hasPermission(operate.permission) : true;
|
||||
return notDetailMode && (showAlways || ((editMode || addMode) && permission));
|
||||
},
|
||||
|
||||
resetForm(excludeId = false, immediate = false) {
|
||||
setTimeout(
|
||||
() => {
|
||||
Object.keys(this.dataForm).forEach((key) => {
|
||||
if (excludeId && key === "id") return;
|
||||
this.dataForm[key] = null;
|
||||
});
|
||||
Object.keys(this.shadowDataForm).forEach((key) => {
|
||||
this.shadowDataForm[key] = null;
|
||||
});
|
||||
this.$refs.dataForm.clearValidate();
|
||||
this.$emit("dialog-closed"); // 触发父组件销毁自己
|
||||
},
|
||||
immediate ? 0 : 200
|
||||
);
|
||||
},
|
||||
|
||||
/** init **/
|
||||
init(id, detailMode, tagInfo, extraParams) {
|
||||
// console.log("[DialogJustForm] init", this.dataForm, id, detailMode);
|
||||
if (this.$refs.dataForm) {
|
||||
// console.log("[DialogJustForm] clearing form validation...");
|
||||
// 当不是首次渲染dialog的时候,一开始就清空验证信息,本组件的循环里只有一个 dataForm 所以只用取 [0] 即可
|
||||
this.$refs.dataForm.clearValidate();
|
||||
}
|
||||
|
||||
this.detailMode = detailMode ?? false;
|
||||
this.editMode = id ? true : false;
|
||||
|
||||
/** 判断 extraParams */
|
||||
if (extraParams && typeof extraParams === "object") {
|
||||
for (const [key, value] of Object.entries(extraParams)) {
|
||||
// console.log("[dialog] dataForm | key | value", this.dataForm, key, value);
|
||||
this.$set(this.dataForm, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.dataForm.id = id || null;
|
||||
if (this.dataForm.id) {
|
||||
// 如果是编辑
|
||||
this.loadingStatus = true;
|
||||
|
||||
// 获取基本信息
|
||||
this.$http
|
||||
.get(this.urls.base + `/${this.dataForm.id}`)
|
||||
.then(({ data: res }) => {
|
||||
if (res && res.code === 0) {
|
||||
if (res.data) this.dataForm = __pick(res.data, Object.keys(this.dataForm));
|
||||
else {
|
||||
this.$message({
|
||||
message: "后端返回的数据为 null",
|
||||
type: "error",
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
/** 格式化文件上传列表 */
|
||||
if (Array.isArray(this.dataForm.files)) {
|
||||
this.dataForm.files = this.dataForm.files.map((file) => ({
|
||||
id: file.id,
|
||||
name: file.fileUrl.split("/").pop(),
|
||||
typeCode: file.typeCode,
|
||||
url: file.fileUrl,
|
||||
}));
|
||||
}
|
||||
// console.log("[DialogJustForm] init():", this.dataForm);
|
||||
} else {
|
||||
this.$message({
|
||||
message: `${res.code}: ${res.msg}`,
|
||||
type: "error",
|
||||
duration: 1500,
|
||||
});
|
||||
}
|
||||
this.loadingStatus = false;
|
||||
})
|
||||
.catch((err) => {
|
||||
this.loadingStatus = false;
|
||||
this.$message({
|
||||
message: `${err}`,
|
||||
type: "error",
|
||||
duration: 1500,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.loadingStatus = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleButtonClick(col) {
|
||||
if (!("onClick" in col)) {
|
||||
console.log("[handleButtonClick] 配置文件config.js 里没有绑定 onClick");
|
||||
return;
|
||||
}
|
||||
|
||||
col.onClick.call(this, this.dataForm.id);
|
||||
},
|
||||
|
||||
/** handlers */
|
||||
handleSelectChange(col, eventValue) {
|
||||
if ("autoUpdateProp" in col) {
|
||||
// 自动更新 相关联 的字段
|
||||
// console.log(col.options, eventValue, this.savedDatalist);
|
||||
const item = this.savedDatalist[col.prop].find((item) => item.id === eventValue);
|
||||
this.shadowDataForm[col.autoUpdateProp] = item.cate ?? null;
|
||||
}
|
||||
this.$forceUpdate();
|
||||
},
|
||||
|
||||
handleSwitchChange(val) {
|
||||
console.log("[dialog] switch change: ", val, this.dataForm);
|
||||
},
|
||||
|
||||
handleComponentModelUpdate(propName, { subject, payload: { data } }) {
|
||||
this.dataForm[propName] = JSON.stringify(data);
|
||||
console.log("[DialogJustForm] handleComponentModelUpdate", this.dataForm[propName]);
|
||||
},
|
||||
|
||||
addOrUpdate(method = "POST", url) {
|
||||
if ("parentId" in this.dataForm) {
|
||||
console.log("[DialogJustForm parentId]", this.dataForm.parentId);
|
||||
// 对特殊的键做特殊处理,如 parentId 是一个 cascader,获取的值是 ["xxx"],后端只需要xxx
|
||||
const lastItem = this.dataForm.parentId.length - 1;
|
||||
this.dataForm.parentId = this.dataForm.parentId[lastItem];
|
||||
}
|
||||
|
||||
this.$refs.dataForm.validate((passed, result) => {
|
||||
if (passed) {
|
||||
this.loadingStatus = true;
|
||||
|
||||
let httpPayload = null;
|
||||
/** 针对有文件上传选项的弹窗提供特殊的 payload */
|
||||
if (this.dataForm.files) {
|
||||
httpPayload = {
|
||||
...this.dataForm,
|
||||
fileIds: this.dataForm.files.map((file) => file.id),
|
||||
};
|
||||
} else {
|
||||
httpPayload = this.dataForm;
|
||||
}
|
||||
|
||||
/** 针对时间段设置 payload */
|
||||
if ("startTime" in this.dataForm && "endTime" in this.dataForm) {
|
||||
const { startTime, endTime } = this.dataForm;
|
||||
httpPayload = {
|
||||
...httpPayload,
|
||||
startTime: startTime
|
||||
? moment(startTime).format("YYYY-MM-DDTHH:mm:ss")
|
||||
: moment().format("YYYY-MM-DDTHH:mm:ss"),
|
||||
endTime: endTime ? moment(endTime).format("YYYY-MM-DDTHH:mm:ss") : null, // moment().format("YYYY-MM-DDTHH:mm:ss"),
|
||||
};
|
||||
}
|
||||
|
||||
/** 针对时间段设置 payload */
|
||||
if ("updateTime" in this.dataForm) {
|
||||
const { updateTime } = this.dataForm;
|
||||
httpPayload = {
|
||||
...httpPayload,
|
||||
updateTime: updateTime
|
||||
? moment(updateTime).format("YYYY-MM-DDTHH:mm:ss")
|
||||
: moment().format("YYYY-MM-DDTHH:mm:ss"),
|
||||
};
|
||||
}
|
||||
|
||||
/** 发送 */
|
||||
return this.$http({
|
||||
// url: this.urls.formUrl ? this.urls.formUrl : this.urls.base,
|
||||
url: url ?? this.urls.base,
|
||||
method,
|
||||
data: httpPayload,
|
||||
})
|
||||
.then(({ data: res }) => {
|
||||
console.log("[add&update] res is: ", res);
|
||||
this.loadingStatus = false;
|
||||
if (res.code === 0) {
|
||||
this.$message.success(method === "POST" ? "添加成功" : "更新成功");
|
||||
this.$emit("refreshDataList");
|
||||
// if (method === "POST") this.handleClose();
|
||||
this.handleClose();
|
||||
} else {
|
||||
this.$message({
|
||||
message: `${res.code}: ${res.msg}`,
|
||||
type: "error",
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((errMsg) => {
|
||||
this.$message.error("参数错误:" + errMsg);
|
||||
if (this.loadingStatus) this.loadingStatus = false;
|
||||
});
|
||||
} else {
|
||||
this.$message.error("请核查字段信息");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleBtnClick(payload) {
|
||||
console.log("btn click payload: ", payload);
|
||||
|
||||
if ("name" in payload) {
|
||||
switch (payload.name) {
|
||||
case "cancel":
|
||||
this.handleClose();
|
||||
break;
|
||||
case "reset":
|
||||
this.resetForm(true, true); // true means exclude id, immediate execution
|
||||
break;
|
||||
case "add":
|
||||
case "update":
|
||||
this.addOrUpdate(payload.name === "add" ? "POST" : "PUT");
|
||||
break;
|
||||
case "add-pos-manually": {
|
||||
this.addOrUpdate("POST", this.urls.posFormUrl);
|
||||
break;
|
||||
}
|
||||
case "add-car-payload": {
|
||||
console.log("edit-car-payload", payload);
|
||||
this.addOrUpdate("POST", this.urls.payloadFormUrl);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log("[x] 不是这么用的! 缺少name属性");
|
||||
}
|
||||
},
|
||||
|
||||
handleUploadChange(file, fileList) {
|
||||
console.log("[Upload] handleUploadChange...", file, fileList);
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.resetForm();
|
||||
this.$emit("update:dialogVisible", false);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-simple >>> .el-dialog__body {
|
||||
/* padding-top: 16px !important;
|
||||
padding-bottom: 16px !important; */
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.el-select,
|
||||
.el-cascader,
|
||||
.el-date-editor {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.dialog-simple >>> .el-dialog__header {
|
||||
padding: 10px 20px 10px;
|
||||
/* background: linear-gradient(to bottom, rgba(0, 0, 0, 0.25), white); */
|
||||
}
|
||||
</style>
|
||||
978
src/layout/components/DialogWithMenu.vue
Normal file
978
src/layout/components/DialogWithMenu.vue
Normal file
@@ -0,0 +1,978 @@
|
||||
<template>
|
||||
<el-dialog class="dialog-with-menu" :visible="dialogVisible" :destroy-on-close="false" @close="handleClose"
|
||||
:close-on-click-modal="configs.clickModalToClose ?? true">
|
||||
<!-- title -->
|
||||
<div slot="title" class="dialog-title">
|
||||
<h1 class="">
|
||||
{{ detailMode ? "查看详情" : dataForm.id ? "编辑" : "新增" }}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="dialog-body__inner relative">
|
||||
<!-- v-if="dataForm.id && !detailMode && /属性|详情/.test(activeMenu) && $hasPermission()" -->
|
||||
<el-button v-if="configs.allowAdd ?? (dataForm.id && !detailMode && /属性|详情|参数/.test(activeMenu))" plain
|
||||
type="primary" size="small" class="at-right-top" style="margin-bottom: 16px" @click="handleAddParam()">+
|
||||
添加</el-button>
|
||||
<template v-if="dataForm.id && !detailMode && /附件/.test(activeMenu)">
|
||||
<el-upload style="position: absolute; width: 100%; height: 0" name="files" :action="uploadUrl"
|
||||
:show-file-list="false" :headers="uploadHeaders" :on-success="handleUploadSuccess"
|
||||
:before-upload="handleUploadCheck">
|
||||
<el-button plain type="primary" size="small" class="at-right-top" style=""> <i class="el-icon-upload"></i> 上传
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<!-- menu -->
|
||||
<el-tabs v-model="activeMenu" type="card" @tab-click="handleTabClick">
|
||||
<!-- <el-tab-pane v-for="(tab, index) in configs.menu" :key="index" :label="tab.name" :name="tab.name"> -->
|
||||
<el-tab-pane v-for="(tab, index) in actualMenus" :key="index" :name="tab.name">
|
||||
<span class="slot" slot="label">
|
||||
<i :class="{
|
||||
'el-icon-edit': tab.key === 'info',
|
||||
'el-icon-s-data': tab.key === 'attr',
|
||||
'el-icon-folder-opened': tab.key === 'attachment',
|
||||
}"></i>
|
||||
{{ tab.name }}
|
||||
</span>
|
||||
|
||||
<!-- 表单标签页 -->
|
||||
<div v-if="tab.key === 'info'">
|
||||
<!-- form -->
|
||||
<el-form ref="dataForm" :model="dataForm" v-loading="loadingStatus">
|
||||
<el-row v-for="(row, rowIndex) in configs.form.rows" :key="'row_' + rowIndex" :gutter="20">
|
||||
<el-col v-for="(col, colIndex) in row" :key="colIndex" :span="24 / row.length">
|
||||
<el-form-item :label="col.label" :prop="col.prop" :rules="col.rules || null"
|
||||
v-show="!col.forceDisabled || (col.forceDisabled && dataForm.id)">
|
||||
<div v-if="col.forceDisabled && dataForm.id" class="force-disabled">
|
||||
<el-tag :key="col.key" :type="col.type">{{ dataForm[col.prop] || "-" }}</el-tag>
|
||||
</div>
|
||||
<el-input v-if="col.input" v-model="dataForm[col.prop]" clearable
|
||||
:disabled="disableCondition(col.prop)" v-bind="col.elparams" />
|
||||
<el-cascader v-if="col.cascader" v-model="dataForm[col.prop]" :options="col.options"
|
||||
:disabled="detailMode" v-bind="col.elparams"></el-cascader>
|
||||
<el-select v-if="col.select" v-model="dataForm[col.prop]" clearable
|
||||
:disabled="disableCondition(col.prop)" v-bind="col.elparams"
|
||||
@change="handleSelectChange(col, $event)">
|
||||
<el-option v-for="(opt, optIdx) in col.options" :key="'option_' + optIdx" :label="opt.label"
|
||||
:value="opt.value" />
|
||||
</el-select>
|
||||
<el-switch v-if="col.switch" v-model="dataForm[col.prop]" :active-value="1" :inactive-value="0"
|
||||
@change="handleSwitchChange" :disabled="disableCondition(col.prop)" />
|
||||
<el-input v-if="col.textarea" type="textarea" v-model="dataForm[col.prop]"
|
||||
:disabled="disableCondition(col.prop)" v-bind="col.elparams" />
|
||||
<quillEditor v-if="col.richInput" ref="quill-editor" v-model="dataForm[col.prop]"
|
||||
:options="col.quillConfig ?? defaultQuillConfig" style="margin-top: 42px"
|
||||
:disabled="disableCondition(col.prop)" />
|
||||
|
||||
<div class="" v-if="col.component" style="margin: 42px 0 0">
|
||||
<!-- 下面这个 component 几乎是为 富文本 quill 定制的了... TODO:后续可能会根据业务需求创建新的版本 -->
|
||||
<component :is="col.component" :key="'component_' + col.prop"
|
||||
@update:modelValue="handleComponentModelUpdate(col.prop, $event)" :modelValue="dataForm[col.prop]"
|
||||
:mode="detailMode ? 'detail' : dataForm.id ? 'edit' : 'create'" />
|
||||
</div>
|
||||
<!-- add more... -->
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 表格标签页 -->
|
||||
<div v-if="dataForm.id && tab.key === 'attr'" key="attr-list">
|
||||
<BaseListTable :table-config="null" :column-config="filteredTableProps" :table-data="subList"
|
||||
@operate-event="handleTableRowOperate" :current-page="attrPage" :current-size="attrSize"
|
||||
:refresh-layout-key="Math.random()" v-loading="loadingStatus" />
|
||||
<!-- paginator -->
|
||||
<el-pagination class="" style="text-align: left" background @size-change="handleSizeChange"
|
||||
@current-change="handlePageChange" :current-page.sync="attrPage" :page-sizes="[5, 10, 20]"
|
||||
:page-size="attrSize" :total="attrTotal" layout="total, sizes, prev, next"></el-pagination>
|
||||
</div>
|
||||
|
||||
<!-- 附件标签页 -->
|
||||
<div v-if="dataForm.id && tab.key === 'attachment'" key="attachment">
|
||||
<div class="upload-tips" style="font-size: 0.8em; margin-bottom: 12px">文件大小不要超过 2MB</div>
|
||||
<!-- 附件列表 -->
|
||||
<div class="" v-loading="loadingStatus">
|
||||
<ul class="file-list">
|
||||
<li v-for="(file, index) in fileList" :key="index">
|
||||
<span class="file-name">{{ file.name }}</span>
|
||||
<span class="file-operations">
|
||||
<span class="file-icon preview" @click="handleFileClick('view', file)">
|
||||
<i class="el-icon-view" style="cursor: pointer"></i>
|
||||
</span>
|
||||
<span class="file-icon download" @click="handleFileClick('download', file)">
|
||||
<i class="el-icon-download" style="color: #0b58ff; cursor: pointer"></i>
|
||||
</span>
|
||||
<span class="file-icon delete" @click="handleFileClick('delete', file)">
|
||||
<i class="el-icon-delete" style="color: red; cursor: pointer"></i>
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- img preview dialog -->
|
||||
<el-dialog key="image-preview-dialog" class="image-preview-dialog" :visible.sync="imgPreviewDialogVisible"
|
||||
:append-to-body="true">
|
||||
<div class="img-container">
|
||||
<img width="100%" :src="currentImgUrl" alt="" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<!-- sub dialog -->
|
||||
<small-dialog :append-to-body="true" v-if="showSubDialog" ref="subDialog" :url="urls.subase"
|
||||
:configs="configs.subDialog" :related-id="dataForm.id" @refreshDataList="getSubList"></small-dialog>
|
||||
|
||||
<!-- footer -->
|
||||
<div slot="footer">
|
||||
<template v-for="(operate, index) in configs.form.operations">
|
||||
<el-button v-if="showButton(operate)" :key="'operation_' + index" :type="operate.type"
|
||||
@click="handleBtnClick(operate)"
|
||||
:loading="(operate.name === 'add' || operate.name === 'update') && btnLoading">{{ operate.label }}</el-button>
|
||||
</template>
|
||||
<el-button @click="handleBtnClick({ name: 'cancel' })">取消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { pick as __pick } from "@/utils/filters";
|
||||
import SmallDialog from "@/components/SmallDialog.vue";
|
||||
import BaseListTable from "@/components/BaseListTable.vue";
|
||||
import Cookies from "js-cookie";
|
||||
import "quill/dist/quill.core.css";
|
||||
import "quill/dist/quill.snow.css";
|
||||
import "quill/dist/quill.bubble.css";
|
||||
|
||||
import { quillEditor } from "vue-quill-editor";
|
||||
|
||||
function reConstructTreeData(listObj) {
|
||||
const entry = [];
|
||||
Object.keys(listObj).map((key) => {
|
||||
const currentNode = listObj[key];
|
||||
currentNode.label = currentNode.name;
|
||||
currentNode.value = currentNode.id;
|
||||
if (currentNode.parentId === "0") {
|
||||
entry.push(listObj[key]);
|
||||
return; // return { label: currentNode.name, value: currentNode.id, children: currentNode.children ?? [] };
|
||||
}
|
||||
const parentNode = listObj[currentNode.parentId];
|
||||
if (!parentNode.children) {
|
||||
parentNode.children = [];
|
||||
}
|
||||
parentNode.children.push(currentNode);
|
||||
});
|
||||
return entry;
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "DialogWithMenu",
|
||||
components: { SmallDialog, BaseListTable, quillEditor },
|
||||
props: {
|
||||
configs: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
dialogVisible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
inject: ["urls"],
|
||||
data() {
|
||||
const dataForm = {};
|
||||
const autoDisabledQueue = [];
|
||||
const watingToRefreshQueue = [];
|
||||
const cached = {}
|
||||
|
||||
this.configs.form.rows.forEach((row) => {
|
||||
row.forEach((col) => {
|
||||
if (col.upload) dataForm[col.prop] = col.default ?? [];
|
||||
else dataForm[col.prop] = col.default ?? null;
|
||||
|
||||
if (col.autoDisabled) autoDisabledQueue.push(col.prop);
|
||||
if (!!col.refreshOptionsAfterConfirm) watingToRefreshQueue.push(col);
|
||||
|
||||
if (col.fetchData)
|
||||
col.fetchData().then(({ data: res }) => {
|
||||
console.log("[Fetch Data]", "list" in res.data, res.data, res.data.list);
|
||||
if (res.code === 0) {
|
||||
if (col.cacheFetchedData) {
|
||||
// cache fetched data
|
||||
cached[col.prop] = 'list' in res.data ? res.data.list : (Array.isArray(res.data) ? res.data : [])
|
||||
}
|
||||
|
||||
if (!col.options || !col.options.length) {
|
||||
this.$set(
|
||||
col,
|
||||
"options",
|
||||
"list" in res.data
|
||||
? res.data.list.map((i) => ({
|
||||
label: col.optionLabel ? i[col.optionLabel] : i.name,
|
||||
value: col.optionValue ? i[col.optionValue] : i.id,
|
||||
}))
|
||||
: res.data.map((i) => ({
|
||||
label: col.optionLabel ? i[col.optionLabel] : i.name,
|
||||
value: col.optionValue ? i[col.optionValue] : i.id,
|
||||
}))
|
||||
);
|
||||
}
|
||||
// col.options = res.data.list;
|
||||
else if (col.options.length) {
|
||||
"list" in res.data ? res.data.list.unshift(...col.options) : res.data.unshift(...col.options);
|
||||
this.$set(
|
||||
col,
|
||||
"options",
|
||||
"list" in res.data
|
||||
? res.data.list.map((i) => ({
|
||||
label: col.optionLabel ? i[col.optionLabel] : i.name,
|
||||
value: col.optionValue ? i[col.optionValue] : i.id,
|
||||
}))
|
||||
: res.data.map((i) => ({
|
||||
label: col.optionLabel ? i[col.optionLabel] : i.name,
|
||||
value: col.optionValue ? i[col.optionValue] : i.id,
|
||||
}))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
col.options.splice(0);
|
||||
}
|
||||
// dataForm[col.prop] = col.default ?? null; // not perfect!
|
||||
});
|
||||
else if (col.fetchTreeData) {
|
||||
// 获取设备类型时触发的,用于前端构建属性结构,约定,parentId 为0时是顶级节点
|
||||
col.fetchTreeData().then(({ data: res }) => {
|
||||
console.log("[Fetch Tree Data]", res.data);
|
||||
if (res.code === 0) {
|
||||
// 先把数据先重构成一个对象
|
||||
const obj = {};
|
||||
|
||||
if ("list" in res.data) {
|
||||
res.data.list.map((item) => {
|
||||
obj[item.id] = item;
|
||||
});
|
||||
} else if (Array.isArray(res.data)) {
|
||||
res.data.map((item) => {
|
||||
obj[item.id] = item;
|
||||
});
|
||||
}
|
||||
// 再过滤这个对象
|
||||
let filteredList = reConstructTreeData(obj);
|
||||
console.log("** filteredList **", filteredList);
|
||||
// 最后设置 options
|
||||
this.$set(col, "options", filteredList);
|
||||
} else {
|
||||
col.options.splice(0);
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
// configs,
|
||||
btnLoading: false,
|
||||
loadingStatus: false,
|
||||
activeMenu: this.configs.menu[0].name,
|
||||
dataForm,
|
||||
detailMode: false,
|
||||
autoDisabledQueue,
|
||||
watingToRefreshQueue,
|
||||
cached,
|
||||
showBaseDialog: false,
|
||||
baseDialogConfig: null,
|
||||
subList: [],
|
||||
showSubDialog: false,
|
||||
disableXXX: false,
|
||||
defaultQuillConfig: {
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ font: [] }],
|
||||
[{ size: ["small", false, "large", "huge"] }], // custom dropdown
|
||||
["bold", "italic", "underline", "strike"], // toggled buttons
|
||||
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
|
||||
["blockquote", "code-block"],
|
||||
[{ header: 1 }, { header: 2 }], // custom button values
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
// [{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript
|
||||
[{ indent: "-1" }, { indent: "+1" }], // outdent/indent
|
||||
// [{ 'direction': 'rtl' }], // text direction
|
||||
// [{ 'header': [1, 2, 3, 4, 5, 6, false] }],
|
||||
// [{ 'align': [] }],
|
||||
// ['clean'] // remove formatting button
|
||||
],
|
||||
},
|
||||
theme: "snow",
|
||||
readOnly: false,
|
||||
placeholder: "在这里输入描述信息...",
|
||||
scrollingContainer: null,
|
||||
},
|
||||
attrPage: 1,
|
||||
attrSize: 10,
|
||||
attrTotal: 0,
|
||||
fileList: [],
|
||||
imgPreviewDialogVisible: false,
|
||||
currentImgUrl: "",
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.configs.form.rows.forEach((row) => {
|
||||
row.forEach((col) => {
|
||||
if (col.changeReflects && typeof col.changeReflects === 'object' && 'fromKey' in col.changeReflects && 'toProp' in col.changeReflects) {
|
||||
this.$watch(
|
||||
() => this.dataForm[col.prop],
|
||||
val => {
|
||||
if (val && (col.prop in this.cached)) {
|
||||
console.log("here changeReflects", col.prop, col.changeReflects.toProp, this.cached[col.prop])
|
||||
if (typeof col.changeReflects.fromKey === 'string') {
|
||||
this.dataForm[col.changeReflects.toProp] = this.cached[col.prop].find(item => item.id === val)?.[col.changeReflects.fromKey]
|
||||
} else if (Array.isArray(col.changeReflects.fromKey) && col.changeReflects.delimiter) {
|
||||
const foundItem = this.dataForm[col.changeReflects.toProp] = this.cached[col.prop].find(item => item.id === val)
|
||||
if (foundItem) {
|
||||
const values = col.changeReflects.fromKey.map(key => foundItem[key])
|
||||
this.dataForm[col.changeReflects.toProp] = values.join(col.changeReflects.delimiter)
|
||||
} else {
|
||||
this.dataForm[col.changeReflects.toProp] = col.changeReflects.delimiter
|
||||
console.log("[DialogWithMenu] mounted() 没找到对应数据")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: false
|
||||
}
|
||||
)
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
watch: {
|
||||
dialogVisible: function (val) {
|
||||
if (!!val) {
|
||||
this.attrPage = 1
|
||||
this.attrSize = 10
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
actualMenus() {
|
||||
return this.configs.menu.filter((m) => {
|
||||
if (m.onlyEditMode && !this.dataForm.id) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
filteredTableProps() {
|
||||
return this.detailMode ? this.configs.table.props.filter((v) => v.prop !== "operations") : this.configs.table.props;
|
||||
},
|
||||
uploadHeaders() {
|
||||
return {
|
||||
token: Cookies.get("token") || "",
|
||||
};
|
||||
},
|
||||
uploadUrl() {
|
||||
return this.configs.menu.find((item) => item.key === "attachment")?.actionUrl || "#";
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
disableCondition(prop) {
|
||||
return this.detailMode || (this.disableXXX && this.autoDisabledQueue.indexOf(prop) !== -1);
|
||||
},
|
||||
/** utitilities */
|
||||
showButton(operate) {
|
||||
const notDetailMode = !this.detailMode;
|
||||
const showAlways = operate.showAlways ?? false;
|
||||
const editMode = operate.showOnEdit && this.dataForm.id;
|
||||
const addMode = !operate.showOnEdit && !this.dataForm.id;
|
||||
const permission = operate.permission ? this.$hasPermission(operate.permission) : true;
|
||||
const currentMenuKey = this.configs.menu.find((item) => item.name === this.activeMenu)?.key;
|
||||
return notDetailMode && (showAlways || ((editMode || addMode) && permission)) && currentMenuKey === "info";
|
||||
},
|
||||
|
||||
resetForm(excludeId = false, immediate = false) {
|
||||
setTimeout(
|
||||
() => {
|
||||
Object.keys(this.dataForm).forEach((key) => {
|
||||
if (excludeId && key === "id") return;
|
||||
if ("files" in this.dataForm) this.dataForm.files = [];
|
||||
if ("fileIds" in this.dataForm) this.dataForm.fileIds = [];
|
||||
else this.dataForm[key] = null;
|
||||
if (Array.isArray(this.fileList)) {
|
||||
this.fileList = [];
|
||||
}
|
||||
});
|
||||
this.activeMenu = this.configs.menu[0].name;
|
||||
this.$refs.dataForm[0].clearValidate();
|
||||
},
|
||||
immediate ? 0 : 200
|
||||
);
|
||||
},
|
||||
|
||||
updateOptions() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.watingToRefreshQueue.length) {
|
||||
this.watingToRefreshQueue.forEach((opt) => {
|
||||
console.log("[刷新数据, ", opt, "]");
|
||||
if ("fetchData" in opt) {
|
||||
opt.fetchData(this.dataForm.id ? this.dataForm.id : -1).then(({ data: res }) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(
|
||||
opt,
|
||||
"options",
|
||||
"list" in res.data
|
||||
? res.data.list.map((i) => ({ label: i.code, value: i.id }))
|
||||
: res.data.map((i) => ({ label: i.code, value: i.id }))
|
||||
);
|
||||
|
||||
resolve({ done: true });
|
||||
} else {
|
||||
this.$message({
|
||||
message: `${res.code}: ${res.msg}`,
|
||||
type: "error",
|
||||
duration: 1500,
|
||||
});
|
||||
|
||||
resolve({ done: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else resolve(null);
|
||||
});
|
||||
},
|
||||
|
||||
/** init **/
|
||||
init(id, detailMode, menu) {
|
||||
// this.dialogVisible = true;
|
||||
if (this.$refs.dataForm && this.$refs.dataForm.length) {
|
||||
// 当不是首次渲染dialog的时候,一开始就清空验证信息,本组件的循环里只有一个 dataForm 所以只用取 [0] 即可
|
||||
this.$refs.dataForm[0].clearValidate();
|
||||
}
|
||||
console.log("[dialog] DialogWithHead init():", id, detailMode);
|
||||
|
||||
this.detailMode = detailMode ?? false;
|
||||
this.$nextTick(() => {
|
||||
this.dataForm.id = id || null;
|
||||
if (this.dataForm.id) {
|
||||
// 如果是编辑
|
||||
this.loadingStatus = true;
|
||||
// 提前获取属性列表
|
||||
this.getSubList();
|
||||
// 获取详情
|
||||
this.updateOptions().then((result) => {
|
||||
if (result === null || (typeof result === "object" && result.done)) {
|
||||
this.$http.get(this.urls.base + `/${this.dataForm.id}`).then(({ data: res }) => {
|
||||
if (res && res.code === 0) {
|
||||
const dataFormKeys = Object.keys(this.dataForm);
|
||||
this.dataForm = __pick(res.data, dataFormKeys);
|
||||
if ("files" in res.data) {
|
||||
console.log("[DialogWithMenu] fileList===>", res.data.files, this.fileList);
|
||||
/** 返回的文件列表 */
|
||||
this.fileList = res.data.files
|
||||
? res.data.files.map((file) => ({
|
||||
id: file.id,
|
||||
name: file.fileUrl.split("/").pop(),
|
||||
url: file.fileUrl,
|
||||
typeCode: file.typeCode,
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
}
|
||||
this.loadingStatus = false;
|
||||
// 是否要跳转到附件页
|
||||
if (menu && menu.key) {
|
||||
this.activeMenu = this.configs.menu.find((item) => item.key === menu.key)?.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 如果不是编辑
|
||||
this.updateOptions();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** handlers */
|
||||
handleFileClick(type, file) {
|
||||
switch (type) {
|
||||
case "view": {
|
||||
// 加载图片
|
||||
this.$http
|
||||
.get("/pms/attachment/downloadFile", {
|
||||
params: {
|
||||
attachmentId: file.id,
|
||||
type: 0, // 0 预览,1 下载
|
||||
},
|
||||
responseType: "blob",
|
||||
})
|
||||
.then(({ data: res }) => {
|
||||
console.log("preivew", res);
|
||||
if (/image/i.test(res.type)) {
|
||||
// 显示图片
|
||||
this.currentImgUrl = URL.createObjectURL(res);
|
||||
this.imgPreviewDialogVisible = true;
|
||||
} else if (/pdf/i.test(res.type)) {
|
||||
// 预览pdf
|
||||
let a = document.createElement('a')
|
||||
a.setAttribute('target', '_blank')
|
||||
a.href = URL.createObjectURL(res)
|
||||
a.click()
|
||||
console.log('before remove a ', a)
|
||||
a.remove()
|
||||
console.log('removed a ', a)
|
||||
} else {
|
||||
this.$message({
|
||||
message: "非图片和PDF文件请下载后预览",
|
||||
type: "error",
|
||||
duration: 1500,
|
||||
});
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "download": {
|
||||
// 下载图片
|
||||
this.$http
|
||||
.get("/pms/attachment/downloadFile", {
|
||||
params: {
|
||||
attachmentId: file.id,
|
||||
type: 1, // 0 预览,1 下载
|
||||
},
|
||||
responseType: "blob",
|
||||
})
|
||||
.then(({ data: res }) => {
|
||||
const blob = new Blob([res]);
|
||||
|
||||
/** 通知 */
|
||||
this.$notify({
|
||||
title: "成功",
|
||||
message: "开始下载",
|
||||
type: "success",
|
||||
duration: 1200,
|
||||
});
|
||||
|
||||
if ("download" in document.createElement("a")) {
|
||||
const alink = document.createElement("a");
|
||||
alink.download = file.name;
|
||||
alink.style.display = "none";
|
||||
alink.target = "_blank";
|
||||
alink.href = URL.createObjectURL(blob);
|
||||
document.body.appendChild(alink);
|
||||
alink.click();
|
||||
URL.revokeObjectURL(alink.href);
|
||||
document.body.removeChild(alink);
|
||||
} else {
|
||||
navigator.msSaveBlob(blob, fileName);
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "delete": {
|
||||
return this.$confirm(`确定删除图片: ${file.name}`, "提示", {
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "我再想想",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
this.loadingStatus = true;
|
||||
const newFilelist = this.fileList.filter((f) => f.id !== file.id);
|
||||
this.updateRemoteFiles(newFilelist).then((msg) => {
|
||||
if (msg && msg === "success") {
|
||||
this.fileList = newFilelist;
|
||||
this.loadingStatus = false;
|
||||
/** 通知 */
|
||||
this.$notify({
|
||||
title: "成功",
|
||||
message: "已删除",
|
||||
type: "success",
|
||||
duration: 1200,
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((err) => { });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleUploadSuccess(response, file, fileList) {
|
||||
console.log("[DialogWithMenu] uploadedFileList", response, file, fileList);
|
||||
|
||||
if (response.code === 0) {
|
||||
const uploadedFile = response.data[0];
|
||||
const fileItem = {
|
||||
id: uploadedFile.id,
|
||||
name: uploadedFile.fileUrl.split("/").pop(),
|
||||
url: uploadedFile.fileUrl,
|
||||
typeCode: file.typeCode,
|
||||
};
|
||||
|
||||
this.loadingStatus = true;
|
||||
this.updateRemoteFiles([...this.fileList, fileItem]).then((msg) => {
|
||||
if (msg && msg === "success") {
|
||||
this.fileList.push(fileItem);
|
||||
this.loadingStatus = false;
|
||||
/** 通知 */
|
||||
this.$notify({
|
||||
title: "成功",
|
||||
message: "上传成功",
|
||||
type: "success",
|
||||
duration: 1200,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateRemoteFiles(filelist) {
|
||||
return this.$http
|
||||
.put("/pms/product", {
|
||||
id: "id" in this.dataForm ? this.dataForm.id : "DEFAULT_ID",
|
||||
fileIds: filelist.map((f) => f.id),
|
||||
})
|
||||
.then(({ data: res }) => {
|
||||
if (res.code === 0) return "success";
|
||||
});
|
||||
},
|
||||
|
||||
handleUploadCheck(file) {
|
||||
console.log("[before upload]", file);
|
||||
const LIMIT = 2 * 1024 * 1024; // bytes
|
||||
if (file.size > LIMIT) {
|
||||
this.$message({
|
||||
message: "文件大小不能超过 2MB",
|
||||
type: "error",
|
||||
duration: 1500,
|
||||
});
|
||||
return false;
|
||||
} else return true;
|
||||
},
|
||||
|
||||
handleComponentModelUpdate(propName, { subject, payload: { data } }) {
|
||||
this.dataForm[propName] = JSON.stringify(data);
|
||||
console.log("[DialogJustForm] handleComponentModelUpdate", this.dataForm[propName]);
|
||||
},
|
||||
handleSelectChange(col, eventValue) {
|
||||
console.log("[dialog] select change: ", col, eventValue);
|
||||
},
|
||||
handleSwitchChange(val) {
|
||||
console.log("[dialog] switch change: ", val, this.dataForm);
|
||||
},
|
||||
handleBtnClick(payload) {
|
||||
console.log("btn click payload: ", payload);
|
||||
|
||||
if ("name" in payload) {
|
||||
switch (payload.name) {
|
||||
case "cancel":
|
||||
this.handleClose();
|
||||
break;
|
||||
case "reset":
|
||||
this.resetForm(true, true); // true means exclude id
|
||||
break;
|
||||
case "add":
|
||||
case "update": {
|
||||
this.$refs.dataForm[0].validate((passed, result) => {
|
||||
if (passed) {
|
||||
// 如果通过验证
|
||||
this.btnLoading = true
|
||||
this.loadingStatus = true;
|
||||
const method = payload.name === "add" ? "POST" : "PUT";
|
||||
|
||||
// 判断是否有附件选项
|
||||
const hasAttachment = !!this.configs.menu.find((item) => item.key === "attachment");
|
||||
if (hasAttachment) {
|
||||
const fileIds = this.fileList.map((item) => item.id);
|
||||
this.$set(this.dataForm, "fileIds", fileIds);
|
||||
}
|
||||
|
||||
// 加载额外需要的 id
|
||||
let extraIds = {};
|
||||
if (this.configs.extraIds && typeof this.configs.extraIds === "object") {
|
||||
// 如果配置里,有 extraIds 选项
|
||||
Object.entries(this.configs.extraIds).forEach(([key, value]) => {
|
||||
extraIds[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
// 实际发送请求
|
||||
this.btnLoading = true
|
||||
this.$http({
|
||||
url: this.urls.base,
|
||||
method,
|
||||
data: {
|
||||
...extraIds,
|
||||
...this.dataForm,
|
||||
},
|
||||
})
|
||||
.then(({ data: res }) => {
|
||||
this.btnLoading = false
|
||||
this.loadingStatus = false;
|
||||
console.log("[add&update] res is: ", res);
|
||||
if (res.code === 0) {
|
||||
this.$message.success(payload.name === "add" ? "添加成功" : "更新成功");
|
||||
this.$emit("refreshDataList");
|
||||
|
||||
// 如果 watingToRefreshQueue 队列里有数据
|
||||
// if (this.watingToRefreshQueue.length) {
|
||||
// // 刷新队列
|
||||
// this.watingToRefreshQueue.forEach((opt) => {
|
||||
// console.log("[刷新数据, ", opt, "]");
|
||||
// if ("fetchData" in opt) {
|
||||
// opt.fetchData().then(({ data: res }) => {
|
||||
// if (res.code === 0) {
|
||||
// this.$set(
|
||||
// opt,
|
||||
// "options",
|
||||
// "list" in res.data
|
||||
// ? res.data.list.map((i) => ({ label: i.name, value: i.id }))
|
||||
// : res.data.map((i) => ({ label: i.name, value: i.id }))
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
this.handleClose();
|
||||
} else {
|
||||
this.$message({
|
||||
message: `${res.code}: ${res.msg}`,
|
||||
type: "error",
|
||||
duration: 2000,
|
||||
});
|
||||
if (this.btnLoading) this.btnLoading = false
|
||||
}
|
||||
})
|
||||
.catch((errMsg) => {
|
||||
this.$message.error("参数错误:" + errMsg);
|
||||
if (this.loadingStatus) this.loadingStatus = false;
|
||||
if (this.btnLoading) this.btnLoading = false
|
||||
});
|
||||
} else {
|
||||
this.$message.error("请核查字段信息");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
handleTabClick(payload) {
|
||||
// console.log("tab click payload: ", this.activeMenu);
|
||||
// if (this.activeMenu === this.configs.menu[1].name) {
|
||||
// // 获取数据
|
||||
// this.getSubList();
|
||||
// }
|
||||
},
|
||||
|
||||
handlePageChange(val) {
|
||||
this.getSubList(val, this.attrSize);
|
||||
},
|
||||
handleSizeChange(val) {
|
||||
this.attrPage = 1;
|
||||
this.attrSize = val
|
||||
this.getSubList(1, val);
|
||||
},
|
||||
|
||||
getSubList(page, size) {
|
||||
const params = {};
|
||||
params.page = page ?? this.attrPage;
|
||||
params.limit = size ?? this.attrSize;
|
||||
|
||||
const requiredParams = this.configs.table.extraParams;
|
||||
if (requiredParams) {
|
||||
if (Array.isArray(requiredParams)) {
|
||||
requiredParams.forEach((str) => {
|
||||
if (/id/i.test(str)) {
|
||||
params[str] = this.dataForm.id;
|
||||
} else {
|
||||
params[str] = "";
|
||||
}
|
||||
});
|
||||
} else if (typeof requiredParams === "string") {
|
||||
// 如果需要额外参数,一般肯定需要
|
||||
params[this.configs.table.extraParams] = this.dataForm.id;
|
||||
// 此时 dataForm.id 一定是存在的
|
||||
}
|
||||
}
|
||||
this.$http.get(this.urls.subpage, { params }).then(({ data: res }) => {
|
||||
console.log("[DialogWithMenu] getSubList:", res);
|
||||
if (res.code === 0 && res.data?.list) {
|
||||
// 有数据
|
||||
this.subList = res.data.list;
|
||||
this.attrTotal = res.data.total;
|
||||
} else {
|
||||
this.subList.splice(0);
|
||||
this.attrTotal = 0;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
handleAddParam(id) {
|
||||
this.showSubDialog = true;
|
||||
this.$nextTick(() => {
|
||||
this.$refs.subDialog.init(id ?? null);
|
||||
});
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.resetForm();
|
||||
this.$emit("update:dialogVisible", false);
|
||||
},
|
||||
|
||||
/** 列表handlers */
|
||||
handleAddItem() {
|
||||
// console.log('[dialog] handleAddItem ot list...');
|
||||
this.showBaseDialog = true;
|
||||
this.$nextTick(() => {
|
||||
console.log("[sub-dialog] ", this.$refs["sub-dialog"].init());
|
||||
});
|
||||
},
|
||||
handleTableRowOperate({ type, data }) {
|
||||
console.log("handleTableRowOperate", type, data);
|
||||
|
||||
switch (type) {
|
||||
case "delete": {
|
||||
// 确认是否删除
|
||||
console.log('delete....', data)
|
||||
const itemName = typeof data === 'object' ? (data.attrName || data.name || data.material || data.id) : data
|
||||
return this.$confirm(`是否删除条目: ${itemName}`, "提示", {
|
||||
confirmButtonText: "确认",
|
||||
cancelButtonText: "我再想想",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
// this.$http.delete(this.urls.base + `/${data}`).then((res) => {
|
||||
this.$http({
|
||||
url: this.urls.subase,
|
||||
method: "DELETE",
|
||||
data: [`${data.id}`],
|
||||
}).then(({ data: res }) => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success({
|
||||
message: "删除成功!",
|
||||
duration: 1000,
|
||||
onClose: () => {
|
||||
this.getSubList(1, 20);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((err) => { });
|
||||
}
|
||||
case "edit": {
|
||||
this.handleAddParam(data); /** data is ==> id */
|
||||
break;
|
||||
}
|
||||
// case 'view-detail-action':
|
||||
// this.openDialog(data, true);
|
||||
// break;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-menu {
|
||||
margin: 16px 0 !important;
|
||||
}
|
||||
|
||||
.el-menu.el-menu--horizontal {
|
||||
border: none !important;
|
||||
/* background: #0f02 !important; */
|
||||
}
|
||||
|
||||
/* .el-menu--horizontal > .el-menu-item.is-active { */
|
||||
/* border-bottom-color: #0b58ff; */
|
||||
/* } */
|
||||
|
||||
.dialog-with-menu>>>.el-dialog__body {
|
||||
/* padding-top: 16px !important;
|
||||
padding-bottom: 16px !important; */
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.el-select,
|
||||
.el-cascader {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.dialog-with-menu>>>.el-dialog__header {
|
||||
padding: 10px 20px 10px;
|
||||
/* background: linear-gradient(to bottom, rgba(0, 0, 0, 0.25), white); */
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.at-right-top {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
ul.file-list,
|
||||
ul.file-list>li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
max-height: 20vh;
|
||||
overflow-y: auto;
|
||||
margin-top: 12px;
|
||||
/* width: 240px; */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ul.file-list>li {
|
||||
border-radius: 4px;
|
||||
background-color: #edededd2;
|
||||
padding: 8px;
|
||||
margin-bottom: 2px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
ul.file-list>li:hover {
|
||||
background-color: #ededed;
|
||||
}
|
||||
|
||||
.file-operations {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
place-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* .image-preview-dialog {
|
||||
} */
|
||||
|
||||
.force-disabled {
|
||||
margin-top: 42px;
|
||||
}
|
||||
</style>
|
||||
24
src/layout/components/IframeToggle/index.vue
Normal file
24
src/layout/components/IframeToggle/index.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<transition-group name="fade-transform" mode="out-in">
|
||||
<inner-link
|
||||
v-for="(item, index) in iframeViews"
|
||||
:key="item.path"
|
||||
:iframeId="'iframe' + index"
|
||||
v-show="$route.path === item.path"
|
||||
:src="item.meta.link"
|
||||
></inner-link>
|
||||
</transition-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import InnerLink from "../InnerLink/index.vue"
|
||||
|
||||
export default {
|
||||
components: { InnerLink },
|
||||
computed: {
|
||||
iframeViews() {
|
||||
return this.$store.state.tagsView.iframeViews
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
47
src/layout/components/InnerLink/index.vue
Normal file
47
src/layout/components/InnerLink/index.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div :style="'height:' + height" v-loading="loading" element-loading-text="正在加载页面,请稍候!">
|
||||
<iframe
|
||||
:id="iframeId"
|
||||
style="width: 100%; height: 100%"
|
||||
:src="src"
|
||||
frameborder="no"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
src: {
|
||||
type: String,
|
||||
default: "/"
|
||||
},
|
||||
iframeId: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
height: document.documentElement.clientHeight - 94.5 + "px;"
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
var _this = this;
|
||||
const iframeId = ("#" + this.iframeId).replace(/\//g, "\\/");
|
||||
const iframe = document.querySelector(iframeId);
|
||||
// iframe页面loading控制
|
||||
if (iframe.attachEvent) {
|
||||
this.loading = true;
|
||||
iframe.attachEvent("onload", function () {
|
||||
_this.loading = false;
|
||||
});
|
||||
} else {
|
||||
this.loading = true;
|
||||
iframe.onload = function () {
|
||||
_this.loading = false;
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
83
src/layout/components/Message/index.vue
Normal file
83
src/layout/components/Message/index.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-popover placement="bottom" width="600" trigger="click">
|
||||
<!-- icon 展示 -->
|
||||
<el-badge slot="reference" :is-dot="unreadCount > 0" type="danger">
|
||||
<svg-icon icon-class="message" @click="getList"/>
|
||||
</el-badge>
|
||||
|
||||
<!-- 弹出列表 -->
|
||||
<el-table v-loading="loading" :data="list">
|
||||
<el-table-column width="120" property="templateNickname" label="发送人" />
|
||||
<el-table-column width="180" property="createTime" label="发送时间">
|
||||
<template v-slot="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="类型" align="center" prop="templateType" width="100">
|
||||
<template v-slot="scope">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" :value="scope.row.templateType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="templateContent" label="内容" />
|
||||
</el-table>
|
||||
|
||||
<!-- 更多 -->
|
||||
<div style="text-align: right; margin-top: 10px">
|
||||
<el-button type="primary" size="mini" @click="goMyList">查看全部</el-button>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {getUnreadNotifyMessageCount, getUnreadNotifyMessageList} from "@/api/system/notify/message";
|
||||
|
||||
export default {
|
||||
name: 'NotifyMessage',
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
loading: false,
|
||||
// 列表
|
||||
list: [],
|
||||
// 未读数量,
|
||||
unreadCount: 0,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 首次加载小红点
|
||||
this.getUnreadCount()
|
||||
// 轮询刷新小红点
|
||||
setInterval(() => {
|
||||
this.getUnreadCount()
|
||||
},1000 * 60 * 2)
|
||||
},
|
||||
methods: {
|
||||
getList: function() {
|
||||
this.loading = true;
|
||||
getUnreadNotifyMessageList().then(response => {
|
||||
this.list = response.data;
|
||||
this.loading = false;
|
||||
// 强制设置 unreadCount 为 0,避免小红点因为轮询太慢,不消除
|
||||
this.unreadCount = 0
|
||||
});
|
||||
},
|
||||
getUnreadCount: function() {
|
||||
getUnreadNotifyMessageCount().then(response => {
|
||||
this.unreadCount = response.data;
|
||||
})
|
||||
},
|
||||
goMyList: function() {
|
||||
this.$router.push({
|
||||
name: 'MyNotifyMessage'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.el-badge__content.is-fixed {
|
||||
top: 10px; /* 保证徽章的位置 */
|
||||
}
|
||||
</style>
|
||||
198
src/layout/components/Navbar.vue
Normal file
198
src/layout/components/Navbar.vue
Normal file
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div class="navbar">
|
||||
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
|
||||
|
||||
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!topNav"/>
|
||||
<top-nav id="topmenu-container" class="topmenu-container" v-if="topNav"/>
|
||||
|
||||
<div class="right-menu">
|
||||
<template v-if="device!=='mobile'">
|
||||
<search id="header-search" class="right-menu-item" />
|
||||
|
||||
<!-- 站内信 -->
|
||||
<!-- <notify-message class="right-menu-item hover-effect" /> -->
|
||||
|
||||
<screenfull id="screenfull" class="right-menu-item hover-effect" />
|
||||
|
||||
<el-tooltip content="布局大小" effect="dark" placement="bottom">
|
||||
<size-select id="size-select" class="right-menu-item hover-effect" />
|
||||
</el-tooltip>
|
||||
|
||||
</template>
|
||||
|
||||
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
|
||||
<div class="avatar-wrapper">
|
||||
<!-- <img :src="avatar" class="user-avatar"> -->
|
||||
<span v-if="nickname" class="user-nickname">{{ nickname }}</span>
|
||||
<i class="el-icon-caret-bottom" />
|
||||
</div>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<router-link to="/user/profile">
|
||||
<el-dropdown-item>个人中心</el-dropdown-item>
|
||||
</router-link>
|
||||
<el-dropdown-item @click.native="setting = true">
|
||||
<span>布局设置</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click.native="logout">
|
||||
<span>退出登录</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import Breadcrumb from '@/components/Breadcrumb'
|
||||
import TopNav from '@/components/TopNav'
|
||||
import Hamburger from '@/components/Hamburger'
|
||||
import Screenfull from '@/components/Screenfull'
|
||||
import SizeSelect from '@/components/SizeSelect'
|
||||
import Search from '@/components/HeaderSearch'
|
||||
import NotifyMessage from '@/layout/components/Message'
|
||||
import {getPath} from "@/utils/ruoyi";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Breadcrumb,
|
||||
TopNav,
|
||||
Hamburger,
|
||||
Screenfull,
|
||||
SizeSelect,
|
||||
Search,
|
||||
NotifyMessage
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
'sidebar',
|
||||
'avatar',
|
||||
'nickname',
|
||||
'device'
|
||||
]),
|
||||
setting: {
|
||||
get() {
|
||||
return this.$store.state.settings.showSettings
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'showSettings',
|
||||
value: val
|
||||
})
|
||||
}
|
||||
},
|
||||
topNav: {
|
||||
get() {
|
||||
return this.$store.state.settings.topNav
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleSideBar() {
|
||||
this.$store.dispatch('app/toggleSideBar')
|
||||
},
|
||||
async logout() {
|
||||
this.$modal.confirm('确定注销并退出系统吗?', '提示').then(() => {
|
||||
this.$store.dispatch('LogOut').then(() => {
|
||||
location.href = getPath('/index');
|
||||
})
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar {
|
||||
height: 56px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
||||
|
||||
.hamburger-container {
|
||||
line-height: 52px;
|
||||
height: 100%;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
transition: background .3s;
|
||||
-webkit-tap-highlight-color:transparent;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, .025)
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-container {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.topmenu-container {
|
||||
position: absolute;
|
||||
left: 50px;
|
||||
}
|
||||
|
||||
.errLog-container {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.right-menu {
|
||||
float: right;
|
||||
height: 100%;
|
||||
line-height: 56px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.right-menu-item {
|
||||
display: inline-block;
|
||||
padding: 0 8px;
|
||||
height: 100%;
|
||||
font-size: 18px;
|
||||
color: #5a5e66;
|
||||
vertical-align: text-bottom;
|
||||
|
||||
&.hover-effect {
|
||||
cursor: pointer;
|
||||
transition: background .3s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, .025)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
margin-right: 30px;
|
||||
|
||||
.avatar-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.user-nickname{
|
||||
margin-left: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.el-icon-caret-bottom {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 25px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
257
src/layout/components/Settings/index.vue
Normal file
257
src/layout/components/Settings/index.vue
Normal file
@@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<div class="drawer-container">
|
||||
<div>
|
||||
<div class="setting-drawer-content">
|
||||
<div class="setting-drawer-title">
|
||||
<h3 class="drawer-title">主题风格设置</h3>
|
||||
</div>
|
||||
<div class="setting-drawer-block-checbox">
|
||||
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
|
||||
<img src="@/assets/images/dark.svg" alt="dark">
|
||||
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
|
||||
<i aria-label="图标: check" class="anticon anticon-check">
|
||||
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true"
|
||||
focusable="false" class="">
|
||||
<path
|
||||
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
|
||||
</svg>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
|
||||
<img src="@/assets/images/light.svg" alt="light">
|
||||
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
|
||||
<i aria-label="图标: check" class="anticon anticon-check">
|
||||
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true"
|
||||
focusable="false" class="">
|
||||
<path
|
||||
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
|
||||
</svg>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>主题颜色</span>
|
||||
<theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider/>
|
||||
|
||||
<h3 class="drawer-title">系统布局配置</h3>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>开启 TopNav</span>
|
||||
<el-switch v-model="topNav" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>开启 Tags-Views</span>
|
||||
<el-switch v-model="tagsView" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>固定 Header</span>
|
||||
<el-switch v-model="fixedHeader" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>显示 Logo</span>
|
||||
<el-switch v-model="sidebarLogo" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>动态标题</span>
|
||||
<el-switch v-model="dynamicTitle" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<el-divider/>
|
||||
|
||||
<el-button size="small" type="primary" plain icon="el-icon-document-add" @click="saveSetting">保存配置</el-button>
|
||||
<el-button size="small" plain icon="el-icon-refresh" @click="resetSetting">重置配置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ThemePicker from '@/components/ThemePicker'
|
||||
|
||||
export default {
|
||||
components: { ThemePicker },
|
||||
data() {
|
||||
return {
|
||||
theme: this.$store.state.settings.theme,
|
||||
sideTheme: this.$store.state.settings.sideTheme
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
fixedHeader: {
|
||||
get() {
|
||||
return this.$store.state.settings.fixedHeader
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'fixedHeader',
|
||||
value: val
|
||||
})
|
||||
}
|
||||
},
|
||||
topNav: {
|
||||
get() {
|
||||
return this.$store.state.settings.topNav
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'topNav',
|
||||
value: val
|
||||
})
|
||||
if (!val) {
|
||||
this.$store.dispatch('app/toggleSideBarHide', false);
|
||||
this.$store.commit("SET_SIDEBAR_ROUTERS", this.$store.state.permission.defaultRoutes);
|
||||
}
|
||||
}
|
||||
},
|
||||
tagsView: {
|
||||
get() {
|
||||
return this.$store.state.settings.tagsView
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'tagsView',
|
||||
value: val
|
||||
})
|
||||
}
|
||||
},
|
||||
sidebarLogo: {
|
||||
get() {
|
||||
return this.$store.state.settings.sidebarLogo
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'sidebarLogo',
|
||||
value: val
|
||||
})
|
||||
}
|
||||
},
|
||||
dynamicTitle: {
|
||||
get() {
|
||||
return this.$store.state.settings.dynamicTitle
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'dynamicTitle',
|
||||
value: val
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
themeChange(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'theme',
|
||||
value: val
|
||||
})
|
||||
this.theme = val;
|
||||
},
|
||||
handleTheme(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'sideTheme',
|
||||
value: val
|
||||
})
|
||||
this.sideTheme = val;
|
||||
},
|
||||
saveSetting() {
|
||||
this.$modal.loading("正在保存到本地,请稍候...");
|
||||
this.$cache.local.set(
|
||||
"layout-setting",
|
||||
`{
|
||||
"topNav":${this.topNav},
|
||||
"tagsView":${this.tagsView},
|
||||
"fixedHeader":${this.fixedHeader},
|
||||
"sidebarLogo":${this.sidebarLogo},
|
||||
"dynamicTitle":${this.dynamicTitle},
|
||||
"sideTheme":"${this.sideTheme}",
|
||||
"theme":"${this.theme}"
|
||||
}`
|
||||
);
|
||||
setTimeout(this.$modal.closeLoading(), 1000)
|
||||
},
|
||||
resetSetting() {
|
||||
this.$modal.loading("正在清除设置缓存并刷新,请稍候...");
|
||||
this.$cache.local.remove("layout-setting")
|
||||
setTimeout("window.location.reload()", 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.setting-drawer-content {
|
||||
.setting-drawer-title {
|
||||
margin-bottom: 12px;
|
||||
color: rgba(0, 0, 0, .85);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.setting-drawer-block-checbox {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.setting-drawer-block-checbox-item {
|
||||
position: relative;
|
||||
margin-right: 16px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.setting-drawer-block-checbox-selectIcon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 15px;
|
||||
padding-left: 24px;
|
||||
color: #1890ff;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-container {
|
||||
padding: 24px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
|
||||
.drawer-title {
|
||||
margin-bottom: 12px;
|
||||
color: rgba(0, 0, 0, .85);
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.drawer-item {
|
||||
color: rgba(0, 0, 0, .65);
|
||||
font-size: 14px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.drawer-switch {
|
||||
float: right
|
||||
}
|
||||
}
|
||||
</style>
|
||||
25
src/layout/components/Sidebar/FixiOSBug.js
Normal file
25
src/layout/components/Sidebar/FixiOSBug.js
Normal file
@@ -0,0 +1,25 @@
|
||||
export default {
|
||||
computed: {
|
||||
device() {
|
||||
return this.$store.state.app.device
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// In order to fix the click on menu on the ios device will trigger the mouseleave bug
|
||||
this.fixBugIniOS()
|
||||
},
|
||||
methods: {
|
||||
fixBugIniOS() {
|
||||
const $subMenu = this.$refs.subMenu
|
||||
if ($subMenu) {
|
||||
const handleMouseleave = $subMenu.handleMouseleave
|
||||
$subMenu.handleMouseleave = (e) => {
|
||||
if (this.device === 'mobile') {
|
||||
return
|
||||
}
|
||||
handleMouseleave(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/layout/components/Sidebar/Item.vue
Normal file
33
src/layout/components/Sidebar/Item.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'MenuItem',
|
||||
functional: true,
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
render(h, context) {
|
||||
const { icon, title } = context.props
|
||||
const vnodes = []
|
||||
|
||||
if (icon) {
|
||||
vnodes.push(<svg-icon icon-class={icon}/>)
|
||||
}
|
||||
|
||||
if (title) {
|
||||
if (title.length > 5) {
|
||||
vnodes.push(<span slot='title' title={(title)}>{(title)}</span>)
|
||||
} else {
|
||||
vnodes.push(<span slot='title'>{(title)}</span>)
|
||||
}
|
||||
}
|
||||
return vnodes
|
||||
}
|
||||
}
|
||||
</script>
|
||||
43
src/layout/components/Sidebar/Link.vue
Normal file
43
src/layout/components/Sidebar/Link.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<component :is="type" v-bind="linkProps(to)">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { isExternal } from '@/utils/validate'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
to: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isExternal() {
|
||||
return isExternal(this.to)
|
||||
},
|
||||
type() {
|
||||
if (this.isExternal) {
|
||||
return 'a'
|
||||
}
|
||||
return 'router-link'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
linkProps(to) {
|
||||
if (this.isExternal) {
|
||||
return {
|
||||
href: to,
|
||||
target: '_blank',
|
||||
rel: 'noopener'
|
||||
}
|
||||
}
|
||||
return {
|
||||
to: to
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
126
src/layout/components/Sidebar/Logo.vue
Normal file
126
src/layout/components/Sidebar/Logo.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div
|
||||
class="sidebar-logo-container"
|
||||
:class="{ collapse: collapse }"
|
||||
:style="{
|
||||
backgroundColor:
|
||||
sideTheme === 'theme-dark' ? '#1f2d3d' : variables.menuLightBackground,
|
||||
}">
|
||||
<!-- sideTheme === 'theme-dark'
|
||||
? variables.menuBackground
|
||||
: variables.menuLightBackground, -->
|
||||
<transition name="sidebarLogoFade">
|
||||
<router-link
|
||||
v-if="collapse"
|
||||
key="collapse"
|
||||
class="sidebar-logo-link"
|
||||
to="/">
|
||||
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
||||
<h1
|
||||
v-else
|
||||
class="sidebar-title"
|
||||
:style="{
|
||||
color:
|
||||
sideTheme === 'theme-dark'
|
||||
? variables.logoTitleColor
|
||||
: variables.logoLightTitleColor,
|
||||
}">
|
||||
{{ title }}
|
||||
</h1>
|
||||
</router-link>
|
||||
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||
<img v-if="logo" :src="logo" class="sidebar-logo" />
|
||||
<h1
|
||||
class="sidebar-title"
|
||||
:style="{
|
||||
color:
|
||||
sideTheme === 'theme-dark'
|
||||
? variables.logoTitleColor
|
||||
: variables.logoLightTitleColor,
|
||||
}">
|
||||
{{ title }}
|
||||
</h1>
|
||||
</router-link>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import logoImg from '@/assets/logo/logo.png';
|
||||
import variables from '@/assets/styles/variables.scss';
|
||||
|
||||
export default {
|
||||
name: 'SidebarLogo',
|
||||
props: {
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
variables() {
|
||||
return variables;
|
||||
},
|
||||
sideTheme() {
|
||||
return this.$store.state.settings.sideTheme;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: '中建材智能化院',
|
||||
logo: logoImg,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sidebarLogoFade-enter-active {
|
||||
transition: opacity 1.5s;
|
||||
}
|
||||
|
||||
.sidebarLogoFade-enter,
|
||||
.sidebarLogoFade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sidebar-logo-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 56px;
|
||||
line-height: 56px;
|
||||
background: #1445cc;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
|
||||
& .sidebar-logo-link {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
& .sidebar-logo {
|
||||
width: 32px;
|
||||
height: 40px;
|
||||
vertical-align: middle;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
& .sidebar-title {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
line-height: 50px;
|
||||
font-size: 18px;
|
||||
letter-spacing: 1px;
|
||||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&.collapse {
|
||||
.sidebar-logo {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
96
src/layout/components/Sidebar/SidebarItem.vue
Normal file
96
src/layout/components/Sidebar/SidebarItem.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div v-if="!item.hidden">
|
||||
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
|
||||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
|
||||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
|
||||
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
</template>
|
||||
|
||||
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
|
||||
<template slot="title">
|
||||
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
|
||||
</template>
|
||||
<sidebar-item
|
||||
v-for="(child, index) in item.children"
|
||||
:key="child.path + index"
|
||||
:is-nest="true"
|
||||
:item="child"
|
||||
:base-path="resolvePath(child.path)"
|
||||
class="nest-menu"
|
||||
/>
|
||||
</el-submenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import path from 'path'
|
||||
import { isExternal } from '@/utils/validate'
|
||||
import Item from './Item'
|
||||
import AppLink from './Link'
|
||||
import FixiOSBug from './FixiOSBug'
|
||||
|
||||
export default {
|
||||
name: 'SidebarItem',
|
||||
components: { Item, AppLink },
|
||||
mixins: [FixiOSBug],
|
||||
props: {
|
||||
// route object
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isNest: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
this.onlyOneChild = null
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
hasOneShowingChild(children = [], parent) {
|
||||
if (!children) {
|
||||
children = [];
|
||||
}
|
||||
const showingChildren = children.filter(item => {
|
||||
if (item.hidden) {
|
||||
return false
|
||||
} else {
|
||||
// Temp set(will be used if only has one showing child)
|
||||
this.onlyOneChild = item
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// When there is only one child router, the child router is displayed by default
|
||||
if (showingChildren.length === 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Show parent if there are no child router to display
|
||||
if (showingChildren.length === 0) {
|
||||
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
resolvePath(routePath) {
|
||||
if (isExternal(routePath)) {
|
||||
return routePath
|
||||
}
|
||||
if (isExternal(this.basePath)) {
|
||||
return this.basePath
|
||||
}
|
||||
return path.resolve(this.basePath, routePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
71
src/layout/components/Sidebar/index.vue
Normal file
71
src/layout/components/Sidebar/index.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{ 'has-logo': showLogo }"
|
||||
:style="{
|
||||
backgroundColor:
|
||||
settings.sideTheme === 'theme-dark'
|
||||
? variables.menuBackground
|
||||
: variables.menuLightBackground,
|
||||
}">
|
||||
<logo v-if="showLogo" :collapse="isCollapse" />
|
||||
<el-scrollbar :class="settings.sideTheme" wrap-class="scrollbar-wrapper">
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
:collapse="isCollapse"
|
||||
:background-color="
|
||||
settings.sideTheme === 'theme-dark'
|
||||
? variables.menuBackground
|
||||
: variables.menuLightBackground
|
||||
"
|
||||
:text-color="
|
||||
settings.sideTheme === 'theme-dark'
|
||||
? variables.menuColor
|
||||
: variables.menuLightColor
|
||||
"
|
||||
:unique-opened="true"
|
||||
:active-text-color="settings.theme"
|
||||
:collapse-transition="false"
|
||||
mode="vertical">
|
||||
<!-- 根据 sidebarRouters 路由,生成菜单 -->
|
||||
<sidebar-item
|
||||
v-for="(route, index) in sidebarRouters"
|
||||
:key="route.path + index"
|
||||
:item="route"
|
||||
:base-path="route.path" />
|
||||
</el-menu>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapState } from 'vuex';
|
||||
import Logo from './Logo';
|
||||
import SidebarItem from './SidebarItem';
|
||||
import variables from '@/assets/styles/variables.scss';
|
||||
|
||||
export default {
|
||||
components: { SidebarItem, Logo },
|
||||
computed: {
|
||||
...mapState(['settings']),
|
||||
...mapGetters(['sidebarRouters', 'sidebar']),
|
||||
activeMenu() {
|
||||
const route = this.$route;
|
||||
const { meta, path } = route;
|
||||
// if set path, the sidebar will highlight the path you set
|
||||
if (meta.activeMenu) {
|
||||
return meta.activeMenu;
|
||||
}
|
||||
return path;
|
||||
},
|
||||
showLogo() {
|
||||
return this.$store.state.settings.sidebarLogo;
|
||||
},
|
||||
variables() {
|
||||
return variables;
|
||||
},
|
||||
isCollapse() {
|
||||
return !this.sidebar.opened;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
99
src/layout/components/TagsView/ScrollPane.vue
Normal file
99
src/layout/components/TagsView/ScrollPane.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
|
||||
<slot />
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const tagAndTagSpacing = 4 // tagAndTagSpacing
|
||||
|
||||
export default {
|
||||
name: 'ScrollPane',
|
||||
data() {
|
||||
return {
|
||||
left: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
scrollWrapper() {
|
||||
return this.$refs.scrollContainer.$refs.wrap
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
|
||||
},
|
||||
methods: {
|
||||
handleScroll(e) {
|
||||
const eventDelta = e.wheelDelta || -e.deltaY * 40
|
||||
const $scrollWrapper = this.scrollWrapper
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
|
||||
},
|
||||
emitScroll() {
|
||||
this.$emit('scroll')
|
||||
},
|
||||
moveToTarget(currentTag) {
|
||||
const $container = this.$refs.scrollContainer.$el
|
||||
const $containerWidth = $container.offsetWidth
|
||||
const $scrollWrapper = this.scrollWrapper
|
||||
const tagList = this.$parent.$refs.tag
|
||||
|
||||
let firstTag = null
|
||||
let lastTag = null
|
||||
|
||||
// find first tag and last tag
|
||||
if (tagList.length > 0) {
|
||||
firstTag = tagList[0]
|
||||
lastTag = tagList[tagList.length - 1]
|
||||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = 0
|
||||
} else if (lastTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
|
||||
} else {
|
||||
// find preTag and nextTag
|
||||
const currentIndex = tagList.findIndex(item => item === currentTag)
|
||||
const prevTag = tagList[currentIndex - 1]
|
||||
const nextTag = tagList[currentIndex + 1]
|
||||
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
|
||||
|
||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
|
||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.scroll-container {
|
||||
background: #f9f9f9;
|
||||
box-shadow: inset 0 0 8px 1px #e8e8e8;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
:deep(.el-scrollbar__bar) {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-scrollbar__wrap) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
366
src/layout/components/TagsView/index.vue
Normal file
366
src/layout/components/TagsView/index.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div id="tags-view-container" class="tags-view-container">
|
||||
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
|
||||
<router-link
|
||||
v-for="tag in visitedViews"
|
||||
ref="tag"
|
||||
:key="tag.path"
|
||||
:class="isActive(tag) ? 'active' : ''"
|
||||
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
||||
tag="span"
|
||||
class="tags-view-item"
|
||||
:style="activeStyle(tag)"
|
||||
@click.middle.native="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
||||
@contextmenu.prevent.native="openMenu(tag, $event)">
|
||||
{{ tag.title }}
|
||||
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
|
||||
</router-link>
|
||||
</scroll-pane>
|
||||
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
|
||||
<li @click="refreshSelectedTag(selectedTag)">
|
||||
<i class="el-icon-refresh-right"></i>
|
||||
刷新页面
|
||||
</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||
<i class="el-icon-close"></i>
|
||||
关闭当前
|
||||
</li>
|
||||
<li @click="closeOthersTags">
|
||||
<i class="el-icon-circle-close"></i>
|
||||
关闭其他
|
||||
</li>
|
||||
<li v-if="!isFirstView()" @click="closeLeftTags">
|
||||
<i class="el-icon-back"></i>
|
||||
关闭左侧
|
||||
</li>
|
||||
<li v-if="!isLastView()" @click="closeRightTags">
|
||||
<i class="el-icon-right"></i>
|
||||
关闭右侧
|
||||
</li>
|
||||
<li @click="closeAllTags(selectedTag)">
|
||||
<i class="el-icon-circle-close"></i>
|
||||
全部关闭
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScrollPane from './ScrollPane';
|
||||
import path from 'path';
|
||||
|
||||
export default {
|
||||
components: { ScrollPane },
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
top: 0,
|
||||
left: 0,
|
||||
selectedTag: {},
|
||||
affixTags: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
visitedViews() {
|
||||
return this.$store.state.tagsView.visitedViews;
|
||||
},
|
||||
routes() {
|
||||
return this.$store.state.permission.routes;
|
||||
},
|
||||
theme() {
|
||||
return this.$store.state.settings.theme;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route() {
|
||||
this.addTags();
|
||||
this.moveToCurrentTag();
|
||||
},
|
||||
visible(value) {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', this.closeMenu);
|
||||
} else {
|
||||
document.body.removeEventListener('click', this.closeMenu);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initTags();
|
||||
this.addTags();
|
||||
},
|
||||
methods: {
|
||||
isActive(route) {
|
||||
return route.path === this.$route.path;
|
||||
},
|
||||
activeStyle(tag) {
|
||||
if (!this.isActive(tag)) return {};
|
||||
return {
|
||||
'background-color': this.theme,
|
||||
'border-color': this.theme,
|
||||
};
|
||||
},
|
||||
isAffix(tag) {
|
||||
return tag.meta && tag.meta.affix;
|
||||
},
|
||||
isFirstView() {
|
||||
try {
|
||||
return this.selectedTag.fullPath === this.visitedViews[1].fullPath || this.selectedTag.fullPath === '/index';
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isLastView() {
|
||||
try {
|
||||
return this.selectedTag.fullPath === this.visitedViews[this.visitedViews.length - 1].fullPath;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
filterAffixTags(routes, basePath = '/') {
|
||||
let tags = [];
|
||||
routes.forEach((route) => {
|
||||
if (route.meta && route.meta.affix) {
|
||||
const tagPath = path.resolve(basePath, route.path);
|
||||
tags.push({
|
||||
fullPath: tagPath,
|
||||
path: tagPath,
|
||||
name: route.name,
|
||||
meta: { ...route.meta },
|
||||
});
|
||||
}
|
||||
if (route.children) {
|
||||
const tempTags = this.filterAffixTags(route.children, route.path);
|
||||
if (tempTags.length >= 1) {
|
||||
tags = [...tags, ...tempTags];
|
||||
}
|
||||
}
|
||||
});
|
||||
return tags;
|
||||
},
|
||||
initTags() {
|
||||
const affixTags = (this.affixTags = this.filterAffixTags(this.routes));
|
||||
for (const tag of affixTags) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
this.$store.dispatch('tagsView/addVisitedView', tag);
|
||||
}
|
||||
}
|
||||
},
|
||||
addTags() {
|
||||
const { name } = this.$route;
|
||||
if (name) {
|
||||
this.$store.dispatch('tagsView/addView', this.$route);
|
||||
if (this.$route.meta.link) {
|
||||
this.$store.dispatch('tagsView/addIframeView', this.$route);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
moveToCurrentTag() {
|
||||
const tags = this.$refs.tag;
|
||||
this.$nextTick(() => {
|
||||
for (const tag of tags) {
|
||||
if (tag.to.path === this.$route.path) {
|
||||
this.$refs.scrollPane.moveToTarget(tag);
|
||||
// when query is different then update
|
||||
if (tag.to.fullPath !== this.$route.fullPath) {
|
||||
this.$store.dispatch('tagsView/updateVisitedView', this.$route);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
refreshSelectedTag(view) {
|
||||
this.$tab.refreshPage(view);
|
||||
if (this.$route.meta.link) {
|
||||
this.$store.dispatch('tagsView/delIframeView', this.$route);
|
||||
}
|
||||
},
|
||||
closeSelectedTag(view) {
|
||||
this.$tab.closePage(view).then(({ visitedViews }) => {
|
||||
if (this.isActive(view)) {
|
||||
this.toLastView(visitedViews, view);
|
||||
}
|
||||
});
|
||||
},
|
||||
closeRightTags() {
|
||||
this.$tab.closeRightPage(this.selectedTag).then((visitedViews) => {
|
||||
if (!visitedViews.find((i) => i.fullPath === this.$route.fullPath)) {
|
||||
this.toLastView(visitedViews);
|
||||
}
|
||||
});
|
||||
},
|
||||
closeLeftTags() {
|
||||
this.$tab.closeLeftPage(this.selectedTag).then((visitedViews) => {
|
||||
if (!visitedViews.find((i) => i.fullPath === this.$route.fullPath)) {
|
||||
this.toLastView(visitedViews);
|
||||
}
|
||||
});
|
||||
},
|
||||
closeOthersTags() {
|
||||
this.$router.push(this.selectedTag).catch(() => {});
|
||||
this.$tab.closeOtherPage(this.selectedTag).then(() => {
|
||||
this.moveToCurrentTag();
|
||||
});
|
||||
},
|
||||
closeAllTags(view) {
|
||||
this.$tab.closeAllPage().then(({ visitedViews }) => {
|
||||
if (this.affixTags.some((tag) => tag.path === this.$route.path)) {
|
||||
return;
|
||||
}
|
||||
this.toLastView(visitedViews, view);
|
||||
});
|
||||
},
|
||||
toLastView(visitedViews, view) {
|
||||
const latestView = visitedViews.slice(-1)[0];
|
||||
if (latestView) {
|
||||
this.$router.push(latestView.fullPath);
|
||||
} else {
|
||||
// now the default is to redirect to the home page if there is no tags-view,
|
||||
// you can adjust it according to your needs.
|
||||
if (view.name === 'Dashboard') {
|
||||
// to reload home page
|
||||
this.$router.replace({ path: '/redirect' + view.fullPath });
|
||||
} else {
|
||||
this.$router.push('/');
|
||||
}
|
||||
}
|
||||
},
|
||||
openMenu(tag, e) {
|
||||
const menuMinWidth = 105;
|
||||
const offsetLeft = this.$el.getBoundingClientRect().left; // container margin left
|
||||
const offsetWidth = this.$el.offsetWidth; // container width
|
||||
const maxLeft = offsetWidth - menuMinWidth; // left boundary
|
||||
const left = e.clientX - offsetLeft + 15; // 15: margin right
|
||||
|
||||
if (left > maxLeft) {
|
||||
this.left = maxLeft;
|
||||
} else {
|
||||
this.left = left;
|
||||
}
|
||||
|
||||
this.top = e.clientY;
|
||||
this.visible = true;
|
||||
this.selectedTag = tag;
|
||||
},
|
||||
closeMenu() {
|
||||
this.visible = false;
|
||||
},
|
||||
handleScroll() {
|
||||
this.closeMenu();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-view-container {
|
||||
height: 42px;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #d8dce5;
|
||||
border-top: 1px solid #d8dce5;
|
||||
|
||||
// box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
border: 1px solid #d8dce5;
|
||||
color: #495060;
|
||||
background: #fff;
|
||||
padding: 0 8px 0 12px;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
margin-left: 4px;
|
||||
border-radius: 0;
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 15px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #42b983;
|
||||
color: #fff;
|
||||
border-color: #42b983;
|
||||
// &::before {
|
||||
// content: '';
|
||||
// background: #fff;
|
||||
// display: inline-block;
|
||||
// width: 8px;
|
||||
// height: 8px;
|
||||
// border-radius: 50%;
|
||||
// position: relative;
|
||||
// margin-right: 2px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contextmenu {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
z-index: 3000;
|
||||
position: absolute;
|
||||
list-style-type: none;
|
||||
padding: 5px 0;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #333;
|
||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 7px 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
//reset element css of el-icon-close
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
.el-icon-close {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
// transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
// transform-origin: 100% 50%;
|
||||
|
||||
&:before {
|
||||
transform: scale(0.8);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #e0564c;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
.el-icon-close {
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
5
src/layout/components/index.js
Normal file
5
src/layout/components/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as AppMain } from './AppMain'
|
||||
export { default as Navbar } from './Navbar'
|
||||
export { default as Settings } from './Settings'
|
||||
export { default as Sidebar } from './Sidebar/index.vue'
|
||||
export { default as TagsView } from './TagsView/index.vue'
|
||||
Reference in New Issue
Block a user