Commit 1a092ae5 by zhaopanyu

zpy

parent 9196f89a
# 页面标题 # 页面标题
VUE_APP_TITLE = 若依管理系统 VUE_APP_TITLE = 系统
# 开发环境配置 # 开发环境配置
ENV = 'development' ENV = 'development'
......
# 页面标题 # 页面标题
VUE_APP_TITLE = 若依管理系统 VUE_APP_TITLE = 系统
# 生产环境配置 # 生产环境配置
ENV = 'production' ENV = 'production'
......
# 页面标题 # 页面标题
VUE_APP_TITLE = 若依管理系统 VUE_APP_TITLE = 系统
BABEL_ENV = production BABEL_ENV = production
......
{ {
"name": "ruoyi", "name": "ruoyi",
"version": "3.8.9", "version": "3.8.9",
"description": "若依管理系统", "description": "系统",
"author": "若依", "author": "若依",
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
......
<template>
<div class="int-slider">
<div class="slider-header">
<span class="slider-label">{{ label }}</span>
</div>
<div class="slider-content">
<el-slider
v-model="sliderValue"
:min="parseInt(min)"
:max="parseInt(max)"
@input="handleInput"
></el-slider>
<div class="slider-append">
<slot name="append"></slot>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'IntSlider',
props: {
label: {
type: String,
default: ''
},
value: {
type: [Number, String],
default: 0
},
min: {
type: [Number, String],
default: 0
},
max: {
type: [Number, String],
default: 100
}
},
data() {
return {
sliderValue: parseFloat(this.value)
}
},
methods: {
handleInput(value) {
this.$emit('input', value);
}
},
watch: {
value(val) {
this.sliderValue = parseFloat(val);
}
}
}
</script>
<style scoped>
.int-slider {
margin-bottom: 15px;
}
.slider-header {
margin-bottom: 8px;
}
.slider-label {
font-weight: bold;
font-size: 14px;
}
.slider-content {
display: flex;
align-items: center;
}
.slider-append {
margin-left: 15px;
min-width: 40px;
text-align: center;
}
</style>
\ No newline at end of file
<template>
<v-row
class="px-1"
no-gutters
justify="space-between"
style="width: 100%"
>
<v-select
class="pt-0 mt-0"
:value="pattern"
:items="Object.keys(patternList)"
background-color="white"
hide-details
required
light
@input="updatePattern"
>
<template v-slot:selection="{item}">
<div
v-if="item !== 'None'"
:title="item"
style="width: 100%; height: 16px;"
:style="{background: '5px no-repeat url(' + patternList[item] + ')'}"
/>
<div
v-else
class="ml-2"
>
None
</div>
</template>
<template v-slot:item="{item}">
<div
v-if="item !== 'None'"
:title="item"
style="width: 100%; height: 16px;"
:style="{background: '5px no-repeat url(' + patternList[item] + ')'}"
/>
{{ item === 'None' ? 'None' : '' }}
</template>
<template v-slot:append-outer>
<v-menu
transition="scale-transition"
:close-on-content-click="false"
offset-x
>
<template v-slot:activator="{ on }">
<v-badge
class="eyedropper-badge mr-1"
icon="mdi-eyedropper"
bottom
overlap
:bordered="isEyeDropperActive"
@click.native="setEyeDropperEnabled(!isEyeDropperActive)"
>
<v-btn
ref="eyeButton"
:color="colorInfo.rgb"
retain-focus-on-click
fab
height="25"
width="25"
@click.native.stop
v-on="on"
/>
</v-badge>
</template>
<v-color-picker
:value="colorInfo.rgba"
:swatches="SWATCHES"
show-swatches
@input="updateColor"
/>
</v-menu>
</template>
</v-select>
</v-row>
</template>
<script>
import Vuetify, {VRow, VBtn, VMenu, VBadge, VColorPicker} from 'vuetify/lib';
import {RgbaColor} from '@int/geotoolkit/util/RgbaColor';
const SWATCHES = [
['#FF0000', '#AA0000', '#550000'],
['#FFFF00', '#AAAA00', '#555500'],
['#00FF00', '#00AA00', '#005500'],
['#00FFFF', '#00AAAA', '#005555'],
['#0000FF', '#0000AA', '#000055']
];
export default {
name: 'ColorInput',
components: {VRow, VBtn, VMenu, VBadge, VColorPicker},
props: {
name: {
type: String,
default: ''
},
color: {
type: String,
default: '#000000'
},
pattern: {
type: String,
default: ''
},
patternList: {
type: Object,
default: function () {
return null;
}
}
},
data () {
return {
isEyeDropperActive: false,
SWATCHES
};
},
computed: {
colorInfo: function () {
const rgba = new RgbaColor(this.color);
return {
rgb: rgba.toHex(),
rgba: rgba.toHex(true)
};
}
},
mounted: function () {
this._onKeyDown = this.setEyeDropperEnabled.bind(this, false);
window.addEventListener('keydown', this._onKeyDown);
},
destroyed: function () {
this.setEyeDropperEnabled(false);
window.removeEventListener('keydown', this._onKeyDown);
},
methods: {
updateColor (color) {
this.$emit('changed', color, this.pattern);
},
updatePattern (value) {
this.$emit('changed', this.colorInfo.rgba, value);
},
isActive () {
return this.isEyeDropperActive;
},
setEyeDropperEnabled (value) {
if (this.isEyeDropperActive === value) return;
this.isEyeDropperActive = value;
this.$emit('eyedropper-changed', this, value);
}
}
};
</script>
<style scoped>
.eyedropper-badge >>> i.mdi-eyedropper {
color: white !important;
cursor: pointer;
}
.v-badge.eyedropper-badge >>> .v-badge__badge:after {
border-color: red;
}
</style>
<template>
<div class="color-input">
<div class="color-input-header">
<span class="color-input-label">{{ name }}</span>
</div>
<div class="color-input-content">
<div class="color-picker-container">
<el-color-picker
v-model="colorValue"
show-alpha
@change="colorChanged"
></el-color-picker>
</div>
<div class="pattern-selector">
<el-select v-model="patternValue" placeholder="模式" @change="patternChanged">
<el-option
v-for="(pattern, index) in patternList"
:key="index"
:label="pattern"
:value="pattern">
</el-option>
</el-select>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ColorInput',
props: {
name: {
type: String,
default: 'Color'
},
color: {
type: String,
default: '#000000'
},
pattern: {
type: [String, Array],
default: null
},
patternList: {
type: Array,
default: () => []
}
},
data() {
return {
colorValue: this.color,
patternValue: this.pattern
}
},
methods: {
colorChanged(value) {
this.$emit('changed', {
color: value,
pattern: this.patternValue
});
},
patternChanged(value) {
this.$emit('changed', {
color: this.colorValue,
pattern: value
});
},
updateEyeDropper(color) {
this.colorValue = color;
this.$emit('eyedropper-changed', color);
}
},
watch: {
color(val) {
this.colorValue = val;
},
pattern(val) {
this.patternValue = val;
}
}
}
</script>
<style scoped>
.color-input {
margin-bottom: 15px;
}
.color-input-header {
margin-bottom: 8px;
}
.color-input-label {
font-weight: bold;
font-size: 14px;
}
.color-input-content {
display: flex;
align-items: center;
}
.color-picker-container {
margin-right: 15px;
}
.pattern-selector {
flex: 1;
}
</style>
\ No newline at end of file
<template>
<div class="context-menu" v-show="visible" :style="style">
<ul class="menu-list">
<li class="menu-item" @click="handleDelete">
<i class="el-icon-delete"></i>
删除
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'ContextMenu',
data() {
return {
visible: false,
x: 0,
y: 0
}
},
computed: {
style() {
return {
left: this.x + 'px',
top: this.y + 'px'
}
}
},
methods: {
show(x, y) {
this.x = x
this.y = y
this.visible = true
document.addEventListener('click', this.hide)
},
hide() {
this.visible = false
document.removeEventListener('click', this.hide)
},
handleDelete() {
this.$emit('delete')
this.hide()
}
},
beforeDestroy() {
document.removeEventListener('click', this.hide)
}
}
</script>
<style scoped>
.context-menu {
position: fixed;
background: white;
border: 1px solid #e4e7ed;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 2000;
}
.menu-list {
list-style: none;
padding: 0;
margin: 0;
}
.menu-item {
padding: 8px 16px;
cursor: pointer;
display: flex;
align-items: center;
font-size: 14px;
}
.menu-item:hover {
background-color: #f5f7fa;
}
.menu-item i {
margin-right: 8px;
}
</style>
\ No newline at end of file
<template>
<v-dialog
v-bind="$props"
v-on="$listeners"
>
<v-card :style="absolute ? absoluteStyle : ''">
<v-card-title
class="headline"
>
{{ title }}
</v-card-title>
<v-card-text>
<v-container>
<slot />
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
v-if="displayOk"
text
@click="$emit('ok')"
>
Ok
</v-btn>
<v-btn
text
@click="$emit('input', false)"
>
Close
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
// TODO: vuetify will implement the feature of setting custom position for dialog, current solution is temporary
// https://github.com/vuetifyjs/vuetify/issues/4807
export default {
name: 'IntDialog',
props: {
value: {
type: Boolean,
default: false
},
title: {
type: String,
default: 'Title'
},
width: {
type: String,
default: '600px'
},
minWidth: {
type: String,
default: '600px'
},
maxWidth: {
type: String,
default: '600px'
},
absolute: {
type: Boolean,
default: false
},
position: {
type: Object,
default: () => {}
},
displayOk: {
type: Boolean,
default: false
}
},
computed: {
absoluteStyle: function () {
return {
'position': 'absolute',
'top': this.position.top,
'bottom': this.position.bottom,
'right': this.position.right,
'left': this.position.left,
'max-width': this.maxWidth,
'min-width': this.minWidth,
'width': this.width
};
}
}
};
</script>
<style scoped>
>>>.v-dialog {
background-color: white;
}
>>>.v-card__text {
padding-right: 10px!important;
padding-left: 10px!important;
}
</style>
<template>
<v-expansion-panel>
<v-expansion-panel-header
class="int-expansion-header"
expand-icon="mdi-menu-down"
>
{{ title }}
</v-expansion-panel-header>
<v-expansion-panel-content>
<slot />
</v-expansion-panel-content>
</v-expansion-panel>
</template>
<script>
import {VExpansionPanel} from 'vuetify/lib';
export default {
name: 'IntExpansionPanel',
components: {VExpansionPanel},
props: {
title: {
type: String,
default: ''
}
}
};
</script>
<style scoped>
.int-expansion-header {
padding: 4px 8px 4px 12px !important;
flex-direction: row;
justify-content: flex-end;
min-height: 32px !important;
}
.theme--dark.v-expansion-panels .v-expansion-panel {
background-color: #272727;
}
</style>
<template>
<div class="int-expansion-panels">
<el-collapse v-model="activeNames">
<slot />
</el-collapse>
</div>
</template>
<script>
export default {
name: 'IntExpansionPanels',
props: {
openedPanels: {
type: Array,
default: () => []
}
},
data() {
return {
activeNames: this.openedPanels || []
}
},
watch: {
openedPanels(val) {
this.activeNames = val;
}
},
methods: {
handleChange(val) {
this.$emit('update:openedPanels', val);
}
}
};
</script>
<style scoped>
.int-expansion-panels {
margin-bottom: 10px;
}
/* 确保与Element UI折叠面板样式兼容 */
.int-expansion-panels .el-collapse {
border-top: 1px solid #EBEEF5;
border-bottom: 1px solid #EBEEF5;
}
.int-expansion-panels .el-collapse-item__header {
font-size: 14px;
padding-left: 12px;
}
.int-expansion-panels .el-collapse-item__content {
padding: 10px;
}
</style>
<template>
<div style="width: 112px; height: 90px">
<div class="int-loader" />
<div v-if="label != null" style="text-align: center; padding-left: 14px;">
{{ label }}
</div>
</div>
</template>
<script>
export default {
name: 'IntLoader',
props: {
label: {
type: String,
default: null
}
}
};
</script>
<style scoped>
.int-loader {
margin: auto;
border: 5px solid #f3f3f3; /* Light grey */
border-top: 5px solid #1c3d62; /* Blue */
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
<template>
<v-navigation-drawer class="int-navigation-drawer" permanent clipped app touchless :width="computedWidth"
:min-width="computedWidth" mini-variant-width="40" :mini-variant="collapsed" :right="right">
<v-btn v-if="collapse" :class="right ? 'float-left' : 'float-right'" icon
:title="collapsed ? 'Show Panel' : 'Hide Panel'" @click.stop="onCollapseClick()">
<v-icon>{{ chevronIcon }}</v-icon>
</v-btn>
<slot v-if="!collapsed" name="default" />
<slot v-else name="collapsed" />
</v-navigation-drawer>
</template>
<script>
import { VNavigationDrawer } from 'vuetify/lib';
import PlotHostHeightUtil from '../utils/PlotHostHeightUtil';
export default {
name: 'IntSidebar',
components: { VNavigationDrawer },
props: {
right: {
type: Boolean,
default: false
},
collapse: {
type: Boolean,
default: false
},
isCollapsed: {
type: Boolean,
default: false
},
closable: {
type: Boolean,
default: false
},
width: {
type: [Number, String],
default: 236
}
},
data() {
return {
collapsed: this.isCollapsed
};
},
computed: {
chevronIcon: function () {
const collapsedIcon = this.right ? 'mdi-chevron-double-left' : 'mdi-chevron-double-right';
const defaultIcon = this.right ? 'mdi-chevron-double-right' : 'mdi-chevron-double-left';
return this.collapsed ? collapsedIcon : defaultIcon;
},
computedWidth: function () {
// TODO: Should also accept string, but because of a bug in vuetify resize doesn't work or
// because the navigation drawer is inside v-content which is forbidden by docs
return parseInt(this.width);
}
},
watch: {
isCollapsed(val) {
this.collapsed = val;
}
},
methods: {
onCollapseClick: function () {
this.collapsed = !this.collapsed;
this.$emit('collapse', this.collapsed);
},
plotHostHeight() {
return new PlotHostHeightUtil().plotHostHeight;
}
}
};
</script>
<style scoped>
.int-navigation-drawer {
top: auto !important;
vertical-align: top;
}
.int-navigation-drawer.v-navigation-drawer--mini-variant {
width: 40px !important;
min-width: 40px;
}
.int-navigation-drawer>>>button {
outline: none;
}
.int-navigation-drawer.theme--dark {
background-color: #272727;
}
</style>
<template>
<div>
<v-tooltip bottom>
<template v-slot:activator="on">
<v-btn
class="int-sidebar-mini-button"
:title="title"
width="100%"
min-width="100%"
tile
text
height="110px"
:ripple="false"
v-on="on"
@click="$emit('input', true)"
>
{{ title }}
</v-btn>
</template>
<span>{{ title }}</span>
</v-tooltip>
<int-dialog
absolute
:title="title"
:max-width="dialogWidth"
:min-width="dialogWidth"
:width="dialogWidth"
:value="value"
:position="absolutePosition"
@input="$emit('input', $event)"
>
<slot name="dialog-content" />
</int-dialog>
</div>
</template>
<script>
import {VBtn, VTooltip} from 'vuetify/lib';
import IntDialog from '../components/IntDialog.vue';
const SIDE_MARGIN = '45px';
const TOP_MARGIN = '190px';
export default {
name: 'IntSidebarMiniButton',
components: {VBtn, VTooltip, IntDialog},
props: {
title: {type: String, default: ''},
right: {type: Boolean, default: false},
value: {type: Boolean, default: false},
dialogWidth: {type: [String, Number], default: '600px'}
},
computed: {
absolutePosition: function () {
return {
top: TOP_MARGIN,
left: this.right ? null : SIDE_MARGIN,
right: this.right ? SIDE_MARGIN : null
};
}
}
};
</script>
<style scoped>
.int-sidebar-mini-button {
font: bold 12px Arial;
transition: none !important;
}
::v-deep .v-btn__content {
transition: none !important;
transform: rotate(-90deg);
}
::v-deep .v-btn.outlined {
background-color: rgba(155, 155, 155, 0.5);
}
</style>
<template>
<v-slider
class="int-slider"
:input-value="value"
v-bind="$props"
v-on="$listeners"
>
<template v-slot:prepend>
<slot name="prepend" />
</template>
<template v-slot:append>
<slot name="append" />
</template>
</v-slider>
</template>
<script>
import {VSlider} from 'vuetify/lib';
export default {
name: 'IntSlider',
components: {VSlider},
props: {
label: {
type: String,
default: ''
},
max: {
type: [String, Number],
default: 0
},
min: {
type: [String, Number],
default: 0
},
step: {
type: Number,
default: 1
},
value: {
type: Number,
default: 1
}
}
};
</script>
<style scoped>
.int-slider {
max-height: 30px;
max-width: 200px;
}
</style>
<template>
<el-button
:icon="icon"
:plain="plain"
:loading="loading"
:title="title"
@click="handleClick"
>
<slot></slot>
</el-button>
</template>
<script>
export default {
name: 'IntButton',
props: {
icon: {
type: String,
default: ''
},
plain: {
type: Boolean,
default: true
},
loading: {
type: Boolean,
default: false
},
title: {
type: String,
default: ''
}
},
methods: {
handleClick() {
this.$emit('click');
}
}
};
</script>
<style scoped>
</style>
\ No newline at end of file
<template>
<el-collapse-item :name="panelId" :title="title">
<template #title>
<span class="panel-title">{{ title }}</span>
</template>
<div class="panel-content">
<slot></slot>
</div>
</el-collapse-item>
</template>
<script>
export default {
name: 'IntExpansionPanel',
props: {
title: {
type: String,
default: ''
},
expanded: {
type: Boolean,
default: false
},
panelId: {
type: [String, Number],
default() {
return String(Date.now());
}
}
}
}
</script>
<style scoped>
.int-expansion-panel {
margin-bottom: 10px;
border-radius: 4px;
overflow: hidden;
}
.panel-title {
font-weight: bold;
font-size: 14px;
}
.panel-content {
padding: 10px;
}
</style>
\ No newline at end of file
<template>
<el-dialog :visible.sync="showDialog" width="800px" :before-close="onClose" :title="title" append-to-body>
<el-form :model="dialogData" label-width="120px">
<!-- 纸张格式 -->
<el-form-item label="纸张格式">
<el-select v-model="dialogData.paperFormat" @change="onChange">
<el-option v-for="format in papers" :key="format" :label="format" :value="format">
</el-option>
</el-select>
</el-form-item>
<!-- 宽度和高度 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="宽度">
<el-input-number v-model="dialogData.width" :disabled="dialogData.paperFormat !== 'Custom'" :precision="1"
:step="0.1">
</el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="高度">
<el-input-number v-model="dialogData.height" :disabled="dialogData.paperFormat !== 'Custom'" :precision="1"
:step="0.1">
</el-input-number>
</el-form-item>
</el-col>
</el-row>
<!-- 方向和缩放 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="方向">
<el-select v-model="dialogData.orientation" @change="onChange">
<el-option v-for="orient in orientations" :key="orient" :label="orientationLabels[orient] || orient"
:value="orient">
</el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="缩放">
<el-select v-model="dialogData.scaling">
<el-option v-for="scale in scalingOptions" :key="scale" :label="scalingLabels[scale] || scale"
:value="scale">
</el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 保持比例和连续 -->
<el-row :gutter="20">
<el-col :span="12">
<el-form-item>
<el-checkbox v-model="dialogData.keepAspectRatio">保持比例</el-checkbox>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<el-checkbox v-model="dialogData.continuous">连续</el-checkbox>
</el-form-item>
</el-col>
</el-row>
<!-- 边距设置 -->
<el-form-item label="单位">
<el-select v-model="dialogData.units">
<el-option v-for="unit in unitOptions" :key="unit" :label="unit" :value="unit">
</el-option>
</el-select>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="上边距">
<el-input-number v-model="dialogData.top" :precision="1" :step="0.1"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="下边距">
<el-input-number v-model="dialogData.bottom" :precision="1" :step="0.1"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="左边距">
<el-input-number v-model="dialogData.left" :precision="1" :step="0.1"></el-input-number>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="右边距">
<el-input-number v-model="dialogData.right" :precision="1" :step="0.1"></el-input-number>
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-checkbox v-model="dialogData.drawWestToEast">从西到东绘制</el-checkbox>
</el-form-item>
</el-form>
<slot />
<div v-if="exportPending" style="text-align: center; margin: 20px 0;">
<IntLoader label="导出中..." />
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="onClose">关闭</el-button>
<el-button type="primary" :disabled="exportPending" @click="onExport">导出</el-button>
</span>
</el-dialog>
</template>
<script>
import { PaperFormatFactory } from '@int/geotoolkit/scene/exports/PaperFormatFactory.js';
import { PaperOrientation } from '@int/geotoolkit/scene/exports/PaperOrientation.js';
import { ScalingOptions } from '@int/geotoolkit/scene/exports/ScalingOptions.js';
import IntLoader from '@/components/IntLoader.vue';
export default {
name: 'PrintDialog',
components: { IntLoader },
props: {
showDialog: {
type: Boolean,
default: false
},
exportPending: {
type: Boolean,
default: false
},
title: {
type: String,
default: 'Export to PDF'
},
paperFormat: {
type: String,
default: 'Letter'
},
orientation: {
type: String,
default: PaperOrientation.Portrait
},
top: {
type: [Number, String],
default: 1
},
bottom: {
type: [Number, String],
default: 1
},
left: {
type: [Number, String],
default: 0.5
},
right: {
type: [Number, String],
default: 0.5
},
keepAspectRatio: {
type: [Boolean, String],
default: false
},
scaling: {
type: [String],
default: ScalingOptions.FitToPage
},
continuous: {
type: [Boolean, String],
default: false
},
drawWestToEast: {
type: [Boolean, String],
default: false
},
units: {
type: String,
default: 'cm'
}
},
data() {
return {
paperFactory: null,
papers: [],
orientations: [],
scalingOptions: [],
unitOptions: ['cm', 'mm', 'px', 'inch'],
dialogData: {},
// 方向选项的中文映射
orientationLabels: {
'Portrait': '纵向',
'Landscape': '横向'
},
// 缩放选项的中文映射
scalingLabels: {
'AsIs': '原始大小',
'FitBoth': '适应页面',
'FitWidth': '适应宽度',
'FitHeight': '适应高度',
'Custom': '自定义'
}
};
},
created() {
this.paperFactory = PaperFormatFactory.getInstance();
this.papers = this.paperFactory.getPaperList();
this.orientations = Object.keys(PaperOrientation);
this.scalingOptions = Object.keys(ScalingOptions);
this.dialogData = {
'paperFormat': this.paperFormat,
'orientation': this.orientation,
'top': Number(this.top),
'bottom': Number(this.bottom),
'left': Number(this.left),
'right': Number(this.right),
'scaling': this.scaling,
'keepAspectRatio': this.keepAspectRatio === 'true' || this.keepAspectRatio,
'continuous': this.continuous === 'true' || this.continuous,
'units': this.units,
'drawWestToEast': this.drawWestToEast === 'true' || this.drawWestToEast,
'width': 10,
'height': 15
};
// 初始化宽高
const paper = this.getPaper();
this.dialogData.width = +paper.getWidth().toFixed('1');
this.dialogData.height = +paper.getHeight().toFixed('1');
},
methods: {
getPaper() {
const { paperFormat, units, orientation } = this.dialogData;
return this.paperFactory.getPaper(paperFormat, units, orientation);
},
onChange() {
const paper = this.getPaper();
this.dialogData.width = +paper.getWidth().toFixed('1');
this.dialogData.height = +paper.getHeight().toFixed('1');
},
onExport() {
const settings = {
...this.dialogData,
'paperFormat': this.getPaper()
};
this.$emit('save', { 'printSettings': settings });
},
onClose() {
this.$emit('close');
}
}
};
</script>
<template> <template>
<div class="login"> <div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form"> <el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">{{title}}</h3> <h3 class="title">{{ title }}</h3>
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号">
v-model="loginForm.username"
type="text"
auto-complete="off"
placeholder="账号"
>
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" /> <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="password"> <el-form-item prop="password">
<el-input <el-input v-model="loginForm.password" type="password" auto-complete="off" placeholder="密码"
v-model="loginForm.password" @keyup.enter.native="handleLogin">
type="password"
auto-complete="off"
placeholder="密码"
@keyup.enter.native="handleLogin"
>
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" /> <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="code" v-if="captchaEnabled"> <el-form-item prop="code" v-if="captchaEnabled">
<el-input <el-input v-model="loginForm.code" auto-complete="off" placeholder="验证码" style="width: 63%"
v-model="loginForm.code" @keyup.enter.native="handleLogin">
auto-complete="off"
placeholder="验证码"
style="width: 63%"
@keyup.enter.native="handleLogin"
>
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" /> <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
</el-input> </el-input>
<div class="login-code"> <div class="login-code">
<img :src="codeUrl" @click="getCode" class="login-code-img"/> <img :src="codeUrl" @click="getCode" class="login-code-img" />
</div> </div>
</el-form-item> </el-form-item>
<el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox> <el-checkbox v-model="loginForm.rememberMe" style="margin:0px 0px 25px 0px;">记住密码</el-checkbox>
<el-form-item style="width:100%;"> <el-form-item style="width:100%;">
<el-button <el-button :loading="loading" size="medium" type="primary" style="width:100%;"
:loading="loading" @click.native.prevent="handleLogin">
size="medium"
type="primary"
style="width:100%;"
@click.native.prevent="handleLogin"
>
<span v-if="!loading">登 录</span> <span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span> <span v-else>登 录 中...</span>
</el-button> </el-button>
...@@ -84,7 +64,13 @@ export default { ...@@ -84,7 +64,13 @@ export default {
{ required: true, trigger: "blur", message: "请输入您的账号" } { required: true, trigger: "blur", message: "请输入您的账号" }
], ],
password: [ password: [
{ required: true, trigger: "blur", message: "请输入您的密码" } { required: true, trigger: "blur", message: "请输入您的密码" },
{
pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])\S{6,20}$/,
message:
'用户密码长度为 6 到 20 个字符,且必须包含字母、数字以及特殊符号',
trigger: 'blur',
},
], ],
code: [{ required: true, trigger: "change", message: "请输入验证码" }] code: [{ required: true, trigger: "change", message: "请输入验证码" }]
}, },
...@@ -98,7 +84,7 @@ export default { ...@@ -98,7 +84,7 @@ export default {
}, },
watch: { watch: {
$route: { $route: {
handler: function(route) { handler: function (route) {
this.redirect = route.query && route.query.redirect this.redirect = route.query && route.query.redirect
}, },
immediate: true immediate: true
...@@ -142,7 +128,7 @@ export default { ...@@ -142,7 +128,7 @@ export default {
Cookies.remove('rememberMe') Cookies.remove('rememberMe')
} }
this.$store.dispatch("Login", this.loginForm).then(() => { this.$store.dispatch("Login", this.loginForm).then(() => {
this.$router.push({ path: this.redirect || "/" }).catch(()=>{}) this.$router.push({ path: this.redirect || "/" }).catch(() => { })
}).catch(() => { }).catch(() => {
this.loading = false this.loading = false
if (this.captchaEnabled) { if (this.captchaEnabled) {
...@@ -165,6 +151,7 @@ export default { ...@@ -165,6 +151,7 @@ export default {
background-image: url("../assets/images/login-background.jpg"); background-image: url("../assets/images/login-background.jpg");
background-size: cover; background-size: cover;
} }
.title { .title {
margin: 0px auto 30px auto; margin: 0px auto 30px auto;
text-align: center; text-align: center;
...@@ -177,32 +164,39 @@ export default { ...@@ -177,32 +164,39 @@ export default {
width: 400px; width: 400px;
padding: 25px 25px 5px 25px; padding: 25px 25px 5px 25px;
z-index: 1; z-index: 1;
.el-input { .el-input {
height: 38px; height: 38px;
input { input {
height: 38px; height: 38px;
} }
} }
.input-icon { .input-icon {
height: 39px; height: 39px;
width: 14px; width: 14px;
margin-left: 2px; margin-left: 2px;
} }
} }
.login-tip { .login-tip {
font-size: 13px; font-size: 13px;
text-align: center; text-align: center;
color: #bfbfbf; color: #bfbfbf;
} }
.login-code { .login-code {
width: 33%; width: 33%;
height: 38px; height: 38px;
float: right; float: right;
img { img {
cursor: pointer; cursor: pointer;
vertical-align: middle; vertical-align: middle;
} }
} }
.el-login-footer { .el-login-footer {
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
...@@ -215,6 +209,7 @@ export default { ...@@ -215,6 +209,7 @@ export default {
font-size: 12px; font-size: 12px;
letter-spacing: 1px; letter-spacing: 1px;
} }
.login-code-img { .login-code-img {
height: 38px; height: 38px;
} }
......
<template> <template>
<div class="register"> <div class="register">
<el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form"> <el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form">
<h3 class="title">{{title}}</h3> <h3 class="title">{{ title }}</h3>
<el-form-item prop="username"> <el-form-item prop="username">
<el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号"> <el-input v-model="registerForm.username" type="text" auto-complete="off" placeholder="账号">
<svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" /> <svg-icon slot="prefix" icon-class="user" class="el-input__icon input-icon" />
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="password"> <el-form-item prop="password">
<el-input <el-input v-model="registerForm.password" type="password" auto-complete="off" placeholder="密码"
v-model="registerForm.password" @keyup.enter.native="handleRegister">
type="password"
auto-complete="off"
placeholder="密码"
@keyup.enter.native="handleRegister"
>
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" /> <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="confirmPassword"> <el-form-item prop="confirmPassword">
<el-input <el-input v-model="registerForm.confirmPassword" type="password" auto-complete="off" placeholder="确认密码"
v-model="registerForm.confirmPassword" @keyup.enter.native="handleRegister">
type="password"
auto-complete="off"
placeholder="确认密码"
@keyup.enter.native="handleRegister"
>
<svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" /> <svg-icon slot="prefix" icon-class="password" class="el-input__icon input-icon" />
</el-input> </el-input>
</el-form-item> </el-form-item>
<el-form-item prop="code" v-if="captchaEnabled"> <el-form-item prop="code" v-if="captchaEnabled">
<el-input <el-input v-model="registerForm.code" auto-complete="off" placeholder="验证码" style="width: 63%"
v-model="registerForm.code" @keyup.enter.native="handleRegister">
auto-complete="off"
placeholder="验证码"
style="width: 63%"
@keyup.enter.native="handleRegister"
>
<svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" /> <svg-icon slot="prefix" icon-class="validCode" class="el-input__icon input-icon" />
</el-input> </el-input>
<div class="register-code"> <div class="register-code">
<img :src="codeUrl" @click="getCode" class="register-code-img"/> <img :src="codeUrl" @click="getCode" class="register-code-img" />
</div> </div>
</el-form-item> </el-form-item>
<el-form-item style="width:100%;"> <el-form-item style="width:100%;">
<el-button <el-button :loading="loading" size="medium" type="primary" style="width:100%;"
:loading="loading" @click.native.prevent="handleRegister">
size="medium"
type="primary"
style="width:100%;"
@click.native.prevent="handleRegister"
>
<span v-if="!loading">注 册</span> <span v-if="!loading">注 册</span>
<span v-else>注 册 中...</span> <span v-else>注 册 中...</span>
</el-button> </el-button>
...@@ -96,8 +76,8 @@ export default { ...@@ -96,8 +76,8 @@ export default {
], ],
password: [ password: [
{ required: true, trigger: "blur", message: "请输入您的密码" }, { required: true, trigger: "blur", message: "请输入您的密码" },
{ min: 5, max: 20, message: "用户密码长度必须介于 5 和 20 之间", trigger: "blur" }, { pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])\S{6,20}$/, message: '用户密码长度为 6 到 20 个字符,且必须包含字母、数字以及特殊符号', trigger: 'blur' }
{ pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" } // { pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }
], ],
confirmPassword: [ confirmPassword: [
{ required: true, trigger: "blur", message: "请再次输入您的密码" }, { required: true, trigger: "blur", message: "请再次输入您的密码" },
...@@ -133,7 +113,7 @@ export default { ...@@ -133,7 +113,7 @@ export default {
type: 'success' type: 'success'
}).then(() => { }).then(() => {
this.$router.push("/login") this.$router.push("/login")
}).catch(() => {}) }).catch(() => { })
}).catch(() => { }).catch(() => {
this.loading = false this.loading = false
if (this.captchaEnabled) { if (this.captchaEnabled) {
...@@ -156,6 +136,7 @@ export default { ...@@ -156,6 +136,7 @@ export default {
background-image: url("../assets/images/login-background.jpg"); background-image: url("../assets/images/login-background.jpg");
background-size: cover; background-size: cover;
} }
.title { .title {
margin: 0px auto 30px auto; margin: 0px auto 30px auto;
text-align: center; text-align: center;
...@@ -167,32 +148,39 @@ export default { ...@@ -167,32 +148,39 @@ export default {
background: #ffffff; background: #ffffff;
width: 400px; width: 400px;
padding: 25px 25px 5px 25px; padding: 25px 25px 5px 25px;
.el-input { .el-input {
height: 38px; height: 38px;
input { input {
height: 38px; height: 38px;
} }
} }
.input-icon { .input-icon {
height: 39px; height: 39px;
width: 14px; width: 14px;
margin-left: 2px; margin-left: 2px;
} }
} }
.register-tip { .register-tip {
font-size: 13px; font-size: 13px;
text-align: center; text-align: center;
color: #bfbfbf; color: #bfbfbf;
} }
.register-code { .register-code {
width: 33%; width: 33%;
height: 38px; height: 38px;
float: right; float: right;
img { img {
cursor: pointer; cursor: pointer;
vertical-align: middle; vertical-align: middle;
} }
} }
.el-register-footer { .el-register-footer {
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
...@@ -205,6 +193,7 @@ export default { ...@@ -205,6 +193,7 @@ export default {
font-size: 12px; font-size: 12px;
letter-spacing: 1px; letter-spacing: 1px;
} }
.register-code-img { .register-code-img {
height: 38px; height: 38px;
} }
......
<template> <template>
<el-form ref="form" :model="user" :rules="rules" label-width="80px"> <el-form ref="form" :model="user" :rules="rules" label-width="80px">
<el-form-item label="旧密码" prop="oldPassword"> <el-form-item label="旧密码" prop="oldPassword">
<el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password/> <el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password />
</el-form-item> </el-form-item>
<el-form-item label="新密码" prop="newPassword"> <el-form-item label="新密码" prop="newPassword">
<el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password/> <el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password />
</el-form-item> </el-form-item>
<el-form-item label="确认密码" prop="confirmPassword"> <el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="user.confirmPassword" placeholder="请确认新密码" type="password" show-password/> <el-input v-model="user.confirmPassword" placeholder="请确认新密码" type="password" show-password />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" size="mini" @click="submit">保存</el-button> <el-button type="primary" size="mini" @click="submit">保存</el-button>
...@@ -37,16 +37,16 @@ export default { ...@@ -37,16 +37,16 @@ export default {
// 表单校验 // 表单校验
rules: { rules: {
oldPassword: [ oldPassword: [
{ required: true, message: "旧密码不能为空", trigger: "blur" } { required: true, message: "旧密码不能为空", trigger: "blur" },
{ pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])\S{6,20}$/, message: '用户密码长度为 6 到 20 个字符,且必须包含字母、数字以及特殊符号', trigger: 'blur' }
], ],
newPassword: [ newPassword: [
{ required: true, message: "新密码不能为空", trigger: "blur" }, { required: true, message: "新密码不能为空", trigger: "blur" },
{ min: 6, max: 20, message: "长度在 6 到 20 个字符", trigger: "blur" }, { pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])\S{6,20}$/, message: '用户密码长度为 6 到 20 个字符,且必须包含字母、数字以及特殊符号', trigger: 'blur' }
{ pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }
], ],
confirmPassword: [ confirmPassword: [
{ required: true, message: "确认密码不能为空", trigger: "blur" }, { required: true, message: "确认密码不能为空", trigger: "blur" },
{ required: true, validator: equalToPassword, trigger: "blur" } { pattern: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*?&])\S{6,20}$/, message: '用户密码长度为 6 到 20 个字符,且必须包含字母、数字以及特殊符号', trigger: 'blur' }
] ]
} }
} }
......
This source diff could not be displayed because it is too large. You can view the blob instead.
'use strict' "use strict";
const path = require('path') const path = require("path");
function resolve(dir) { function resolve(dir) {
return path.join(__dirname, dir) return path.join(__dirname, dir);
} }
const CompressionPlugin = require('compression-webpack-plugin') const CompressionPlugin = require("compression-webpack-plugin");
const name = process.env.VUE_APP_TITLE || '若依管理系统' // 网页标题 const name = process.env.VUE_APP_TITLE || "系统"; // 网页标题
const baseUrl = 'http://localhost:8080' // 后端接口 const baseUrl = "http://localhost:8080"; // 后端接口
const port = process.env.port || process.env.npm_config_port || 80 // 端口 const port = process.env.port || process.env.npm_config_port || 80; // 端口
// vue.config.js 配置说明 // vue.config.js 配置说明
//官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions //官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions
...@@ -22,117 +22,116 @@ module.exports = { ...@@ -22,117 +22,116 @@ module.exports = {
// 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。 // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
publicPath: process.env.NODE_ENV === "production" ? "/" : "/", publicPath: process.env.NODE_ENV === "production" ? "/" : "/",
// 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)(默认dist) // 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)(默认dist)
outputDir: 'dist', outputDir: "dist",
// 用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下) // 用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下)
assetsDir: 'static', assetsDir: "static",
// 是否开启eslint保存检测,有效值:ture | false | 'error' // 是否开启eslint保存检测,有效值:ture | false | 'error'
lintOnSave: process.env.NODE_ENV === 'development', lintOnSave: process.env.NODE_ENV === "development",
// 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。 // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
productionSourceMap: false, productionSourceMap: false,
transpileDependencies: ['quill'], transpileDependencies: ["quill"],
// webpack-dev-server 相关配置 // webpack-dev-server 相关配置
devServer: { devServer: {
host: '0.0.0.0', host: "0.0.0.0",
port: port, port: port,
open: true, open: true,
proxy: { proxy: {
// detail: https://cli.vuejs.org/config/#devserver-proxy // detail: https://cli.vuejs.org/config/#devserver-proxy
[process.env.VUE_APP_BASE_API]: { [process.env.VUE_APP_BASE_API]: {
target: baseUrl, target: `http://192.168.31.108:8999`, //井测试
changeOrigin: true, changeOrigin: true,
pathRewrite: { pathRewrite: {
['^' + process.env.VUE_APP_BASE_API]: '' ["^" + process.env.VUE_APP_BASE_API]: "",
} },
}, },
// springdoc proxy // springdoc proxy
'^/v3/api-docs/(.*)': { "^/v3/api-docs/(.*)": {
target: baseUrl, target: baseUrl,
changeOrigin: true changeOrigin: true,
} },
}, },
disableHostCheck: true disableHostCheck: true,
}, },
css: { css: {
loaderOptions: { loaderOptions: {
sass: { sass: {
sassOptions: { outputStyle: "expanded" } sassOptions: { outputStyle: "expanded" },
} },
} },
}, },
configureWebpack: { configureWebpack: {
name: name, name: name,
resolve: { resolve: {
alias: { alias: {
'@': resolve('src') "@": resolve("src"),
} },
}, },
plugins: [ plugins: [
// http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件 // http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
new CompressionPlugin({ new CompressionPlugin({
cache: false, // 不启用文件缓存 cache: false, // 不启用文件缓存
test: /\.(js|css|html|jpe?g|png|gif|svg)?$/i, // 压缩文件格式 test: /\.(js|css|html|jpe?g|png|gif|svg)?$/i, // 压缩文件格式
filename: '[path][base].gz[query]', // 压缩后的文件名 filename: "[path][base].gz[query]", // 压缩后的文件名
algorithm: 'gzip', // 使用gzip压缩 algorithm: "gzip", // 使用gzip压缩
minRatio: 0.8, // 压缩比例,小于 80% 的文件不会被压缩 minRatio: 0.8, // 压缩比例,小于 80% 的文件不会被压缩
deleteOriginalAssets: false // 压缩后删除原文件 deleteOriginalAssets: false, // 压缩后删除原文件
}) }),
], ],
}, },
chainWebpack(config) { chainWebpack(config) {
config.plugins.delete('preload') // TODO: need test config.plugins.delete("preload"); // TODO: need test
config.plugins.delete('prefetch') // TODO: need test config.plugins.delete("prefetch"); // TODO: need test
// set svg-sprite-loader // set svg-sprite-loader
config.module.rule("svg").exclude.add(resolve("src/assets/icons")).end();
config.module config.module
.rule('svg') .rule("icons")
.exclude.add(resolve('src/assets/icons'))
.end()
config.module
.rule('icons')
.test(/\.svg$/) .test(/\.svg$/)
.include.add(resolve('src/assets/icons')) .include.add(resolve("src/assets/icons"))
.end() .end()
.use('svg-sprite-loader') .use("svg-sprite-loader")
.loader('svg-sprite-loader') .loader("svg-sprite-loader")
.options({ .options({
symbolId: 'icon-[name]' symbolId: "icon-[name]",
}) })
.end() .end();
config.when(process.env.NODE_ENV !== 'development', config => { config.when(process.env.NODE_ENV !== "development", (config) => {
config config
.plugin('ScriptExtHtmlWebpackPlugin') .plugin("ScriptExtHtmlWebpackPlugin")
.after('html') .after("html")
.use('script-ext-html-webpack-plugin', [{ .use("script-ext-html-webpack-plugin", [
{
// `runtime` must same as runtimeChunk name. default is `runtime` // `runtime` must same as runtimeChunk name. default is `runtime`
inline: /runtime\..*\.js$/ inline: /runtime\..*\.js$/,
}]) },
.end() ])
.end();
config.optimization.splitChunks({ config.optimization.splitChunks({
chunks: 'all', chunks: "all",
cacheGroups: { cacheGroups: {
libs: { libs: {
name: 'chunk-libs', name: "chunk-libs",
test: /[\\/]node_modules[\\/]/, test: /[\\/]node_modules[\\/]/,
priority: 10, priority: 10,
chunks: 'initial' // only package third parties that are initially dependent chunks: "initial", // only package third parties that are initially dependent
}, },
elementUI: { elementUI: {
name: 'chunk-elementUI', // split elementUI into a single package name: "chunk-elementUI", // split elementUI into a single package
test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
priority: 20 // the weight needs to be larger than libs and app or it will be packaged into libs or app priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
}, },
commons: { commons: {
name: 'chunk-commons', name: "chunk-commons",
test: resolve('src/components'), // can customize your rules test: resolve("src/components"), // can customize your rules
minChunks: 3, // minimum common number minChunks: 3, // minimum common number
priority: 5, priority: 5,
reuseExistingChunk: true reuseExistingChunk: true,
} },
} },
}) });
config.optimization.runtimeChunk('single') config.optimization.runtimeChunk("single");
}) });
} },
} };
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment