feat: 巡檢設定切版 | 公用組件: Alert、PaginatedTable

This commit is contained in:
huliang 2025-11-19 14:01:34 +08:00
parent 0e33a0b64f
commit 4237875453
24 changed files with 1735 additions and 81 deletions

29
package-lock.json generated
View File

@ -16,7 +16,8 @@
"leaflet-tilelayer-mbtiles": "^1.4.1", "leaflet-tilelayer-mbtiles": "^1.4.1",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.5.1" "vue-router": "^4.5.1",
"vue3-signature": "^0.4.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",
@ -1759,6 +1760,12 @@
} }
} }
}, },
"node_modules/default-passive-events": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/default-passive-events/-/default-passive-events-2.0.0.tgz",
"integrity": "sha512-eMtt76GpDVngZQ3ocgvRcNCklUMwID1PaNbCNxfpDXuiOXttSh0HzBbda1HU9SIUsDc02vb7g9+3I5tlqe/qMQ==",
"license": "MIT"
},
"node_modules/delayed-stream": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -3081,6 +3088,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/signature_pad": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-5.1.2.tgz",
"integrity": "sha512-zYmjddQDolKgJnzYRoaMYaGezKaZbwjNBBwk1W7uVY0cyNWW30Izeu9BNVAGEgXvqB6APDJmf783oWTU7W67LQ==",
"license": "MIT"
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@ -3474,6 +3487,20 @@
"vue": "^3.2.0" "vue": "^3.2.0"
} }
}, },
"node_modules/vue3-signature": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/vue3-signature/-/vue3-signature-0.4.0.tgz",
"integrity": "sha512-RvU0AqmFmZ/kTc90qbCPCGO5oavr8hidtcstLFwBZMyEpYKOvujHGzX9JO0eKkTVMEhV2gNzqtl/D9mOIHQctg==",
"license": "MIT",
"dependencies": {
"default-passive-events": "^2.0.0",
"signature_pad": "^5.1.1",
"vue": "^3.2.37"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/webpack-virtual-modules": { "node_modules/webpack-virtual-modules": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",

View File

@ -17,7 +17,8 @@
"leaflet-tilelayer-mbtiles": "^1.4.1", "leaflet-tilelayer-mbtiles": "^1.4.1",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"vue": "^3.5.22", "vue": "^3.5.22",
"vue-router": "^4.5.1" "vue-router": "^4.5.1",
"vue3-signature": "^0.4.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.1", "@vitejs/plugin-vue": "^6.0.1",

View File

@ -2,6 +2,8 @@
import { ref, onMounted, onUnmounted } from "vue"; import { ref, onMounted, onUnmounted } from "vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import { ElConfigProvider } from "element-plus"; import { ElConfigProvider } from "element-plus";
import zhTW from 'element-plus/es/locale/lang/zh-tw'
import GlobalAlerts from './components/Common/GlobalAlerts.vue';
import Navbar from "./components/Navbar.vue"; import Navbar from "./components/Navbar.vue";
import Sidebar from "./components/Sidebar.vue"; import Sidebar from "./components/Sidebar.vue";
const router = useRouter(); const router = useRouter();
@ -30,24 +32,24 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<el-config-provider :size="size" :z-index="zIndex"> <el-config-provider :size="size" :z-index="zIndex" :locale="zhTW">
<GlobalAlerts placement="top-right" />
<div class="common-layout"> <div class="common-layout">
<el-container> <el-container>
<template v-if="!isMobile"> <template v-if="!isMobile">
<el-aside :width="isCollapse ? '0px' : '240px'"> <el-aside :width="isCollapse ? '0px' : '240px'">
<Sidebar :isCollapse="isCollapse" /> <Sidebar v-model:isCollapse="isCollapse" :isMobile="isMobile" />
</el-aside> </el-aside>
</template> </template>
<template v-else> <template v-else>
<el-drawer <el-drawer
v-if="isMobile"
v-model="isCollapse" v-model="isCollapse"
direction="ltr" direction="ltr"
:with-header="false" :with-header="false"
size="70%" size="70%"
body-class="mobile-sidebar-drawer" body-class="mobile-sidebar-drawer"
> >
<Sidebar :isCollapse="isCollapse" /> <Sidebar v-model:isCollapse="isCollapse" :isMobile="isMobile" />
</el-drawer> </el-drawer>
</template> </template>
<el-container> <el-container>

9
src/components.d.ts vendored
View File

@ -8,27 +8,35 @@ export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAside: typeof import('element-plus/es')['ElAside'] ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb']
ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElContainer: typeof import('element-plus/es')['ElContainer'] ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown'] ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElHeader: typeof import('element-plus/es')['ElHeader'] ElHeader: typeof import('element-plus/es')['ElHeader']
ElIcon: typeof import('element-plus/es')['ElIcon'] ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElMain: typeof import('element-plus/es')['ElMain'] ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption'] ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import('element-plus/es')['ElRow']
@ -39,6 +47,7 @@ declare module 'vue' {
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs'] ElTabs: typeof import('element-plus/es')['ElTabs']
ElTag: typeof import('element-plus/es')['ElTag']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
} }

View File

@ -0,0 +1,55 @@
<template>
<div class="alerts-container" :class="position">
<transition-group name="alert-slide" tag="div">
<el-alert
v-for="a in alerts"
:key="a.id"
:title="a.title"
:type="a.type"
:description="a.description"
:closable="a.closable"
:effect="a.effect || 'light'"
show-icon
@close="remove(a.id)"
class="alert-item"
/>
</transition-group>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useAlert } from '../../composables/useAlert';
const props = defineProps({
// 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
placement: { type: String, default: 'top-right' },
});
const { alerts, remove } = useAlert();
const position = computed(() => props.placement);
</script>
<style scoped>
.alerts-container {
position: fixed;
z-index: 3000;
width: 380px;
pointer-events: none; /* 讓下層可點擊;單個 alert 仍可關閉 */
}
.alerts-container :deep(.el-alert) {
pointer-events: auto;
}
.alert-item { margin-bottom: 10px; }
.top-right { top: 16px; right: 16px; }
.top-left { top: 16px; left: 16px; }
.bottom-right { bottom: 16px; right: 16px; }
.bottom-left { bottom: 16px; left: 16px; }
.alert-slide-enter-active,
.alert-slide-leave-active { transition: all .2s ease; }
.alert-slide-enter-from { opacity: 0; transform: translateY(-8px); }
.alert-slide-leave-to { opacity: 0; transform: translateY(-8px); }
</style>

