This commit is contained in:
gtz
2023-07-26 09:54:50 +08:00
commit 0fca3e1ec4
677 changed files with 80083 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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)
}
}
}
}
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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'