Compare commits

..

No commits in common. "d77506ed310ada990bd57cad385b214a4b2211dc" and "c2e901e4bc8e19ef20e3110e3e063e1056d3b572" have entirely different histories.

9 changed files with 185 additions and 446 deletions

View File

@ -13,7 +13,7 @@ import { useSettings } from "./store/settings";
const props = defineProps(["path"]); const props = defineProps(["path"]);
const pages = ["3d", "data", "realtime", "alert", "announcement"]; const pages = ['3d', 'data', 'realtime', 'alert', 'announcement']
const currentPage = ref("3d"); const currentPage = ref("3d");
const handlePageChange = (page) => { const handlePageChange = (page) => {
currentPage.value = page; currentPage.value = page;
@ -23,84 +23,67 @@ const mainContainer = ref(null);
const store = useSettings(); const store = useSettings();
const timer = ref(null); const timer = ref(null);
function startCarousel(pages, duration) {
if (timer.value) clearInterval(timer.value);
timer.value = setInterval(() => {
handlePageChange(
pages[(pages.indexOf(currentPage.value) + 1) % pages.length]
);
}, duration);
}
store.$subscribe((mutation, state) => { store.$subscribe((mutation, state) => {
const pages = state.settings.carouselPages;
// //
if ( if (mutation.events.key == 'carouselTime' && state.settings.carouselTime > 0 && state.settings.carousel) {
mutation.events.key == "carouselTime" && if (timer.value) clearInterval(timer.value);
state.settings.carouselTime > 0 && timer.value = setInterval(() => {
state.settings.carousel handlePageChange(pages[(pages.indexOf(currentPage.value) + 1) % pages.length])
) { }, state.settings.carouselTime * 1000);
startCarousel(pages, state.settings.carouselTime * 1000); } else if (mutation.events.key == 'carousel') {
} else if (mutation.events.key == "carousel") {
// //
if (state.settings.carousel) { if (state.settings.carousel) {
startCarousel(pages, state.settings.carouselTime * 1000); timer.value = setInterval(() => {
} else { handlePageChange(pages[(pages.indexOf(currentPage.value) + 1) % pages.length])
clearInterval(timer.value); }, state.settings.carouselTime * 1000);
timer.value = null;
}
} else if (mutation.events.key == "carouselPages") {
if (state.settings.carousel) {
startCarousel(pages, state.settings.carouselTime * 1000);
} else { } else {
clearInterval(timer.value); clearInterval(timer.value);
timer.value = null; timer.value = null;
} }
} }
}); })
// //
onMounted(() => { onMounted(() => {
const settings = store.settings; const settings = store.settings;
const pages = settings.carouselPages;
if (settings.carousel) { if (settings.carousel) {
// //
if (timer.value) clearInterval(timer.value); if (timer.value) clearInterval(timer.value);
timer.value = setInterval(() => { timer.value = setInterval(() => {
handlePageChange( handlePageChange(pages[(pages.indexOf(currentPage.value) + 1) % pages.length])
pages[(pages.indexOf(currentPage.value) + 1) % pages.length]
);
}, settings.carouselTime * 1000); }, settings.carouselTime * 1000);
} }
// //
handleResolutionChange(settings.resolution.width, settings.resolution.height); handleResolutionChange(settings.resolution.width, settings.resolution.height);
}); })
const pathMap = { const pathMap = {
// 线 // 线
"/3-1": 1, '/3-1': 1,
"/3-2": 2, '/3-2': 2,
"/3-3": 11, // 3, '/3-3': 11, // 3,
"/3-4": 12, '/3-4': 12,
// 线 // 线
"/2-1": 5, '/2-1': 5,
"/2-2": 6, '/2-2': 6,
"/2-3": 7, '/2-3': 7,
"/2-4": 4, '/2-4': 4,
// 线 // 线
"/1-1": 9, '/1-1': 9,
"/1-2": 10, '/1-2': 10,
"/1-3": 3, '/1-3': 3,
"/1-4": 8, '/1-4': 8
}; }
function handleResolutionChange(width, height) { function handleResolutionChange(width, height) {
console.log("document.documentElement", document.documentElement); console.log('document.documentElement', document.documentElement)
if (mainContainer.value) { if (mainContainer.value) {
// mainContainer.value.style.width = `${width}px`; // mainContainer.value.style.width = `${width}px`;
// mainContainer.value.style.height = `${height}px`; // mainContainer.value.style.height = `${height}px`;
// changeScale(mainContainer.value, width, height) // changeScale(mainContainer.value, width, height)
changeScale(mainContainer.value, width, height); changeScale(mainContainer.value, width, height)
} }
} }
@ -119,37 +102,23 @@ function resetScale(elm) {
elm.style.transform = "initial"; elm.style.transform = "initial";
elm.style.transformOrigin = "initial"; elm.style.transformOrigin = "initial";
} }
</script> </script>
<template> <template>
<div id="main-container" ref="mainContainer" class="main-container"> <div id="main-container" ref="mainContainer" class="main-container">
<DatetimeTool v-if="currentPage !== 'announcement'" /> <DatetimeTool v-if="currentPage !== 'announcement'" />
<Tools <Tools v-if="currentPage !== 'announcement'" @change-resolution="handleResolutionChange" />
v-if="currentPage !== 'announcement'"
@change-resolution="handleResolutionChange"
/>
<AppHeader v-if="currentPage !== 'announcement'" /> <AppHeader v-if="currentPage !== 'announcement'" />
<AnnoucementPage <AnnoucementPage v-if="currentPage === 'announcement'" class="annoucement-page"
v-if="currentPage === 'announcement'" @home="() => handlePageChange('3d')" />
class="annoucement-page"
@home="() => handlePageChange('3d')"
/>
<div v-else class="pages-wrapper"> <div v-else class="pages-wrapper">
<NavMenu @change="handlePageChange" :value="currentPage" /> <NavMenu @change="handlePageChange" :value="currentPage" />
<!-- <TriplePage v-if="currentPage === '3d'" :line="pathMap[path] ?? '1'" /> --> <!-- <TriplePage v-if="currentPage === '3d'" :line="pathMap[path] ?? '1'" /> -->
<ThreeDimension <ThreeDimension v-if="currentPage === '3d'" :line="pathMap[path] ?? '1'" />
v-if="currentPage === '3d'"
:line="pathMap[path] ?? '1'"
/>
<DataPage v-if="currentPage === 'data'" :line="pathMap[path] ?? '1'" /> <DataPage v-if="currentPage === 'data'" :line="pathMap[path] ?? '1'" />
<AlertListPage <AlertListPage v-if="currentPage === 'alert'" :line="pathMap[path] ?? '1'" />
v-if="currentPage === 'alert'" <RealtimePage v-if="currentPage === 'realtime'" :line="pathMap[path] ?? '1'" />
:line="pathMap[path] ?? '1'"
/>
<RealtimePage
v-if="currentPage === 'realtime'"
:line="pathMap[path] ?? '1'"
/>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { nextTick, onMounted, ref, watch } from "vue"; import { nextTick, onMounted, ref } from "vue";
import * as echarts from "echarts"; import * as echarts from "echarts";
import getOptions from "./yieldOption"; import getOptions from "./yieldOption";
@ -25,17 +25,10 @@ const chart = ref(null);
const yieldChartRef = ref(null); const yieldChartRef = ref(null);
function reInitChart() { function reInitChart() {
// if (props.displayPlaceholder) return; if (props.displayPlaceholder) return;
if (chart.value) chart.value.dispose(); if (chart.value) chart.value.dispose();
const _chart = echarts.init(yieldChartRef.value); const _chart = echarts.init(yieldChartRef.value);
_chart.setOption( _chart.setOption(getOptions(props.rawData));
getOptions(
props.rawData ?? {
nowProduction: 0,
targetProduction: 0,
}
)
);
chart.value = _chart; chart.value = _chart;
} }
@ -48,9 +41,8 @@ onMounted(() => {
<template> <template>
<div class="chart yield-chart"> <div class="chart yield-chart">
<!-- <div v-if="displayPlaceholder" class="chart-placeholder"></div> <div v-if="displayPlaceholder" class="chart-placeholder"></div>
<div v-else ref="yieldChartRef" class="chart-container"></div> --> <div v-else ref="yieldChartRef" class="chart-container"></div>
<div ref="yieldChartRef" class="chart-container"></div>
<div class="text-intro"> <div class="text-intro">
<div class="text-intro__item"> <div class="text-intro__item">
<span class="legend-box green"></span> <span class="legend-box green"></span>

View File

@ -128,7 +128,7 @@ const targetSerie = {
}; };
export default (data) => { export default (data) => {
title.subtext = "当前成品率\u2002"; title.subtext = data?.nowYield == null ? "" : "当前成品率\u2002";
dataSerie.data[0].value = data?.nowYield ?? 0; dataSerie.data[0].value = data?.nowYield ?? 0;
dataSerie.data[1].value = 100 - (data?.nowYield ?? 0); dataSerie.data[1].value = 100 - (data?.nowYield ?? 0);
targetSerie.data[0].value = data?.targetYield ?? 0; targetSerie.data[0].value = data?.targetYield ?? 0;

View File

@ -1,5 +1,4 @@
const radius = ["58%", "72%"]; const radius = ["55%", "70%"];
const radius2 = ["45%", "58%"];
const grid = { const grid = {
top: 0, top: 0,
left: 24, left: 24,
@ -84,65 +83,12 @@ const dataSerie = {
], ],
}; };
const targetSerie = {
type: "pie",
radius: radius2,
center: ["50%", "40%"],
avoidLabelOvervlap: false,
label: {
show: false,
},
labelLine: {
show: false,
},
data: [
{
value: 90,
name: "目标成产量",
selected: false,
itemStyle: {
borderJoin: "round",
borderCap: "round",
borderWidth: 12,
borderRadius: "50%",
color: {
type: "linear",
x: 1,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: "#1065ff66" },
{ offset: 1, color: "#1065ff" },
],
},
},
},
{
value: 20,
name: "-",
itemStyle: { color: "transparent" },
label: { show: false },
},
],
};
export default (data) => { export default (data) => {
// title.text = // title.text =
// (100 * (+data.nowProduction / +data.targetProduction)).toFixed(0) + "%"; // (100 * (+data.nowProduction / +data.targetProduction)).toFixed(0) + "%";
// 外圈 title.text = (data.nowProduction || 0) + '片';
title.text = data.nowProduction || 0;
dataSerie.data[0].value = data.nowProduction; dataSerie.data[0].value = data.nowProduction;
dataSerie.data[1].value = !data.targetProduction dataSerie.data[1].value = data.targetProduction - data.nowProduction;
? data.nowProduction == 0
? 1
: 0
: data.targetProduction - data.nowProduction;
// 内圈
targetSerie.data[0].value = data?.targetProduction ?? 0;
targetSerie.data[1].value = data?.targetProduction ? 0 : 1;
return { return {
tooltip, tooltip,
title, title,
@ -150,8 +96,8 @@ export default (data) => {
series: [ series: [
// background // background
bgSerie, bgSerie,
// actual data
dataSerie, dataSerie,
targetSerie,
], ],
}; };
}; };

View File

@ -1,363 +1,208 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from 'vue';
import { useSettings } from "../store/settings"; import { useSettings } from '../store/settings'
const emit = defineEmits(["close", "change-resolution"]); const emit = defineEmits(["close", "change-resolution"]);
const store = useSettings(); const store = useSettings();
const settings = ref(store.settings); const settings = ref(store.settings);
store.$subscribe((mutation, state) => { store.$subscribe((mutation, state) => {
settings.value.fullscreen = state.settings.fullscreen; settings.value.fullscreen = state.settings.fullscreen;
}); })
function handleCancel() { function handleCancel() {
emit("close"); emit('close')
} }
function handleConfirm() { function handleConfirm() {
if (settings.value.resolution.width < 480) // alert(JSON.stringify(settings, null, 2))
store.settings.resolution.width = 480; // changeScale(settings.resolution.width, settings.resolution.height)
if (settings.value.resolution.width > 7680) if (settings.value.resolution.width < 480) store.settings.resolution.width = 480;
store.settings.resolution.width = 7680; if (settings.value.resolution.width > 7680) store.settings.resolution.width = 7680;
if (settings.value.resolution.height < 270) if (settings.value.resolution.height < 270) store.settings.resolution.height = 270;
store.settings.resolution.height = 270; if (settings.value.resolution.height > 4320) store.settings.resolution.height = 4320;
if (settings.value.resolution.height > 4320)
store.settings.resolution.height = 4320;
emit( emit('change-resolution', store.settings.resolution.width, store.settings.resolution.height)
"change-resolution",
store.settings.resolution.width,
store.settings.resolution.height
);
} }
</script> </script>
<template> <template>
<div class="setting-dialog"> <div class="setting-dialog">
<h1>设置</h1> <h1>设置</h1>
<div class="main-content"> <div class="main-content">
<div class="form-item"> <div class="form-item">
<label for="carousel">轮播时间</label> <label for="carousel">轮播时间</label>
<input id="carousel" type="number" v-model="settings.carouselTime" /> <input id="carousel" type="number" v-model="settings.carouselTime" />
<span></span> <span></span>
</div> </div>
<div <div class="form-item">
class="form-item" <label for="resolution1">分辨率</label>
style="display: flex; flex-direction: column; gap: 12px" <input id="resolution1" type="number" min="480" max="7680" v-model="settings.resolution.width" />
> <span>X</span>
<label for="carouselPages">轮播项目</label> <input id="resolution2" type="number" min="270" max="4320" v-model="settings.resolution.height" />
<div class="carousel-page__list"> <span>px</span>
<div> </div>
<input <div class="form-item selector">
type="checkbox" <!-- <div class="opt opt1">
id="cp-3d"
name="carouselPages"
:class="[
settings.carouselPages.includes('3d') ? 'checked' : '',
'carousel-page',
]"
@change="
() => {
store.updateSettings({ type: 'carousel-page', value: '3d' });
}
"
/>
<label for="cp-3d">三维界面</label>
</div>
<div>
<input
type="checkbox"
id="cp-data"
name="carouselPages"
:class="[
settings.carouselPages.includes('data') ? 'checked' : '',
'carousel-page',
]"
@change="
() => {
store.updateSettings({
type: 'carousel-page',
value: 'data',
});
}
"
/>
<label for="cp-data">数据界面</label>
</div>
<div>
<input
type="checkbox"
id="cp-realtime"
name="carouselPages"
:class="[
settings.carouselPages.includes('realtime') ? 'checked' : '',
'carousel-page',
]"
@change="
() => {
store.updateSettings({
type: 'carousel-page',
value: 'realtime',
});
}
"
/>
<label for="cp-realtime">实时数据</label>
</div>
<div>
<input
type="checkbox"
id="cp-alert"
name="carouselPages"
:class="[
settings.carouselPages.includes('alert') ? 'checked' : '',
'carousel-page',
]"
@change="
() => {
store.updateSettings({
type: 'carousel-page',
value: 'alert',
});
}
"
/>
<label for="cp-alert">报警列表</label>
</div>
<div>
<input
type="checkbox"
id="cp-announcement"
name="carouselPages"
:class="[
settings.carouselPages.includes('announcement')
? 'checked'
: '',
'carousel-page',
]"
@change="
() => {
store.updateSettings({
type: 'carousel-page',
value: 'announcement',
});
}
"
/>
<label for="cp-announcement">公告页面</label>
</div>
</div>
</div>
<div class="form-item">
<label for="resolution1">分辨率</label>
<input
id="resolution1"
type="number"
min="480"
max="7680"
v-model="settings.resolution.width"
/>
<span>X</span>
<input
id="resolution2"
type="number"
min="270"
max="4320"
v-model="settings.resolution.height"
/>
<span>px</span>
</div>
<div class="form-item selector">
<!-- <div class="opt opt1">
<input type="checkbox" id="fullscreen" name="fullscreen" :class="[settings.fullscreen ? 'checked' : '']" <input type="checkbox" id="fullscreen" name="fullscreen" :class="[settings.fullscreen ? 'checked' : '']"
v-model="settings.fullscreen" /> v-model="settings.fullscreen" />
<label for="fullscreen">全屏显示</label> <label for="fullscreen">全屏显示</label>
</div> --> </div> -->
<div class="opt opt2"> <div class="opt opt2">
<input <input type="checkbox" id="status" name="status" :class="[settings.eqStatus ? 'checked' : '']"
type="checkbox" v-model="settings.eqStatus" />
id="status" <label for="status">设备状态</label>
name="status" </div>
:class="[settings.eqStatus ? 'checked' : '', 'carousel-page']" </div>
v-model="settings.eqStatus" </div>
/> <div class="footer">
<label for="status">设备状态</label> <button @click="handleCancel" class="btn btn-cancel">取消</button>
<button @click="handleConfirm" class="btn btn-confirm">确认</button>
</div> </div>
</div>
</div> </div>
<div class="footer"> <div class="modal"></div>
<button @click="handleCancel" class="btn btn-cancel">取消</button>
<button @click="handleConfirm" class="btn btn-confirm">确认</button>
</div>
</div>
<div class="modal"></div>
</template> </template>
<style scoped> <style scoped>
* { * {
user-select: none; user-select: none;
} }
.setting-dialog { .setting-dialog {
position: absolute; position: absolute;
margin: auto; margin: auto;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
width: 577px; width: 577px;
height: 422px; height: 422px;
background: url(../assets/dialog-bg.png) 100% / contain no-repeat; background: url(../assets/dialog-bg.png) 100% / contain no-repeat;
z-index: 1001; z-index: 1001;
transition: all 0.3s ease-out; transition: all .3s ease-out;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 12px; gap: 18px;
padding: 24px 80px; padding: 24px 80px;
} }
.main-content { .main-content {
flex: 1; flex: 1;
padding-top: 10px; padding-top: 10px;
} }
.form-item { .form-item {
margin: 20px 0; margin: 32px 0;
display: flex; display: flex;
gap: 8px; gap: 8px;
color: #fff; color: #fff;
font-size: 24px; font-size: 28px;
letter-spacing: 2px; letter-spacing: 2px;
padding: 0 12px; padding: 0 12px;
} }
.form-item.selector { .form-item.selector {
gap: 32px; gap: 32px;
} }
.opt { .opt {
display: flex; margin-left: 24px;
align-items: center; display: flex;
gap: 8px; align-items: center;
} gap: 8px;
.opt2 {
font-size: 16px;
} }
.form-item input { .form-item input {
flex: 1; flex: 1;
border: none; border: none;
appearance: none; appearance: none;
outline: none; outline: none;
border-radius: 4px; border-radius: 4px;
padding: 4px 12px; padding: 4px 12px;
font-size: 18px; font-size: 18px;
} }
input#resolution1, input#resolution1,
input#resolution2 { input#resolution2 {
width: 10px; width: 10px;
} }
input[type="checkbox"] { input[type="checkbox"] {
appearance: initial; appearance: initial;
width: 24px; width: 24px;
height: 24px; height: 24px;
background: #fff; background: #fff;
flex: unset; flex: unset;
padding: unset; padding: unset;
font-size: unset; font-size: unset;
cursor: pointer; cursor: pointer;
} }
input[type="checkbox"].checked { input[type="checkbox"].checked {
background: #0049ff; background: #0049ff;
position: relative; position: relative;
} }
input[type="checkbox"].checked::after { input[type="checkbox"].checked::after {
content: "\2713"; content: '\2713';
color: #fff; color: #fff;
font-size: 22px; font-size: 22px;
line-height: 24px; line-height: 24px;
text-align: center; text-align: center;
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
}
.carousel-page__list {
display: flex;
gap: 8px;
flex-wrap: wrap;
font-size: 16px;
}
.carousel-page__list > div {
display: flex;
align-items: center;
gap: 8px;
}
input[type="checkbox"].carousel-page {
width: 18px;
height: 18px;
}
input[type="checkbox"].carousel-page.checked::after {
font-size: 14px;
line-height: 18px;
} }
label { label {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }
.footer { .footer {
display: flex; display: flex;
gap: 12px; gap: 12px;
} }
.btn { .btn {
flex: 1; flex: 1;
padding: 12px; padding: 18px;
font-size: 20px; font-size: 28px;
cursor: pointer; cursor: pointer;
letter-spacing: 8px; letter-spacing: 12px;
background: url(../assets/dialog-button.png) 0 0 / 100% 100% no-repeat; background: url(../assets/dialog-button.png) 0 0 / 100% 100% no-repeat;
} }
button { button {
appearance: none; appearance: none;
outline: none; outline: none;
background: none; background: none;
border: none; border: none;
color: #fff; color: #fff;
} }
.modal { .modal {
/* position: fixed; /* position: fixed;
height: 1080px; height: 1080px;
width: 1920px; */ width: 1920px; */
position: absolute; position: absolute;
height: 100%; height: 100%;
width: 100%; width: 100%;
left: 0; left: 0;
top: 0; top: 0;
background: #0003; background: #0003;
backdrop-filter: blur(3px); backdrop-filter: blur(3px);
z-index: 999; z-index: 999;
transition: all 0.3s ease-out; transition: all .3s ease-out;
} }
.setting-dialog > h1 { .setting-dialog>h1 {
text-align: center; text-align: center;
letter-spacing: 24px; letter-spacing: 24px;
font-weight: 400; font-weight: 400;
text-shadow: 0 5px 1px #001124; text-shadow: 0 5px 1px #001124;
user-select: none; user-select: none;
color: #fff; color: #fff;
} }
</style> </style>

View File

@ -98,6 +98,7 @@ function loadData(monthlyTarget) {
<p v-show="!show" class="empty-data-hint">暂无数据</p> --> <p v-show="!show" class="empty-data-hint">暂无数据</p> -->
<div class="container-body__h-full"> <div class="container-body__h-full">
<yield-chart <yield-chart
:display-placeholder="!displayProductionChart"
:key="refreshToken + '_yield_chart_linemonth'" :key="refreshToken + '_yield_chart_linemonth'"
:raw-data="websocketData" :raw-data="websocketData"
/> />

View File

@ -14,10 +14,10 @@ const store = useWsStore();
onMounted(() => { onMounted(() => {
// websocketData.value = loadData([ // websocketData.value = loadData([
// { // {
// targetProduction: 1220, // targetProduction: 120,
// nowProduction: 8, // nowProduction: 0,
// targetYield: null, // targetYield: 0,
// nowYield: null, // nowYield: 10,
// }, // },
// ]); // ]);
websocketData.value = loadData(store.data2.dailyTarget); websocketData.value = loadData(store.data2.dailyTarget);
@ -103,6 +103,7 @@ function loadData(dailyTarget) {
<p v-show="!show" class="empty-data-hint">暂无数据</p> --> <p v-show="!show" class="empty-data-hint">暂无数据</p> -->
<div class="container-body__h-full"> <div class="container-body__h-full">
<yield-chart <yield-chart
:display-placeholder="!displayProductionChart"
:key="refreshToken + '_yield_chart_linetoday'" :key="refreshToken + '_yield_chart_linetoday'"
:raw-data="websocketData" :raw-data="websocketData"
/> />

View File

@ -10,7 +10,7 @@ const pinia = createPinia();
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
}, 60 * 60 * 1000); }, 24 * 60 * 60 * 1000);
const app = createApp(App); const app = createApp(App);
app.use(pinia); app.use(pinia);

View File

@ -13,7 +13,6 @@ export const useSettings = defineStore("settings", () => {
}, },
carousel: true, carousel: true,
carouselTime: 30, // s carouselTime: 30, // s
carouselPages: ["3d", "data", "realtime", "alert", "announcement"],
fullscreen: false, fullscreen: false,
eqStatus: true, eqStatus: true,
} }
@ -71,20 +70,6 @@ export const useSettings = defineStore("settings", () => {
settings.value.resolution.height = value.height; settings.value.resolution.height = value.height;
settings.value.resolution.width = value.width; settings.value.resolution.width = value.width;
break; break;
case "carousel-page":
const exists = settings.value.carouselPages.includes(value);
if (exists)
settings.value.carouselPages = settings.value.carouselPages.filter(
(page) => page !== value
);
else {
settings.value.carouselPages.splice(
["3d", "data", "realtime", "alert", "announcement"].indexOf(value),
0,
value
);
}
break;
} }
} }
return { settings, updateSettings, rewriteSettings }; return { settings, updateSettings, rewriteSettings };