View File

@ -0,0 +1,120 @@
<template>
<div class="paginated-table">
<div class="table-toolbar">
<slot name="toolbar" />
</div>
<el-table
ref="tableRef"
:data="displayData"
v-bind="tableProps"
:row-key="rowKey"
>
<slot />
<template #append>
<slot name="append" />
</template>
<template #empty>
<slot name="empty" />
</template>
</el-table>
<div class="table-pagination" v-if="showPagination">
<el-pagination
v-model:current-page="innerCurrentPage"
v-model:page-size="innerPageSize"
:total="resolvedTotal"
:page-sizes="pageSizes"
:layout="layout"
:background="background"
:size="size"
@current-change="onCurrentChange"
@size-change="onSizeChange"
/>
</div>
</div>
</template>
<script setup>
import { computed, ref, watch } from "vue";
const props = defineProps({
data: { type: Array, required: true },
currentPage: { type: Number, default: 1 },
pageSize: { type: Number, default: 10 },
total: { type: Number, default: undefined },
pageSizes: { type: Array, default: () => [5, 10, 20, 50] },
layout: { type: String, default: "total, sizes, prev, pager, next, jumper" },
background: { type: Boolean, default: true },
size: { type: String, default: "small" },
rowKey: { type: [String, Function], default: undefined },
tableProps: { type: Object, default: () => ({}) },
remote: { type: Boolean, default: false },
showPagination: { type: Boolean, default: true },
});
const emit = defineEmits([
"update:currentPage",
"update:pageSize",
"page-change",
"size-change",
"fetch", //
]);
const innerCurrentPage = ref(props.currentPage);
const innerPageSize = ref(props.pageSize);
watch(
() => props.currentPage,
(v) => {
if (v !== innerCurrentPage.value) innerCurrentPage.value = v;
}
);
watch(
() => props.pageSize,
(v) => {
if (v !== innerPageSize.value) innerPageSize.value = v;
}
);
const resolvedTotal = computed(() =>
typeof props.total === "number" ? props.total : props.data.length
);
const displayData = computed(() => {
if (props.remote) return props.data;
const start = (innerCurrentPage.value - 1) * innerPageSize.value;
return props.data.slice(start, start + innerPageSize.value);
});
function onCurrentChange(page) {
emit("update:currentPage", page);
emit("page-change", { page, pageSize: innerPageSize.value });
if (props.remote) emit("fetch", { page, pageSize: innerPageSize.value });
}
function onSizeChange(size) {
emit("update:pageSize", size);
// 1
if (innerCurrentPage.value !== 1) {
innerCurrentPage.value = 1;
emit("update:currentPage", 1);
}
emit("size-change", { page: innerCurrentPage.value, pageSize: size });
if (props.remote)
emit("fetch", { page: innerCurrentPage.value, pageSize: size });
}
const tableRef = ref();
defineExpose({ tableRef });
</script>
<style scoped>
.table-pagination {
display: flex;
justify-content: flex-end;
padding-top: 20px;
}
.table-toolbar:empty {
display: none;
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<el-dialog
:model-value="visible"
title="新增樣板"
modal-penetrable
style="max-width: 800px; width: 90%"
@close="onClose"
>
<!-- 選擇樣板 -->
<TemplateSelectDialog
v-model:visible="showTemplateDialog"
:templates="filteredTemplates"
@select="onTemplatePicked"
/>
<el-form :model="form">
<el-form-item label="電廠選擇">
<el-select v-model="form.factory">
<el-option label="四磺子坪" value="四磺子坪" />
<el-option label="硫磺子坪" value="硫磺子坪" />
<el-option label="大清水" value="大清水" />
<el-option label="小清水" value="小清水" />
</el-select>
</el-form-item>
<el-form-item label="樣板名稱">
<el-input v-model="form.templateName">
<template #append>
<el-button type="success" @click="onSetTemplate">設定</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="任務名稱">
<el-input v-model="form.taskName" />
</el-form-item>
<el-form-item label="填寫頻率">
<el-select v-model="form.frequency" placeholder="請選擇頻率">
<el-option label="每日" value="每日" />
<el-option label="每月" value="每月" />
<el-option label="單次" value="單次" />
</el-select>
</el-form-item>
<el-form-item label="填寫次數">
<el-input-number v-model="form.count" :min="1" />
</el-form-item>
<el-form-item label="審查方式">
<el-select v-model="form.reviewType" placeholder="請選擇審查方式">
<el-option label="1關" value="1關" />
<el-option label="2關" value="2關" />
<el-option label="3關" value="3關" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="onClose">取消</el-button>
<el-button type="primary" @click="onConfirm">確定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref } from 'vue';
import TemplateSelectDialog from './TemplateSelectDialog.vue';
import { useAlert } from '../../composables/useAlert';
const props = defineProps({
visible: { type: Boolean, default: false },
form: { type: Object, required: true },
templates: { type: Array, default: () => [] },
});
const emit = defineEmits([
"update:visible",
"confirm",
"close",
]);
const showTemplateDialog = ref(false);
const filteredTemplates = ref([]);
const alert = useAlert()
function onClose() {
emit("update:visible", false);
emit("close");
}
function onConfirm() {
emit("confirm");
}
function onSetTemplate() {
if (!props.form.factory) {
alert.error('請先選擇電廠');
return;
}else{
filteredTemplates.value = props.templates.filter(t => t.factory === props.form.factory);
showTemplateDialog.value = true;
}
}
function onTemplatePicked(row) {
//
props.form.templateName = row.templateName;
}
</script>
<style scoped>
.el-dialog__footer {
display: flex;
justify-content: flex-end;
gap: 16px;
}
</style>

View File

@ -0,0 +1,284 @@
<template>
<el-dialog
:model-value="visible"
title="新增樣板"
modal-penetrable
style="max-width: 800px; width: 90%"
@close="onClose"
>
<DutyLogItemDialog v-model:visible="showDutyLogDialog" @add="addDutyLog" />
<HandoverItemDialog
v-model:visible="showHandoverDialog"
@add="addHandover"
/>
<VerificationSettingDialog
v-model:visible="showVerificationDialog"
@add="addVerification"
/>
<InspectionItemDialog
v-model:visible="showInspectionDialog"
@add="addInspection"
/>
<el-form :model="form">
<el-divider content-position="left">樣板資訊</el-divider>
<el-row :gutter="20">
<el-col :xs="24" :sm="12">
<el-form-item label="電廠">
<el-select v-model="form.factory" placeholder="請選擇電廠">
<el-option label="四磺子坪" value="四磺子坪" />
<el-option label="硫磺子坪" value="硫磺子坪" />
<el-option label="大清水" value="大清水" />
<el-option label="小清水" value="小清水" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12">
<el-form-item label="模板">
<el-select v-model="form.template" placeholder="請選擇模板類型">
<el-option label="巡檢表" value="巡檢表" />
<el-option label="點檢表" value="點檢表" />
<el-option
label="高壓特每日自動檢查表"
value="高壓特每日自動檢查表"
/>
<el-option
label="高壓特每月自動檢查表"
value="高壓特每月自動檢查表"
/>
<el-option
label="一班作業安全許可申請表"
value="一班作業安全許可申請表"
/>
<el-option
label="侷限作業安全許可申請表"
value="侷限作業安全許可申請表"
/>
<el-option
label="動火作業安全許可申請表"
value="動火作業安全許可申請表"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12">
<el-form-item label="母版">
<el-select v-model="form.isParent" placeholder="請選擇">
<el-option label="是" value="是" />
<el-option label="否" value="否" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24">
<el-form-item label="樣板名稱">
<el-input
v-model="form.templateName"
placeholder="請輸入樣板名稱"
/>
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">樣板內容</el-divider>
<el-row :gutter="20">
<el-col :xs="24" :sm="24">
<el-form-item label="案場名稱">
<el-input />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12">
<el-form-item label="值班人員">
<el-input />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12">
<el-form-item label="班別">
<el-input />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12">
<el-form-item label="天氣">
<el-input />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12">
<el-form-item label="環境溫度/濕度">
<el-input />
</el-form-item>
</el-col>
</el-row>
<el-row :align="'middle'" justify="space-between">
<h3>值班日誌</h3>
<el-button type="primary" size="small" @click="showDutyLogDialog = true"
>新增日誌項目</el-button
>
<el-table :data="dutyLogTableData" :border="true">
<el-table-column prop="index" label="項次" width="60" />
<el-table-column prop="change" label="變更內容" />
<el-table-column prop="continue" label="持續" />
<el-table-column prop="result" label="結案" />
<el-table-column prop="remark" label="備註" />
<el-table-column
label="功能"
min-width="80"
width="140"
fixed="right"
>
<template #default>
<el-button size="small" type="primary" plain>修改</el-button>
<el-button size="small" type="danger" plain>刪除</el-button>
</template>
</el-table-column>
</el-table>
</el-row>
<el-row :align="'middle'" justify="space-between">
<h3>交接事項</h3>
<el-button
type="primary"
size="small"
@click="showHandoverDialog = true"
>新增交接事項</el-button
>
<el-table :data="handoverTableData" :border="true">
<el-table-column prop="index" label="項次" width="60" />
<el-table-column prop="content" label="工作內容" />
<el-table-column prop="continue" label="持續" />
<el-table-column prop="result" label="結案" />
<el-table-column prop="remark" label="備註" />
<el-table-column
label="功能"
min-width="80"
width="140"
fixed="right"
>
<template #default>
<el-button size="small" type="primary" plain>修改</el-button>
<el-button size="small" type="danger" plain>刪除</el-button>
</template>
</el-table-column>
</el-table>
</el-row>
<el-row :align="'middle'" justify="space-between">
<h3>巡檢表</h3>
<el-button
type="primary"
size="small"
@click="showInspectionDialog = true"
>新增巡檢表</el-button
>
</el-row>
<PaginatedTable
:data="inspectionTableData"
:page-sizes="[5, 10]"
row-key="index"
>
<el-table-column prop="index" label="項次" width="60" />
<el-table-column prop="system1" label="系統1" />
<el-table-column prop="system2" label="系統2" />
<el-table-column prop="deviceId" label="設備編號" />
<el-table-column prop="item" label="項目" />
<el-table-column prop="unit" label="單位" />
<el-table-column label="功能" width="140" fixed="right">
<template #default>
<el-button size="small" type="primary" plain>修改</el-button>
<el-button size="small" type="danger" plain>刪除</el-button>
</template>
</el-table-column>
</PaginatedTable>
<el-divider content-position="left">樣板查證</el-divider>
<el-row :align="'middle'" justify="space-between">
<h3>查證</h3>
<el-button
type="primary"
size="small"
@click="showVerificationDialog = true"
>新增查證設定</el-button
>
<el-table :data="verificationTableData" :border="true">
<el-table-column prop="index" label="項次" width="60" />
<el-table-column prop="fieldName" label="欄位名稱" />
<el-table-column prop="type" label="類型" />
<el-table-column
label="功能"
min-width="80"
width="140"
fixed="right"
>
<template #default>
<el-button size="small" type="primary" plain>修改</el-button>
<el-button size="small" type="danger" plain>刪除</el-button>
</template>
</el-table-column>
</el-table>
</el-row>
</el-form>
<template #footer>
<el-button @click="onClose">取消</el-button>
<el-button type="primary" @click="onConfirm">確定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import PaginatedTable from "../Common/PaginatedTable.vue";
import DutyLogItemDialog from "./DutyLogItemDialog.vue";
import HandoverItemDialog from "./HandoverItemDialog.vue";
import VerificationSettingDialog from "./VerificationSettingDialog.vue";
import InspectionItemDialog from "./InspectionItemDialog.vue";
const props = defineProps({
visible: { type: Boolean, default: false },
form: { type: Object, required: true },
dutyLogTableData: { type: Array, default: () => [] },
handoverTableData: { type: Array, default: () => [] },
inspectionTableData: { type: Array, default: () => [] },
verificationTableData: { type: Array, default: () => [] },
});
const emit = defineEmits(["update:visible", "confirm", "close"]);
function onClose() {
emit("update:visible", false);
emit("close");
}
function onConfirm() {
emit("confirm");
}
// Dialog
import { ref } from "vue";
const showDutyLogDialog = ref(false);
const showHandoverDialog = ref(false);
const showVerificationDialog = ref(false);
const showInspectionDialog = ref(false);
//
function addDutyLog(item) {
const arr = props.dutyLogTableData;
arr.push({ index: arr.length + 1, ...item });
}
function addHandover(item) {
const arr = props.handoverTableData;
arr.push({ index: arr.length + 1, ...item });
}
function addVerification(item) {
const arr = props.verificationTableData;
arr.push({ index: arr.length + 1, ...item });
}
function addInspection(item) {
const arr = props.inspectionTableData;
arr.push({ index: arr.length + 1, ...item });
}
</script>
<style scoped>
.el-divider--horizontal {
margin-top: 30px;
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<el-dialog
:model-value="visible"
title="新增值班日誌項目"
width="480px"
@close="close"
>
<el-form :model="form" label-width="90px">
<el-form-item label="變更內容">
<el-input v-model="form.change" />
</el-form-item>
<el-form-item label="持續">
<el-input v-model="form.continue" />
</el-form-item>
<el-form-item label="結案">
<el-input v-model="form.result" />
</el-form-item>
<el-form-item label="備註">
<el-input v-model="form.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="confirm">加入</el-button>
</template>
</el-dialog>
</template>
<script setup>
const props = defineProps({
visible: { type: Boolean, default: false },
});
const emit = defineEmits(["update:visible", "add"]);
import { reactive } from "vue";
const form = reactive({ change: "", continue: "", result: "", remark: "" });
function reset() {
form.change = "";
form.continue = "";
form.result = "";
form.remark = "";
}
function close() {
emit("update:visible", false);
reset();
}
function confirm() {
emit("add", { ...form });
close();
}
</script>
<style scoped></style>

View File

@ -0,0 +1,48 @@
<template>
<el-dialog
:model-value="visible"
title="新增交接事項"
width="480px"
@close="close"
>
<el-form :model="form" label-width="90px">
<el-form-item label="工作內容">
<el-input v-model="form.content" />
</el-form-item>
<el-form-item label="持續">
<el-input v-model="form.continue" />
</el-form-item>
<el-form-item label="結案">
<el-input v-model="form.result" />
</el-form-item>
<el-form-item label="備註">
<el-input v-model="form.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="confirm">加入</el-button>
</template>
</el-dialog>
</template>
<script setup>
const props = defineProps({ visible: { type: Boolean, default: false } });
const emit = defineEmits(["update:visible", "add"]);
import { reactive } from "vue";
const form = reactive({ content: "", continue: "", result: "", remark: "" });
function reset() {
form.content = "";
form.continue = "";
form.result = "";
form.remark = "";
}
function close() {
emit("update:visible", false);
reset();
}
function confirm() {
emit("add", { ...form });
close();
}
</script>
<style scoped></style>

View File

@ -0,0 +1,128 @@
<template>
<el-dialog
:model-value="visible"
title="新增巡檢表項目"
width="520px"
@close="close"
>
<el-form :model="form" label-width="70px">
<el-form-item label="系統1">
<el-input v-model="form.system1" />
</el-form-item>
<el-form-item label="系統2">
<el-input v-model="form.system2" />
</el-form-item>
<el-form-item label="設備編號">
<el-input v-model="form.deviceId" />
</el-form-item>
<el-form-item label="項目">
<el-input v-model="form.item" />
</el-form-item>
<el-form-item label="單位">
<el-input v-model="form.unit" />
</el-form-item>
<el-divider content-position="left">下限</el-divider>
<el-row :gutter="20" >
<el-col :xs="24" :sm="12">
<el-form-item label="類型">
<el-select v-model="form.lowerType" >
<el-option label="數值輸入" value="" />
<el-option label="數值判斷(下限)" value="lower" />
<el-option label="數值判斷(上限)" value="" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12">
<el-form-item label="數值">
<el-input v-model="form.lowerValue" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">上限</el-divider>
<el-row :gutter="20" >
<el-col :xs="24" :sm="12">
<el-form-item label="類型">
<el-select v-model="form.upperType" >
<el-option label="數值輸入" value="" />
<el-option label="數值判斷(下限)" value="lower" />
<el-option label="數值判斷(上限)" value="" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12">
<el-form-item label="數值">
<el-input v-model="form.upperValue" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">DCS</el-divider>
<el-row :gutter="20" align="middle">
<el-col :xs="24" :sm="12">
<el-form-item label="系統">
<el-select v-model="form.dcsSystem" >
<el-option label="生產井" value="生產井" />
<el-option label="潤滑系統" value="潤滑系統" />
<el-option label="空氣系統" value="空氣系統" />
<el-option label="渦輪系統" value="渦輪系統" />
<el-option label="冷凝器系統" value="冷凝器系統" />
<el-option label="蒸發器系統" value="蒸發器系統" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12">
<el-form-item label="設備">
<el-select v-model="form.dcsDevice">
<el-option label="TE 1004" value="TE 1004" />
<el-option label="TG 1005" value="TG 1004" />
<el-option label="PT 1004" value="PT 1004" />
<el-option label="PG 1004" value="PG 1004" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12">
<el-form-item label="點位">
<el-select v-model="form.dcsPoint" >
<el-option label="溫度" value="溫度" />
<el-option label="壓力" value="壓力" />
<el-option label="流量" value="流量" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="confirm">加入</el-button>
</template>
</el-dialog>
</template>
<script setup>
const props = defineProps({ visible: { type: Boolean, default: false } });
const emit = defineEmits(["update:visible", "add"]);
import { reactive } from "vue";
const form = reactive({
system1: "",
system2: "",
deviceId: "",
item: "",
unit: "",
});
function reset() {
form.system1 = "";
form.system2 = "";
form.deviceId = "";
form.item = "";
form.unit = "";
}
function close() {
emit("update:visible", false);
reset();
}
function confirm() {
emit("add", { ...form });
close();
}
</script>
<style scoped></style>

View File

@ -0,0 +1,239 @@
<template>
<div>
<el-row justify="end">
<el-button type="primary" :icon="Plus" @click="showDialog = true"
>新增任務</el-button
>
</el-row>
<!-- 新增任務 -->
<AddTaskDialog
v-model:visible="showDialog"
:form="taskForm"
:templates="templateList"
@confirm="handleAddTask"
@close="resetTaskForm"
/>
<PaginatedTable
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:data="tableData"
:page-sizes="[5, 10]"
row-key="index"
>
<el-table-column prop="index" label="項次" width="60" />
<el-table-column
prop="factory"
label="電廠"
:filters="factoryFilters"
:filter-method="filterFactory"
filter-placement="bottom-end"
/>
<el-table-column
prop="category"
label="類別"
:filters="categoryFilters"
:filter-method="filterCategory"
filter-placement="bottom-end"
/>
<el-table-column prop="taskName" label="任務名稱" />
<el-table-column prop="frequency" label="巡檢頻率" />
<el-table-column prop="count" label="巡檢次數" />
<el-table-column prop="reviewType" label="審查方式" />
<el-table-column prop="creator" label="建立者" />
<el-table-column prop="createdAt" label="建立時間" />
<el-table-column label="功能" width="180" fixed="right">
<template #default>
<el-button size="small" type="primary" :icon="Edit">修改</el-button>
<el-button size="small" type="danger" :icon="Delete">刪除</el-button>
</template>
</el-table-column>
</PaginatedTable>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
import { templateList } from "../../constants/templateList.js";
import PaginatedTable from "../Common/PaginatedTable.vue";
import AddTaskDialog from "./AddTaskDialog.vue";
import { Plus, Delete, Edit } from "@element-plus/icons-vue";
const currentPage = ref(1);
const pageSize = ref(10);
const tableData = [
{
index: 1,
factory: "四磺子坪",
category: "巡檢表",
taskName: "四磺子坪1.2MW地熱能ORC發電機",
frequency: "每日",
count: 1,
reviewType: "2關",
creator: "管理員01",
createdAt: "2025-11-01 08:23:41",
},
{
index: 2,
factory: "四磺子坪",
category: "巡檢表",
taskName: "四磺子坪蒸汽機",
frequency: "每日",
count: 1,
reviewType: "1關",
creator: "管理員01",
createdAt: "2025-11-02 14:55:09",
},
{
index: 3,
factory: "四磺子坪",
category: "巡檢表",
taskName: "四磺子坪設備01",
frequency: "每日",
count: 1,
reviewType: "1關",
creator: "管理員01",
createdAt: "2025-11-03 09:12:36",
},
{
index: 4,
factory: "四磺子坪",
category: "巡檢表",
taskName: "四磺子坪設備02",
frequency: "每日",
count: 1,
reviewType: "1關",
creator: "管理員01",
createdAt: "2025-11-04 16:47:52",
},
{
index: 5,
factory: "四磺子坪",
category: "點檢表",
taskName: "四磺子坪1.2MW先導地熱能ORC發電機",
frequency: "每月",
count: 4,
reviewType: "2關",
creator: "管理員01",
createdAt: "2025-11-06 10:31:18",
},
{
index: 6,
factory: "四磺子坪",
category: "高壓特每日自動檢查表",
taskName: "四磺子坪高壓特每日檢點臥式蒸發器",
frequency: "每日",
count: 1,
reviewType: "1關",
creator: "管理員02",
createdAt: "2025-11-08 13:05:44",
},
{
index: 7,
factory: "四磺子坪",
category: "高壓特每月自動檢查表",
taskName: "四磺子坪高壓特每月檢點臥式蒸發器",
frequency: "每月",
count: 1,
reviewType: "2關",
creator: "管理員02",
createdAt: "2025-11-09 11:26:57",
},
{
index: 8,
factory: "四磺子坪",
category: "一般作業安全許可申請表",
taskName: "廠房01維修工程",
frequency: "單次",
count: 1,
reviewType: "1關",
creator: "管理員02",
createdAt: "2025-11-10 15:42:33",
},
{
index: 9,
factory: "四磺子坪",
category: "侷限作業安全許可申請表",
taskName: "廠房02維修工程",
frequency: "單次",
count: 1,
reviewType: "2關",
creator: "管理員03",
createdAt: "2025-11-12 09:58:21",
},
{
index: 10,
factory: "四磺子坪",
category: "動火作業安全許可申請表",
taskName: "廠房03維修工程",
frequency: "單次",
count: 1,
reviewType: "2關",
creator: "管理員03",
createdAt: "2025-11-13 17:14:05",
},
];
//
const factoryFilters = computed(() =>
Array.from(new Set(tableData.map((item) => item.factory))).map((factory) => ({
text: factory,
value: factory,
}))
);
const categoryFilters = computed(() =>
Array.from(new Set(tableData.map((item) => item.category))).map(
(category) => ({
text: category,
value: category,
})
)
);
const filterFactory = (value, row) => row.factory === value;
const filterCategory = (value, row) => row.category === value;
// dialog
const showDialog = ref(false);
const taskForm = ref({
factory: "",
templateName: "",
taskName: "",
frequency: "",
count: 1,
reviewType: "",
});
function resetTaskForm() {
taskForm.value = {
factory: "",
templateName: "",
taskName: "",
frequency: "",
count: 1,
reviewType: "",
};
showDialog.value = false;
}
function handleAddTask() {
// tableData
tableData.push({
index: tableData.length + 1,
factory: taskForm.value.factory,
category: taskForm.value.templateName
? taskForm.value.templateName.split("每日").length > 1
? "點檢表"
: "巡檢表"
: "",
taskName: taskForm.value.taskName,
frequency: taskForm.value.frequency,
count: taskForm.value.count,
reviewType: taskForm.value.reviewType,
creator: "管理員01",
createdAt: new Date().toISOString().slice(0, 19).replace("T", " "),
});
resetTaskForm();
}
// AddTaskDialog
</script>
<style scoped></style>

View File

@ -0,0 +1,235 @@
<template>
<div>
<!-- 樣板管理內容 -->
<el-row justify="end">
<el-button type="primary" :icon="Plus" @click="showDialog = true"
>新增樣板</el-button
>
</el-row>
<AddTemplateDialog
v-model:visible="showDialog"
:form="addTemplateForm"
:duty-log-table-data="dutyLogTableData"
:handover-table-data="handoverTableData"
:inspection-table-data="inspectionTableData"
:verification-table-data="verificationTableData"
@confirm="handleAddTemplate"
@close="resetAddTemplateForm"
/>
<!-- 樣板列表 table共用分頁表格元件 -->
<PaginatedTable
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:data="tableData"
:page-sizes="[5, 10]"
row-key="index"
>
<el-table-column prop="index" label="項次" width="60" />
<el-table-column
prop="factory"
label="電廠"
width="200"
:filters="factoryFilters"
:filter-method="filterFactory"
filter-placement="bottom-end"
/>
<el-table-column
prop="template"
label="模板"
:filters="templateFilters"
:filter-method="filterTemplate"
filter-placement="bottom-end"
/>
<el-table-column prop="templateName" label="樣板名稱" />
<el-table-column prop="isParent" label="母版" width="100">
<template #default="scope">
<el-tag
:type="scope.row.isParent === '是' ? 'success' : 'danger'"
disable-transitions
>
{{ scope.row.isParent }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="功能" width="320" fixed="right">
<template #default>
<el-button size="small" type="info" :icon="View">預覽</el-button>
<el-button size="small" type="success" :icon="CopyDocument"
>複製</el-button
>
<el-button size="small" type="primary" :icon="Edit">修改</el-button>
<el-button size="small" type="danger" :icon="Delete">刪除</el-button>
</template>
</el-table-column>
</PaginatedTable>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
import {
Plus,
Delete,
Edit,
CopyDocument,
View,
} from "@element-plus/icons-vue";
import PaginatedTable from "../Common/PaginatedTable.vue";
import AddTemplateDialog from "./AddTemplateDialog.vue";
import { templateList } from "../../constants/templateList.js";
//
const showDialog = ref(false);
//
const addTemplateForm = ref({
factory: "",
template: "",
templateName: "",
isParent: "",
});
function resetAddTemplateForm() {
addTemplateForm.value = {
factory: "",
template: "",
templateName: "",
isParent: "",
};
showDialog.value = false;
}
function handleAddTemplate() {
// tableData API
resetAddTemplateForm();
}
//
const inspectionTableData = ref([
{
index: 1,
system1: "熱源系統",
system2: "#2生產井",
deviceId: "TE_1004",
item: "#2地熱井溫度",
unit: "°C",
},
{
index: 2,
system1: "熱源系統",
system2: "#2生產井",
deviceId: "TG_1004",
item: "#2地熱井溫度表",
unit: "°C",
},
{
index: 3,
system1: "熱源系統",
system2: "#2生產井",
deviceId: "PT_1004",
item: "#2地熱井壓力",
unit: "bar",
},
{
index: 4,
system1: "熱源系統",
system2: "#2生產井",
deviceId: "PG_1004",
item: "#2地熱井壓力表",
unit: "kg/cm2",
},
{
index: 5,
system1: "熱源系統",
system2: "#2生產井",
deviceId: "",
item: "H2S洩漏偵測",
unit: "ppm",
},
{
index: 6,
system1: "熱源系統",
system2: "#3生產井",
deviceId: "TE_1005",
item: "#3地熱井溫度",
unit: "°C",
},
{
index: 7,
system1: "熱源系統",
system2: "#3生產井",
deviceId: "TG_1005",
item: "#3地熱井溫度表",
unit: "°C",
},
{
index: 8,
system1: "熱源系統",
system2: "#3生產井",
deviceId: "PT_1005",
item: "#3地熱井壓力",
unit: "bar",
},
{
index: 9,
system1: "熱源系統",
system2: "#3生產井",
deviceId: "PG_1005",
item: "#3地熱井壓力表",
unit: "kg/cm2",
},
{
index: 10,
system1: "熱源系統",
system2: "#3生產井",
deviceId: "",
item: "H2S洩漏偵測",
unit: "ppm",
},
]);
// table
const dutyLogTableData = ref([
{ index: 1, change: "", continue: "", result: "", remark: "" },
]);
// table
const handoverTableData = ref([
{ index: 1, content: "", continue: "", result: "", remark: "" },
]);
// table
const verificationTableData = ref([
{ index: 1, fieldName: "現場", type: "數值輸入" },
{ index: 2, fieldName: "比對結果", type: "單一選擇" },
]);
//
const currentPage = ref(1);
const pageSize = ref(10);
//
const tableData = templateList;
//
const factoryFilters = computed(() =>
Array.from(new Set(tableData.map((item) => item.factory))).map((factory) => ({
text: factory,
value: factory,
}))
);
const templateFilters = computed(() =>
Array.from(new Set(tableData.map((item) => item.template))).map(
(template) => ({ text: template, value: template })
)
);
//
const filterFactory = (value, row) => row.factory === value;
const filterTemplate = (value, row) => row.template === value;
</script>
<style scoped></style>

View File

@ -0,0 +1,77 @@
<template>
<el-dialog
:model-value="visible"
title="選擇樣板"
width="900px"
@close="onClose"
>
<PaginatedTable
:data="templates"
:page-sizes="[5, 10]"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
row-key="index"
>
<el-table-column prop="index" label="項次" width="60" />
<el-table-column
prop="template"
label="模板"
:filters="templateFilters"
:filter-method="filterTemplate"
/>
<el-table-column prop="templateName" label="模板名稱" />
<el-table-column prop="isParent" label="母版" width="80">
<template #default="scope">
<span>{{ scope.row.isParent }}</span>
</template>
</el-table-column>
<el-table-column label="功能" width="100" fixed="right">
<template #default="scope">
<el-button
type="primary"
size="small"
@click="selectTemplate(scope.row)"
>選擇</el-button
>
</template>
</el-table-column>
</PaginatedTable>
</el-dialog>
</template>
<script setup>
import { ref, computed } from "vue";
import PaginatedTable from "../Common/PaginatedTable.vue";
const props = defineProps({
visible: { type: Boolean, default: false },
templates: { type: Array, default: () => [] },
});
const emit = defineEmits(["update:visible", "select", "close"]);
const currentPage = ref(1);
const pageSize = ref(10);
function selectTemplate(row) {
emit("select", row);
emit("update:visible", false);
}
function onClose() {
emit("update:visible", false);
emit("close");
}
const templateFilters = computed(() =>
Array.from(new Set(props.templates.map((item) => item.template))).map(
(template) => ({ text: template, value: template })
)
);
const filterTemplate = (value, row) => row.template === value;
</script>
<style scoped>
/* .el-dialog__body {
padding: 16px 24px 0 24px;
} */
</style>

View File

@ -0,0 +1,44 @@
<template>
<el-dialog
:model-value="visible"
title="新增查證設定"
width="480px"
@close="close"
>
<el-form :model="form" label-width="90px">
<el-form-item label="欄位名稱">
<el-input v-model="form.fieldName" />
</el-form-item>
<el-form-item label="類型">
<el-select v-model="form.type" placeholder="選擇類型">
<el-option label="數值輸入" value="數值輸入" />
<el-option label="文字輸入" value="文字輸入" />
<el-option label="單一選擇" value="單一選擇" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="confirm">加入</el-button>
</template>
</el-dialog>
</template>
<script setup>
const props = defineProps({ visible: { type: Boolean, default: false } });
const emit = defineEmits(["update:visible", "add"]);
import { reactive } from "vue";
const form = reactive({ fieldName: "", type: "" });
function reset() {
form.fieldName = "";
form.type = "";
}
function close() {
emit("update:visible", false);
reset();
}
function confirm() {
emit("add", { ...form });
close();
}
</script>
<style scoped></style>

View File

@ -37,7 +37,7 @@
</template> </template>
<script setup> <script setup>
import { computed } from "vue"; import { computed, defineProps, defineEmits } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { menuConfig } from "../constants/menuConfig.js"; import { menuConfig } from "../constants/menuConfig.js";
import { import {
@ -53,6 +53,12 @@ import {
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const props = defineProps({
isCollapse: { type: Boolean, default: false },
isMobile: { type: Boolean, default: false },
});
const emit = defineEmits(["update:isCollapse"]);
// //
const iconComponents = { const iconComponents = {
@ -74,7 +80,14 @@ const getIconComponent = (iconName) => {
// //
const handleMenuClick = (item) => { const handleMenuClick = (item) => {
if (item.routeName) { if (item.routeName) {
router.push({ name: item.routeName, params: item.params || {} }); Promise.resolve(
router.push({ name: item.routeName, params: item.params || {} })
).finally(() => {
//
if (props.isMobile) {
emit("update:isCollapse", false);
}
});
return; return;
} }
}; };

View File

@ -0,0 +1,54 @@
import { reactive, readonly } from 'vue';
// 單例狀態:全專案共用
const state = reactive({
// { id, title, type, description, closable, duration, effect }
alerts: [],
seq: 0,
});
function normalizeType(type) {
// Element Plus Alert 支援: success | info | warning | error
if (type === 'primary') return 'info';
return ['success', 'info', 'warning', 'error'].includes(type) ? type : 'info';
}
function addAlert({ title, type = 'info', description = '', closable = true, duration = 3000, effect = 'light' }) {
const id = ++state.seq;
const alert = { id, title, type: normalizeType(type), description, closable, duration, effect };
state.alerts.push(alert);
if (duration && duration > 0) {
setTimeout(() => removeAlert(id), duration);
}
return id;
}
function removeAlert(id) {
const idx = state.alerts.findIndex(a => a.id === id);
if (idx !== -1) state.alerts.splice(idx, 1);
}
function clearAlerts() {
state.alerts.splice(0, state.alerts.length);
}
export function useAlert() {
const notify = (title, options = {}) => addAlert({ title, ...options });
const success = (title, options = {}) => addAlert({ title, type: 'success', ...options });
const info = (title, options = {}) => addAlert({ title, type: 'info', ...options });
const warning = (title, options = {}) => addAlert({ title, type: 'warning', ...options });
const error = (title, options = {}) => addAlert({ title, type: 'error', ...options });
const primary = (title, options = {}) => addAlert({ title, type: 'primary', effect: 'dark', ...options });
return {
alerts: readonly(state.alerts),
notify,
success,
info,
warning,
error,
primary,
remove: removeAlert,
clear: clearAlerts,
};
}

View File

@ -1,104 +1,104 @@
export const menuConfig = [ export const menuConfig = [
{ {
title: '總覽', title: "總覽",
icon: 'Location', icon: "Location",
children: [ children: [
{ {
title: '地圖總覽', title: "地圖總覽",
routeName: 'PlantsMap' routeName: "PlantsMap",
}, },
{ {
title: '電廠總覽', title: "電廠總覽",
routeName: 'PlantsOverview' routeName: "PlantsOverview",
} },
] ],
}, },
{ {
title: '電廠資訊', title: "電廠資訊",
icon: 'Postcard', icon: "Postcard",
children: [ children: [
{ {
title: '四磺子坪', title: "四磺子坪",
routeName: 'PlantInfo', routeName: "PlantInfo",
params: { id: 1 } params: { id: 1 },
}, },
{ {
title: '宜蘭大清水', title: "宜蘭大清水",
routeName: 'PlantInfo', routeName: "PlantInfo",
params: { id: 2 } params: { id: 2 },
}, },
{ {
title: '宜蘭小清水', title: "宜蘭小清水",
routeName: 'PlantInfo', routeName: "PlantInfo",
params: { id: 3 } params: { id: 3 },
} },
] ],
}, },
{ {
title: '巡檢系統', title: "巡檢系統",
icon: 'DocumentChecked', icon: "DocumentChecked",
children: [ children: [
{ {
title: '巡檢任務', title: "巡檢設定",
routeName: 'PatrolMission', routeName: "PatrolSetting",
}, },
{ {
title: '巡檢設定', title: "巡檢任務",
routeName: 'PatrolSetting', routeName: "PatrolMission",
} },
] ],
}, },
{ {
title: '報表查詢', title: "報表查詢",
icon: 'DataLine', icon: "DataLine",
children: [ children: [
{ {
title: '電廠報表', title: "電廠報表",
routeName: 'PlantsReport' routeName: "PlantsReport",
} },
] ],
}, },
{ {
title: '即時告警', title: "即時告警",
icon: 'Bell', icon: "Bell",
children: [ children: [
{ {
title: '異常事件查詢' title: "異常事件查詢",
} },
] ],
}, },
{ {
title: '備料管理', title: "備料管理",
icon: 'MessageBox', icon: "MessageBox",
children: [ children: [
{ {
title: '備品料件管理' title: "備品料件管理",
}, },
{ {
title: '倉庫櫃位管理' title: "倉庫櫃位管理",
} },
] ],
}, },
{ {
title: '智慧安防', title: "智慧安防",
icon: 'VideoCamera', icon: "VideoCamera",
children: [ children: [
{ {
title: '安防系統' title: "安防系統",
} },
] ],
}, },
{ {
title: '系統設定', title: "系統設定",
icon: 'Setting', icon: "Setting",
children: [ children: [
{ {
title: '電廠設定', title: "電廠設定",
routeName: 'PlantSetting' routeName: "PlantSetting",
}, },
{ {
title: '帳號設定' title: "帳號設定",
} },
] ],
} },
]; ];

View File

@ -0,0 +1,72 @@
export const templateList = [
{
index: 1,
factory: "四磺子坪",
template: "巡檢表",
templateName: "樣板-巡檢表-四磺子坪地熱能發電機組v1.0",
isParent: "是",
},
{
index: 2,
factory: "四磺子坪",
template: "巡檢表",
templateName: "樣板-巡檢表-四磺子坪地熱能發電機組v1.1",
isParent: "否",
},
{
index: 3,
factory: "小清水",
template: "巡檢表",
templateName: "樣板-巡檢表-小清水地熱能發電機組",
isParent: "否",
},
{
index: 4,
factory: "硫磺子坪",
template: "巡檢表",
templateName: "樣板-測試03",
isParent: "否",
},
{
index: 5,
factory: "四磺子坪",
template: "點檢表",
templateName: "樣板-四磺子坪先導地熱發電站每日點檢點表",
isParent: "是",
},
{
index: 6,
factory: "四磺子坪",
template: "高壓特每日自動檢查表",
templateName: "樣板-四磺子坪先導地熱發電站高壓特每日點檢點表",
isParent: "是",
},
{
index: 7,
factory: "四磺子坪",
template: "高壓特每月自動檢查表",
templateName: "樣板-四磺子坪先導地熱發電站高壓特每月定檢表",
isParent: "是",
},
{
index: 8,
factory: "四磺子坪",
template: "一般作業安全許可申請表",
templateName: "樣板-一般施工安全許可申請表",
isParent: "是",
},
{
index: 9,
factory: "四磺子坪",
template: "侷限作業安全許可申請表",
templateName: "樣板-局限空間缺氧危險作業進入許可申請表",
isParent: "是",
},
{
index: 10,
factory: "四磺子坪",
template: "動火作業安全許可申請表",
templateName: "樣板-動火作業安全許可申請表",
isParent: "是",
},
];

View File

@ -1,10 +1,12 @@
import { createApp } from "vue"; import { createApp } from "vue";
import "./styles/style.css";
import App from "./App.vue"; import App from "./App.vue";
import Vue3Signature from "vue3-signature";
import router from "./router"; import router from "./router";
import { createPinia } from "pinia"; import { createPinia } from "pinia";
import "./styles/style.css";
const app = createApp(App); const app = createApp(App);
app.use(createPinia()); app.use(createPinia());
app.use(Vue3Signature);
app.use(router); app.use(router);
app.mount("#app"); app.mount("#app");

View File

@ -9,9 +9,15 @@
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
/* --el-color-primary: #df2cff;
--el-color-primary-light-3: #b266ff;
--el-color-primary-light-5: #c299ff;
--el-color-primary-light-7: #d1ccff;
--el-color-primary-light-8: #e0e0ff;
--el-color-primary-light-9: #f0f3ff;
--el-color-primary-dark-2: #a600e6; */
} }
body { body {
margin: 0; margin: 0;
min-height: 100vh; min-height: 100vh;
@ -26,16 +32,15 @@ body {
/* 統一 el-card 樣式 */ /* 統一 el-card 樣式 */
.el-card.custom-card { .el-card.custom-card {
border-radius: 6px; border-radius: 6px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,0.08); box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
background: #fff; background: #fff;
border: none; border: none;
margin-bottom: 20px; margin-bottom: 20px;
} }
.el-card.custom-card .el-card__header{ .el-card.custom-card .el-card__header {
font-weight: 600; font-weight: 600;
font-size: 1.2rem; font-size: 1.2rem;
letter-spacing: 0.8px; letter-spacing: 0.8px;
color: #213547; color: #213547;
} }

View File

@ -1,9 +1,85 @@
<template> <template>
<el-row :gutter="20" style="margin-top: 20px">
<el-col :xs="24">
<el-card shadow="always" class="custom-card">
<template #header>
<el-radio-group v-model="radio" size="large" fill="#409eff">
<el-radio-button label="待完成" value="pending" />
<el-radio-button label="已完成" value="completed" />
</el-radio-group>
</template>
<div v-if="radio === 'pending'">
<!-- 待完成內容 -->
<el-button-group>
<el-button :icon="TakeawayBox" @click="save('image/png')"
>保存PNG</el-button
>
<el-button :icon="TakeawayBox" @click="save('image/jpeg')"
>保存JPEG</el-button
>
<el-button :icon="Loading" @click="clear">清除</el-button>
<el-button :icon="RefreshLeft" @click="undo">返回</el-button>
</el-button-group>
<Vue3Signature
ref="signature"
:sigOption="options"
:w="'300px'"
:h="'150px'"
class="signature-pad"
/>
</div>
<div v-if="radio === 'completed'">
<!-- 已完成內容 -->
已完成任務列表
</div>
</el-card>
</el-col>
</el-row>
</template> </template>
<script setup> <script setup>
import { ref, computed } from "vue"; import { ref, computed, reactive } from "vue";
import { TakeawayBox, Loading, RefreshLeft } from "@element-plus/icons-vue";
import dayjs from "dayjs";
const radio = ref("pending");
const signature = ref(null);
const options = reactive({
penColor: "rgb(0, 0, 0)",
backgroundColor: "rgb(255, 255, 255)",
});
const save = (format) => {
if (signature.value.isEmpty()) {
alert("Please provide a signature first.");
return;
}
const dataUrl = signature.value.save(format);
// Download the image
const link = document.createElement("a");
const extension = format === "image/jpeg" ? "jpg" : "png";
//
const nowStr = dayjs().format("YYYYMMDD_HHmmss");
link.download = `signature_${nowStr}.${extension}`;
link.href = dataUrl;
link.click();
};
const clear = () => {
signature.value.clear();
};
const undo = () => {
signature.value.undo();
};
</script> </script>
<style scoped></style> <style scoped>
.signature-pad {
border: 1px solid #e0e0e0;
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); */
}
</style>

View File

@ -7,14 +7,13 @@
<el-radio-button label="樣板管理" value="template" /> <el-radio-button label="樣板管理" value="template" />
<el-radio-button label="任務管理" value="task" /> </el-radio-group <el-radio-button label="任務管理" value="task" /> </el-radio-group
></template> ></template>
<!-- 樣板管理 -->
<div v-if="radio === 'template'"> <div v-if="radio === 'template'">
<!-- 樣板管理內容 --> <TemplateManager />
<p>這裡是樣板管理</p>
</div> </div>
<!-- 任務管理 -->
<div v-if="radio === 'task'"> <div v-if="radio === 'task'">
<!-- 任務管理內容 --> <TaskManager />
<p>這裡是任務管理</p>
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
@ -23,6 +22,8 @@
<script setup> <script setup>
import { ref } from "vue"; import { ref } from "vue";
import TemplateManager from "../components/PatrolSetting/TemplateManager.vue";
import TaskManager from "../components/PatrolSetting/TaskManager.vue";
const radio = ref("template"); const radio = ref("template");
</script> </script>

View File

@ -11,6 +11,9 @@ export default defineConfig({
outDir: "../dist", outDir: "../dist",
emptyOutDir: true, emptyOutDir: true,
}, },
server: {
port: 3000,
},
plugins: [ plugins: [
vue(), vue(),
AutoImport({ AutoImport({