Compare commits

...

2 Commits

18 changed files with 2661 additions and 434 deletions

4
src/components.d.ts vendored
View File

@ -30,8 +30,10 @@ declare module 'vue' {
ElFormItem: typeof import('element-plus/es')['ElFormItem'] 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']
ElImage: typeof import('element-plus/es')['ElImage']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElLink: typeof import('element-plus/es')['ElLink']
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']
@ -48,6 +50,8 @@ declare module 'vue' {
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'] ElTag: typeof import('element-plus/es')['ElTag']
ElText: typeof import('element-plus/es')['ElText']
ElUpload: typeof import('element-plus/es')['ElUpload']
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,238 @@
<template>
<!-- 已完成任務列表 -->
<div class="filter-section">
<el-row :gutter="20">
<el-col :xs="24" :md="6">
<el-form-item label="電廠:">
<el-select
v-model="filterFactory"
placeholder="選擇電廠"
clearable
>
<el-option label="四磺子坪" value="四磺子坪" />
<el-option label="宜蘭大清水" value="宜蘭大清水" />
<el-option label="宜蘭小清水" value="宜蘭小清水" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="6">
<el-form-item label="類別:">
<el-select
v-model="filterCategory"
placeholder="選擇類別"
clearable
>
<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" :md="10">
<el-form-item label="任務日期:">
<el-date-picker
v-model="dateRange"
type="daterange"
:shortcuts="shortcuts"
range-separator="至"
start-placeholder="開始日期"
end-placeholder="結束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :md="2">
<el-button type="primary" @click="handleSearch" :icon="Search"
>查詢</el-button
>
</el-col>
</el-row>
</div>
<PaginatedTable
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:data="filteredData"
:page-sizes="[10, 20, 50]"
row-key="index"
:table-props="{ border: true }"
>
<el-table-column prop="index" label="項次" width="80" />
<el-table-column prop="factory" label="電廠" width="120" />
<el-table-column prop="category" label="類別" width="200" />
<el-table-column prop="taskName" label="任務名稱" min-width="300" />
<el-table-column prop="dispatchDate" label="派工日期" />
<el-table-column label="功能" width="100" fixed="right">
<template #default="scope">
<el-button type="success" size="small" :icon="Download">下載</el-button>
</template>
</el-table-column>
</PaginatedTable>
</template>
<script setup>
import { ref, computed } from "vue";
import { Search, Download } from "@element-plus/icons-vue";
import PaginatedTable from "../Common/PaginatedTable.vue";
const filterFactory = ref("四磺子坪");
const filterCategory = ref("");
const dateRange = ref("");
const currentPage = ref(1);
const pageSize = ref(10);
const shortcuts = [
{
text: "近7天",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
return [start, end];
},
},
{
text: "近30天",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
return [start, end];
},
},
{
text: "近3個月",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
return [start, end];
},
},
];
const tableData = ref([
{
index: 1,
factory: "四磺子坪",
category: "巡檢表",
taskName: "四磺子坪1.2MW地熱能ORC發電機",
dispatchDate: "2025-11-11",
},
{
index: 2,
factory: "四磺子坪",
category: "高壓特每日自動檢查表",
taskName: "四磺子坪高壓特每日檢點臥式蒸發器",
dispatchDate: "2025-11-11",
},
{
index: 3,
factory: "四磺子坪",
category: "巡檢表",
taskName: "四磺子坪1.2MW地熱能ORC發電機",
dispatchDate: "2025-11-10",
},
{
index: 4,
factory: "四磺子坪",
category: "高壓特每日自動檢查表",
taskName: "四磺子坪高壓特每日檢點臥式蒸發器",
dispatchDate: "2025-11-10",
},
{
index: 5,
factory: "四磺子坪",
category: "巡檢表",
taskName: "四磺子坪1.2MW地熱能ORC發電機",
dispatchDate: "2025-11-09",
},
{
index: 6,
factory: "四磺子坪",
category: "高壓特每日自動檢查表",
taskName: "四磺子坪高壓特每日檢點臥式蒸發器",
dispatchDate: "2025-11-09",
},
{
index: 7,
factory: "四磺子坪",
category: "巡檢表",
taskName: "四磺子坪1.2MW地熱能ORC發電機",
dispatchDate: "2025-11-08",
},
{
index: 8,
factory: "四磺子坪",
category: "高壓特每日自動檢查表",
taskName: "四磺子坪高壓特每日檢點臥式蒸發器",
dispatchDate: "2025-11-08",
},
{
index: 9,
factory: "四磺子坪",
category: "點檢表",
taskName: "四磺子坪1.2MW先導地熱能ORC發電機",
dispatchDate: "2025-11-08",
},
{
index: 10,
factory: "四磺子坪",
category: "高壓特每月自動檢查表",
taskName: "四磺子坪高壓特每月檢點臥式蒸發器",
dispatchDate: "2025-11-08",
},
]);
const filteredData = computed(() => {
let result = tableData.value;
//
if (filterFactory.value) {
result = result.filter((item) => item.factory === filterFactory.value);
}
//
if (filterCategory.value) {
result = result.filter((item) => item.category === filterCategory.value);
}
//
if (dateRange.value && dateRange.value.length === 2) {
const [startDate, endDate] = dateRange.value;
result = result.filter((item) => {
return item.dispatchDate >= startDate && item.dispatchDate <= endDate;
});
}
return result;
});
const handleSearch = () => {
console.log("搜尋條件:", {
factory: filterFactory.value,
category: filterCategory.value,
dateRange: dateRange.value,
});
};
</script>
<style scoped>
.filter-section {
background: #f5f7fa;
padding: 20px;
margin-bottom: 20px;
border-radius: 4px;
}
.filter-section .el-form-item {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,368 @@
<template>
<el-dialog
:model-value="visible"
:title="dialogTitle"
modal-penetrable
style="max-width: 800px; width: 90%"
@close="onClose"
>
<el-form :model="form" label-width="120px">
<!-- 動態渲染樣板內容欄位 -->
<template v-if="activeSchema">
<template v-for="section in activeSchema.sections" :key="section.title">
<el-divider content-position="left">{{ section.title }}</el-divider>
<el-row :gutter="20">
<el-col
v-for="field in section.fields"
:key="field.key"
:xs="24"
:sm="field.span"
>
<el-form-item :label="field.label">
<!-- 下拉選單 -->
<el-select
v-if="field.type === 'select'"
v-model="dynamicFields[field.key]"
:placeholder="`請選擇${field.label}`"
>
<el-option
v-for="option in field.options"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<!-- 多行文本 -->
<el-input
v-else-if="field.type === 'textarea'"
v-model="dynamicFields[field.key]"
type="textarea"
:rows="3"
:placeholder="`請輸入${field.label}`"
/>
<!-- 一般文本輸入 -->
<el-input
v-else
v-model="dynamicFields[field.key]"
:placeholder="`請輸入${field.label}`"
/>
</el-form-item>
</el-col>
</el-row>
</template>
<!-- 動態渲染表格 -->
<template v-for="table in activeSchema.tables" :key="table.key">
<el-divider content-position="left">{{ table.title }}</el-divider>
<el-row :align="'middle'" justify="space-between" style="margin-bottom: 10px;">
<h3>{{ table.title }}</h3>
<!-- 只有當 allowAdd 不為 false 時才顯示新增按鈕 -->
<el-button
v-if="table.allowAdd !== false && table.key !== 'inspection'"
type="primary"
size="small"
@click="openDialog(table.dialogType)"
>
{{ table.addButtonText }}
</el-button>
</el-row>
<!-- 使用分頁表格或一般表格 -->
<PaginatedTable
v-if="table.usePagination"
:data="dynamicTableData[table.key] || []"
:page-sizes="[10, 20]"
row-key="index"
>
<!-- 基本欄位 -->
<el-table-column
v-for="column in table.columns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:width="column.width"
>
<template #default="scope">
<span>{{ scope.row[column.prop] }}</span>
</template>
</el-table-column>
<!-- 動態查證欄位 (僅巡檢表) -->
<template v-if="table.useVerification && verificationFields.length > 0">
<el-table-column
v-for="field in verificationFields"
:key="field.fieldName"
:label="field.fieldName"
width="140"
fixed="right"
>
<template #default="scope">
<!-- 數值輸入 -->
<el-input
v-if="field.type === '數值輸入'"
v-model="scope.row[`verify_${field.fieldName}`]"
size="small"
type="number"
:placeholder="`請輸入${field.fieldName}`"
/>
<!-- 文字輸入 -->
<el-input
v-else-if="field.type === '文字輸入'"
v-model="scope.row[`verify_${field.fieldName}`]"
size="small"
:placeholder="`請輸入${field.fieldName}`"
/>
<!-- 單一選擇 -->
<el-radio-group
v-else-if="field.type === '單一選擇'"
v-model="scope.row[`verify_${field.fieldName}`]"
size="small"
>
<el-radio-button
v-for="option in field.options"
:key="option"
:label="option"
>
{{ option }}
</el-radio-button>
</el-radio-group>
</template>
</el-table-column>
</template>
<!-- 功能欄位 (根據配置決定是否顯示) -->
<el-table-column
v-if="table.allowEdit !== false"
label="功能"
width="140"
fixed="right"
>
<template #default="scope">
<el-button size="small" type="primary" plain>修改</el-button>
<el-button
size="small"
type="danger"
plain
@click="deleteItem(table.key, scope.$index)"
>
刪除
</el-button>
</template>
</el-table-column>
</PaginatedTable>
<el-table
v-else
:data="dynamicTableData[table.key] || []"
:border="true"
>
<el-table-column
v-for="column in table.columns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:width="column.width"
>
<template #default="scope">
<span>{{ scope.row[column.prop] }}</span>
</template>
</el-table-column>
<el-table-column
v-if="table.allowEdit !== false"
label="功能"
min-width="80"
width="140"
fixed="right"
>
<template #default="scope">
<el-button size="small" type="primary" plain>修改</el-button>
<el-button
size="small"
type="danger"
plain
@click="deleteItem(table.key, scope.$index)"
>
刪除
</el-button>
</template>
</el-table-column>
</el-table>
</template>
</template>
</el-form>
<template #footer>
<el-button @click="onClose">取消</el-button>
<el-button type="primary" @click="onSave">保存</el-button>
</template>
<!-- 子對話框 -->
<DutyLogItemDialog
v-model:visible="dialogVisibility.dutyLog"
@add="(item) => addItem('dutyLog', item)"
/>
<HandoverItemDialog
v-model:visible="dialogVisibility.handover"
@add="(item) => addItem('handover', item)"
/>
</el-dialog>
</template>
<script setup>
import { ref, computed, watch, reactive } from "vue";
import PaginatedTable from "../Common/PaginatedTable.vue";
import DutyLogItemDialog from "./DutyLogItemDialog.vue";
import HandoverItemDialog from "./HandoverItemDialog.vue";
import {
getTemplateSchema,
initializeTableData,
initializeFieldData,
} from "../../constants/templateSchemas.js";
const props = defineProps({
visible: { type: Boolean, default: false },
taskData: { type: Object, default: null }, // ,
});
const emit = defineEmits(["update:visible", "save", "close"]);
//
const form = reactive({});
//
const dynamicFields = reactive({});
//
const dynamicTableData = reactive({});
//
const dialogVisibility = reactive({
dutyLog: false,
handover: false,
});
// schema
const activeSchema = computed(() => {
if (!props.taskData || !props.taskData.templateType) return null;
const schema = getTemplateSchema(props.taskData.templateType);
if (!schema) return null;
//
return {
...schema,
sections: schema.sections?.filter(s => s.showInFill !== false) || [],
tables: schema.tables?.filter(t => t.showInFill !== false) || [],
};
});
//
const dialogTitle = computed(() => {
return props.taskData?.taskName || "填寫任務";
});
// ( verification )
const verificationFields = computed(() => {
if (!props.taskData || !props.taskData.tables || !props.taskData.tables.verification) {
return [];
}
return props.taskData.tables.verification || [];
});
// ()
const isValueColumn = (prop) => {
const infoColumns = ['index', 'system1', 'system2', 'deviceId', 'item', 'unit'];
return !infoColumns.includes(prop);
};
// ,
watch(
() => props.taskData,
(newTaskData) => {
if (!newTaskData || !newTaskData.templateType) return;
const schema = getTemplateSchema(newTaskData.templateType);
if (!schema) return;
//
Object.keys(dynamicFields).forEach((key) => delete dynamicFields[key]);
Object.assign(dynamicFields, initializeFieldData(schema));
//
Object.keys(dynamicTableData).forEach((key) => delete dynamicTableData[key]);
// ,
if (newTaskData.fillData) {
Object.assign(dynamicFields, newTaskData.fillData.fields || {});
Object.assign(dynamicTableData, newTaskData.fillData.tables || {});
} else {
//
// 使 taskData.tables ( API )
if (newTaskData.tables) {
// 使
Object.assign(dynamicTableData, JSON.parse(JSON.stringify(newTaskData.tables)));
} else {
// 使 schema ()
Object.assign(dynamicTableData, initializeTableData(schema));
}
}
},
{ immediate: true }
);
//
function openDialog(dialogType) {
if (dialogType === "dutyLog") {
dialogVisibility.dutyLog = true;
} else if (dialogType === "handover") {
dialogVisibility.handover = true;
}
}
//
function addItem(tableKey, item) {
if (!dynamicTableData[tableKey]) {
dynamicTableData[tableKey] = [];
}
const arr = dynamicTableData[tableKey];
arr.push({ index: arr.length + 1, ...item });
}
//
function deleteItem(tableKey, index) {
if (dynamicTableData[tableKey]) {
dynamicTableData[tableKey].splice(index, 1);
//
dynamicTableData[tableKey].forEach((item, idx) => {
item.index = idx + 1;
});
}
}
function onClose() {
emit("update:visible", false);
emit("close");
}
function onSave() {
//
const fillData = {
fields: { ...dynamicFields },
tables: { ...dynamicTableData },
fillTime: new Date().toISOString(),
};
emit("save", {
taskData: props.taskData,
fillData,
});
onClose();
}
</script>
<style scoped>
.el-divider--horizontal {
margin-top: 30px;
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,321 @@
<template>
<!-- 待完成內容 -->
<div class="filter-section">
<el-row :gutter="20">
<el-col :xs="24" :md="6">
<el-form-item label="電廠:">
<el-select v-model="filterFactory" placeholder="選擇電廠">
<el-option label="四磺子坪" value="四磺子坪" />
<el-option label="宜蘭大清水" value="宜蘭大清水" />
<el-option label="宜蘭小清水" value="宜蘭小清水" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="6">
<el-form-item label="類別:">
<el-select v-model="filterCategory" placeholder="選擇類別">
<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" :md="12">
<el-button type="primary" @click="handleSearch" :icon="Search"
>查詢</el-button
>
</el-col>
</el-row>
</div>
<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="100" />
<el-table-column prop="template" label="模板" width="100" />
<el-table-column prop="taskName" label="任務名稱" />
<el-table-column prop="dispatchDate" label="派工日期" />
<el-table-column label="功能" width="100" fixed="right">
<template #default>
<el-button size="small" type="info" :icon="View">查看</el-button>
</template>
</el-table-column>
<el-table-column label="填表" width="100" fixed="right">
<template #default="scope">
<el-button
size="small"
type="primary"
:icon="Edit"
@click="handleFillTask(scope.row)"
>
填寫
</el-button>
</template>
</el-table-column>
<el-table-column label="1關審查" width="200" fixed="right">
<template #default>
<el-radio-group v-model="radio" fill="#409eff">
<el-radio-button label="未確認" value="" />
<el-radio-button label="已確認" value="" />
</el-radio-group>
</template>
</el-table-column>
<el-table-column label="2關審查" width="200" fixed="right">
<template #default>
<el-radio-group v-model="radio" fill="#409eff">
<el-radio-button label="未確認" value="" />
<el-radio-button label="已確認" value="" />
</el-radio-group>
</template>
</el-table-column>
</PaginatedTable>
<!-- 填寫任務對話框 -->
<FillTaskDialog
v-model:visible="fillDialogVisible"
:task-data="currentTask"
@save="handleSaveFillData"
/>
<h3 style="margin-top: 20px">簽名板</h3>
<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"
/>
</template>
<script setup>
import { ref, computed, reactive } from "vue";
import {
View,
Edit,
TakeawayBox,
Loading,
RefreshLeft,
Search,
} from "@element-plus/icons-vue";
import PaginatedTable from "../Common/PaginatedTable.vue";
import FillTaskDialog from "./FillTaskDialog.vue";
import dayjs from "dayjs";
//
const currentPage = ref(1);
const pageSize = ref(10);
//
const filterFactory = ref("");
const filterCategory = ref("");
// table ( API )
const allTableData = ref([
{
index: 1,
factory: "四磺子坪",
template: "巡檢表",
templateType: "1", // templateSchemas key
taskName: "四磺子坪1.2MW地熱能ORC發電機",
dispatchDate: "2025-11-01",
// API
tables: {
inspection: [
{
index: 1,
system1: "熱源系統",
system2: "#2生產井",
deviceId: "TE_1004",
item: "#2地熱井溫度",
unit: "°C",
lowerLimit: 80,
upperLimit: 120,
dcsValue: 100,
},
{
index: 2,
system1: "熱源系統",
system2: "#2生產井",
deviceId: "TG_1004",
item: "#2地熱井溫度表",
unit: "°C",
lowerLimit: 80,
upperLimit: 120,
dcsValue: 98,
},
// ...
],
verification: [
{ index: 1, fieldName: "現場", type: "數值輸入" },
{
index: 2,
fieldName: "比對結果",
type: "單一選擇",
options: ["符合", "不符合"],
},
],
},
},
{
index: 2,
factory: "四磺子坪",
template: "巡檢表",
templateType: "1",
taskName: "四磺子坪1.2MW先導地熱能ORC發電機",
dispatchDate: "2025-11-01",
tables: {
inspection: [
{
index: 1,
system1: "熱源系統",
system2: "#3生產井",
deviceId: "TE_1005",
item: "#3地熱井溫度",
unit: "°C",
lowerLimit: 80,
upperLimit: 120,
dcsValue: 98,
},
],
verification: [
{ index: 1, fieldName: "現場", type: "數值輸入" },
{
index: 2,
fieldName: "比對結果",
type: "單一選擇",
options: ["符合", "不符合"],
},
],
},
},
{
index: 3,
factory: "四磺子坪",
template: "點檢表",
templateType: "2",
taskName: "廠房01維修工程",
dispatchDate: "2025-11-02",
tables: {
checkItems: [
// ...
],
},
},
]);
//
const tableData = computed(() => {
let result = allTableData.value;
//
if (filterFactory.value) {
result = result.filter((item) => item.factory === filterFactory.value);
}
//
if (filterCategory.value) {
result = result.filter((item) => item.template === filterCategory.value);
}
return result;
});
//
const handleSearch = () => {
console.log("搜尋條件:", {
factory: filterFactory.value,
category: filterCategory.value,
});
// computed tableData
};
//
const fillDialogVisible = ref(false);
const currentTask = ref(null);
//
const handleFillTask = (task) => {
console.log("task", task);
currentTask.value = task;
fillDialogVisible.value = true;
};
//
const handleSaveFillData = (data) => {
console.log("保存填寫資料:", data);
// 調 API
//
const taskIndex = allTableData.value.findIndex(
(t) => t.index === data.taskData.index
);
if (taskIndex !== -1) {
allTableData.value[taskIndex].fillData = data.fillData;
allTableData.value[taskIndex].status = "已填寫";
}
};
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("請先簽名");
return;
}
const dataUrl = signature.value.save(format);
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>
<style scoped>
.filter-section {
background: #f5f7fa;
padding: 20px;
margin-bottom: 20px;
border-radius: 4px;
}
.filter-section .el-form-item {
margin-bottom: 0;
}
.signature-pad {
border: 1px solid #e0e0e0;
}
</style>

View File

@ -6,19 +6,20 @@
style="max-width: 800px; width: 90%" style="max-width: 800px; width: 90%"
@close="onClose" @close="onClose"
> >
<DutyLogItemDialog v-model:visible="showDutyLogDialog" @add="addDutyLog" /> <!-- 通用對話框 -->
<HandoverItemDialog
v-model:visible="showHandoverDialog"
@add="addHandover"
/>
<VerificationSettingDialog <VerificationSettingDialog
v-model:visible="showVerificationDialog" v-model:visible="dialogVisibility.verification"
@add="addVerification" @add="(item) => addItem('verification', item)"
/> />
<InspectionItemDialog <InspectionItemDialog
v-model:visible="showInspectionDialog" v-model:visible="dialogVisibility.inspection"
@add="addInspection" @add="(item) => addItem('inspection', item)"
/> />
<CheckItemsDialog
v-model:visible="dialogVisibility.checkItems"
@add="(item) => addItem('checkItems', item)"
/>
<el-form :model="form"> <el-form :model="form">
<el-divider content-position="left">樣板資訊</el-divider> <el-divider content-position="left">樣板資訊</el-divider>
<el-row :gutter="20"> <el-row :gutter="20">
@ -35,31 +36,24 @@
<el-col :xs="24" :sm="12"> <el-col :xs="24" :sm="12">
<el-form-item label="模板"> <el-form-item label="模板">
<el-select v-model="form.template" placeholder="請選擇模板類型"> <el-select v-model="form.template" placeholder="請選擇模板類型">
<el-option label="巡檢表" value="巡檢表" /> <el-option label="巡檢表" value="1" />
<el-option label="點檢表" value="點檢表" /> <el-option label="點檢表" value="2" />
<el-option <el-option label="高壓特每日自動檢查表" value="3" />
label="高壓特每日自動檢查表" <el-option label="高壓特每月自動檢查表" value="4" />
value="高壓特每日自動檢查表" <el-option label="一班作業安全許可申請表" value="5" />
/> <el-option label="侷限作業安全許可申請表" value="6" />
<el-option <el-option label="動火作業安全許可申請表" value="7" />
label="高壓特每月自動檢查表"
value="高壓特每月自動檢查表"
/>
<el-option
label="一班作業安全許可申請表"
value="一班作業安全許可申請表"
/>
<el-option
label="侷限作業安全許可申請表"
value="侷限作業安全許可申請表"
/>
<el-option
label="動火作業安全許可申請表"
value="動火作業安全許可申請表"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="12">
<el-form-item label="樣板名稱">
<el-input
v-model="form.templateName"
placeholder="請輸入樣板名稱"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12"> <el-col :xs="24" :sm="12">
<el-form-item label="母版"> <el-form-item label="母版">
<el-select v-model="form.isParent" placeholder="請選擇"> <el-select v-model="form.isParent" placeholder="請選擇">
@ -68,153 +62,127 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :xs="24" :sm="24"> </el-row>
<el-form-item label="樣板名稱">
<el-input <!-- 動態渲染樣板內容欄位 -->
v-model="form.templateName" <template v-if="activeSchema">
placeholder="請輸入樣板名稱" <template v-for="section in activeSchema.sections" :key="section.title">
<el-divider content-position="left">{{ section.title }}</el-divider>
<el-row :gutter="20">
<el-col
v-for="field in section.fields"
:key="field.key"
:xs="24"
:sm="field.span"
>
<el-form-item :label="field.label">
<!-- 下拉選單 -->
<el-select
v-if="field.type === 'select'"
v-model="form[field.key]"
:placeholder="`請選擇${field.label}`"
>
<el-option
v-for="option in field.options"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<!-- 多行文本 -->
<el-input
v-else-if="field.type === 'textarea'"
v-model="form[field.key]"
type="textarea"
:rows="3"
:placeholder="`請輸入${field.label}`"
/>
<!-- 一般文本輸入 -->
<el-input
v-else
v-model="dynamicFields[field.key]"
:placeholder="`請輸入${field.label}`"
/>
</el-form-item>
</el-col>
</el-row>
</template>
<!-- 動態渲染表格 -->
<template v-for="table in activeSchema.tables" :key="table.key">
<el-row :align="'middle'" justify="space-between">
<h3>{{ table.title }}</h3>
<el-button
type="primary"
size="small"
@click="openDialog(table.dialogType)"
>
{{ table.addButtonText }}
</el-button>
</el-row>
<!-- 使用分頁表格或一般表格 -->
<PaginatedTable
v-if="table.usePagination"
:data="dynamicTableData[table.key] || []"
:page-sizes="[5, 10]"
row-key="index"
>
<el-table-column
v-for="column in table.columns"
:key="column.prop"
:prop="column.prop"
:label="column.label"
:width="column.width"
/> />
</el-form-item> <el-table-column label="功能" width="140" fixed="right">
</el-col> <template #default="scope">
</el-row> <el-button size="small" type="primary" plain>修改</el-button>
<el-button
size="small"
type="danger"
plain
@click="deleteItem(table.key, scope.$index)"
>
刪除
</el-button>
</template>
</el-table-column>
</PaginatedTable>
<el-divider content-position="left">樣板內容</el-divider> <el-table
<el-row :gutter="20"> v-else
<el-col :xs="24" :sm="24"> :data="dynamicTableData[table.key] || []"
<el-form-item label="案場名稱"> :border="true"
<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-table-column
<el-button size="small" type="primary" plain>修改</el-button> v-for="column in table.columns"
<el-button size="small" type="danger" plain>刪除</el-button> :key="column.prop"
</template> :prop="column.prop"
</el-table-column> :label="column.label"
</el-table> :width="column.width"
</el-row> />
<el-table-column
<el-row :align="'middle'" justify="space-between"> label="功能"
<h3>交接事項</h3> min-width="80"
<el-button width="140"
type="primary" fixed="right"
size="small" >
@click="showHandoverDialog = true" <template #default="scope">
>新增交接事項</el-button <el-button size="small" type="primary" plain>修改</el-button>
> <el-button
<el-table :data="handoverTableData" :border="true"> size="small"
<el-table-column prop="index" label="項次" width="60" /> type="danger"
<el-table-column prop="content" label="工作內容" /> plain
<el-table-column prop="continue" label="持續" /> @click="deleteItem(table.key, scope.$index)"
<el-table-column prop="result" label="結案" /> >
<el-table-column prop="remark" label="備註" /> 刪除
<el-table-column </el-button>
label="功能" </template>
min-width="80" </el-table-column>
width="140" </el-table>
fixed="right" </template>
> </template>
<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> </el-form>
<template #footer> <template #footer>
<el-button @click="onClose">取消</el-button> <el-button @click="onClose">取消</el-button>
@ -224,55 +192,122 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, watch, reactive } from "vue";
import PaginatedTable from "../Common/PaginatedTable.vue"; import PaginatedTable from "../Common/PaginatedTable.vue";
import DutyLogItemDialog from "./DutyLogItemDialog.vue";
import HandoverItemDialog from "./HandoverItemDialog.vue";
import VerificationSettingDialog from "./VerificationSettingDialog.vue"; import VerificationSettingDialog from "./VerificationSettingDialog.vue";
import InspectionItemDialog from "./InspectionItemDialog.vue"; import InspectionItemDialog from "./InspectionItemDialog.vue";
import CheckItemsDialog from "./CheckItemsDialog.vue";
import {
getTemplateSchema,
initializeTableData,
initializeFieldData,
} from "../../constants/templateSchemas.js";
const props = defineProps({ const props = defineProps({
visible: { type: Boolean, default: false }, visible: { type: Boolean, default: false },
form: { type: Object, required: true }, 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"]); const emit = defineEmits(["update:visible", "confirm", "close"]);
// schema
const activeSchema = computed(() => {
const schema = getTemplateSchema(props.form.template);
if (!schema) return null;
//
return {
...schema,
sections: schema.sections?.filter((s) => s.showInTemplate !== false) || [],
tables: schema.tables?.filter((t) => t.showInTemplate !== false) || [],
};
});
//
const dynamicFields = reactive({});
//
const dynamicTableData = reactive({});
//
const dialogVisibility = reactive({
dutyLog: false,
handover: false,
inspection: false,
verification: false,
checkItems: false,
});
// ,
watch(
() => props.form.template,
(newTemplate) => {
if (!newTemplate) return;
const schema = getTemplateSchema(newTemplate);
if (!schema) return;
//
Object.keys(dynamicFields).forEach((key) => delete dynamicFields[key]);
Object.assign(dynamicFields, initializeFieldData(schema));
//
Object.keys(dynamicTableData).forEach(
(key) => delete dynamicTableData[key]
);
Object.assign(dynamicTableData, initializeTableData(schema));
}
);
//
function openDialog(dialogType) {
if (dialogType === "dutyLog") {
dialogVisibility.dutyLog = true;
} else if (dialogType === "handover") {
dialogVisibility.handover = true;
} else if (dialogType === "inspection") {
dialogVisibility.inspection = true;
} else if (dialogType === "verification") {
dialogVisibility.verification = true;
} else if (dialogType === "checkItems") {
dialogVisibility.checkItems = true;
}
// dialogType
}
//
function addItem(tableKey, item) {
if (!dynamicTableData[tableKey]) {
dynamicTableData[tableKey] = [];
}
const arr = dynamicTableData[tableKey];
arr.push({ index: arr.length + 1, ...item });
}
//
function deleteItem(tableKey, index) {
if (dynamicTableData[tableKey]) {
dynamicTableData[tableKey].splice(index, 1);
//
dynamicTableData[tableKey].forEach((item, idx) => {
item.index = idx + 1;
});
}
}
function onClose() { function onClose() {
emit("update:visible", false); emit("update:visible", false);
emit("close"); emit("close");
} }
function onConfirm() { function onConfirm() {
emit("confirm"); // dynamicFields dynamicTableData form
} const templateData = {
...props.form,
// Dialog fields: dynamicFields,
import { ref } from "vue"; tables: dynamicTableData,
const showDutyLogDialog = ref(false); };
const showHandoverDialog = ref(false); emit("confirm", templateData);
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> </script>

View File

@ -0,0 +1,110 @@
<template>
<el-dialog
:model-value="visible"
title="新增點檢項目"
width="600px"
@close="onClose"
>
<el-form :model="form" label-width="120px">
<el-form-item label="設備">
<el-input v-model="form.index" placeholder="請輸入設備名稱" />
</el-form-item>
<el-form-item label="項目">
<el-input v-model="form.item" placeholder="請輸入項目" />
</el-form-item>
<el-form-item label="內容">
<el-input
v-model="form.content"
type="textarea"
:rows="3"
placeholder="請輸入內容"
/>
</el-form-item>
<el-form-item label="檢查結果 (早)">
<el-input v-model="form.resultMorning" placeholder="請輸入檢查結果" />
</el-form-item>
<el-form-item label="檢查結果 (中)">
<el-input v-model="form.resultNoon" placeholder="請輸入檢查結果" />
</el-form-item>
<el-form-item label="檢查結果 (下)">
<el-input v-model="form.resultAfternoon" placeholder="請輸入檢查結果" />
</el-form-item>
<el-form-item label="檢查結果 (晚)">
<el-input v-model="form.resultEvening" placeholder="請輸入檢查結果" />
</el-form-item>
<el-form-item label="備註">
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
placeholder="請輸入備註"
/>
</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, watch } from "vue";
const props = defineProps({
visible: { type: Boolean, default: false },
});
const emit = defineEmits(["update:visible", "add"]);
const form = ref({
index: "",
item: "",
content: "",
resultMorning: "",
resultNoon: "",
resultAfternoon: "",
resultEvening: "",
remark: "",
});
//
watch(
() => props.visible,
(newVal) => {
if (!newVal) {
resetForm();
}
}
);
function resetForm() {
form.value = {
index: "",
item: "",
content: "",
resultMorning: "",
resultNoon: "",
resultAfternoon: "",
resultEvening: "",
remark: "",
};
}
function onClose() {
emit("update:visible", false);
}
function onConfirm() {
//
if (!form.value.index || !form.value.item) {
//
return;
}
emit("add", { ...form.value });
emit("update:visible", false);
}
</script>
<style scoped></style>

View File

@ -1,5 +1,42 @@
<template> <template>
<div> <div>
<div class="filter-section">
<el-row :gutter="20" >
<el-col :xs="24" :md="6">
<el-form-item label="電廠:">
<el-select
v-model="filterFactory"
placeholder="選擇電廠"
clearable
>
<el-option label="四磺子坪" value="四磺子坪" />
<el-option label="宜蘭大清水" value="宜蘭大清水" />
<el-option label="宜蘭小清水" value="宜蘭小清水" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="6">
<el-form-item label="模板:" >
<el-select
v-model="filterCategory"
placeholder="選擇模板"
clearable
>
<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" :md="6">
<el-button type="primary" :icon="Search" @click="handleSearch">
查詢
</el-button>
</el-col>
</el-row>
</div>
<el-row justify="end"> <el-row justify="end">
<el-button type="primary" :icon="Plus" @click="showDialog = true" <el-button type="primary" :icon="Plus" @click="showDialog = true"
>新增任務</el-button >新增任務</el-button
@ -18,7 +55,7 @@
<PaginatedTable <PaginatedTable
v-model:current-page="currentPage" v-model:current-page="currentPage"
v-model:page-size="pageSize" v-model:page-size="pageSize"
:data="tableData" :data="filteredData"
:page-sizes="[5, 10]" :page-sizes="[5, 10]"
row-key="index" row-key="index"
> >
@ -26,16 +63,10 @@
<el-table-column <el-table-column
prop="factory" prop="factory"
label="電廠" label="電廠"
:filters="factoryFilters"
:filter-method="filterFactory"
filter-placement="bottom-end"
/> />
<el-table-column <el-table-column
prop="category" prop="category"
label="類別" label="模板"
:filters="categoryFilters"
:filter-method="filterCategory"
filter-placement="bottom-end"
/> />
<el-table-column prop="taskName" label="任務名稱" /> <el-table-column prop="taskName" label="任務名稱" />
<el-table-column prop="frequency" label="巡檢頻率" /> <el-table-column prop="frequency" label="巡檢頻率" />
@ -57,11 +88,15 @@ import { ref, computed } from "vue";
import { templateList } from "../../constants/templateList.js"; import { templateList } from "../../constants/templateList.js";
import PaginatedTable from "../Common/PaginatedTable.vue"; import PaginatedTable from "../Common/PaginatedTable.vue";
import AddTaskDialog from "./AddTaskDialog.vue"; import AddTaskDialog from "./AddTaskDialog.vue";
import { Plus, Delete, Edit } from "@element-plus/icons-vue"; import { Plus, Delete, Edit, Search } from "@element-plus/icons-vue";
const currentPage = ref(1); const currentPage = ref(1);
const pageSize = ref(10); const pageSize = ref(10);
const tableData = [ //
const filterFactory = ref("");
const filterCategory = ref("");
const tableData = ref([
{ {
index: 1, index: 1,
factory: "四磺子坪", factory: "四磺子坪",
@ -172,24 +207,23 @@ const tableData = [
creator: "管理員03", creator: "管理員03",
createdAt: "2025-11-13 17:14:05", createdAt: "2025-11-13 17:14:05",
}, },
]; ]);
//
const factoryFilters = computed(() => const filteredData = computed(() => {
Array.from(new Set(tableData.map((item) => item.factory))).map((factory) => ({ let result = tableData.value;
text: factory,
value: factory, //
})) if (filterFactory.value) {
); result = result.filter((item) => item.factory === filterFactory.value);
const categoryFilters = computed(() => }
Array.from(new Set(tableData.map((item) => item.category))).map(
(category) => ({ //
text: category, if (filterCategory.value) {
value: category, result = result.filter((item) => item.category === filterCategory.value);
}) }
)
); return result;
const filterFactory = (value, row) => row.factory === value; });
const filterCategory = (value, row) => row.category === value;
// dialog // dialog
const showDialog = ref(false); const showDialog = ref(false);
@ -215,15 +249,23 @@ function resetTaskForm() {
} }
function handleAddTask() { function handleAddTask() {
// templateName category
let category = "";
if (taskForm.value.templateName) {
if (taskForm.value.templateName.includes("點檢表")) {
category = "點檢表";
} else if (taskForm.value.templateName.includes("巡檢表")) {
category = "巡檢表";
} else {
category = taskForm.value.templateName;
}
}
// tableData // tableData
tableData.push({ tableData.value.push({
index: tableData.length + 1, index: tableData.value.length + 1,
factory: taskForm.value.factory, factory: taskForm.value.factory,
category: taskForm.value.templateName category: category,
? taskForm.value.templateName.split("每日").length > 1
? "點檢表"
: "巡檢表"
: "",
taskName: taskForm.value.taskName, taskName: taskForm.value.taskName,
frequency: taskForm.value.frequency, frequency: taskForm.value.frequency,
count: taskForm.value.count, count: taskForm.value.count,
@ -234,6 +276,24 @@ function handleAddTask() {
resetTaskForm(); resetTaskForm();
} }
// AddTaskDialog //
const handleSearch = () => {
console.log("搜尋條件:", {
factory: filterFactory.value,
template: filterCategory.value,
});
// computed filteredData
};
</script> </script>
<style scoped></style> <style scoped>
.filter-section {
background: #f5f7fa;
padding: 20px;
margin-bottom: 20px;
border-radius: 4px;
}
.filter-section .el-form-item {
margin-bottom: 0;
}
</style>

View File

@ -1,19 +1,53 @@
<template> <template>
<div> <div>
<!-- 樣板管理內容 --> <!-- 樣板管理內容 -->
<el-row justify="end"> <div class="filter-section">
<el-row :gutter="20" >
<el-col :xs="24" :md="6">
<el-form-item label="電廠:">
<el-select
v-model="filterFactory"
placeholder="選擇電廠"
clearable
>
<el-option label="四磺子坪" value="四磺子坪" />
<el-option label="宜蘭大清水" value="宜蘭大清水" />
<el-option label="宜蘭小清水" value="宜蘭小清水" />
</el-select>
</el-form-item>
</el-col>
<el-col :xs="24" :md="6">
<el-form-item label="模板:" >
<el-select
v-model="filterTemplate"
placeholder="選擇模板"
clearable
>
<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" :md="6">
<el-button type="primary" :icon="Search" @click="handleSearch">
查詢
</el-button>
</el-col>
</el-row>
</div>
<el-row justify="end" >
<el-button type="primary" :icon="Plus" @click="showDialog = true" <el-button type="primary" :icon="Plus" @click="showDialog = true"
>新增樣板</el-button >新增樣板</el-button
> >
</el-row> </el-row>
<!-- 新增樣板Dialog -->
<AddTemplateDialog <AddTemplateDialog
v-model:visible="showDialog" v-model:visible="showDialog"
:form="addTemplateForm" :form="addTemplateForm"
:duty-log-table-data="dutyLogTableData"
:handover-table-data="handoverTableData"
:inspection-table-data="inspectionTableData"
:verification-table-data="verificationTableData"
@confirm="handleAddTemplate" @confirm="handleAddTemplate"
@close="resetAddTemplateForm" @close="resetAddTemplateForm"
/> />
@ -31,16 +65,10 @@
prop="factory" prop="factory"
label="電廠" label="電廠"
width="200" width="200"
:filters="factoryFilters"
:filter-method="filterFactory"
filter-placement="bottom-end"
/> />
<el-table-column <el-table-column
prop="template" prop="template"
label="模板" label="模板"
:filters="templateFilters"
:filter-method="filterTemplate"
filter-placement="bottom-end"
/> />
<el-table-column prop="templateName" label="樣板名稱" /> <el-table-column prop="templateName" label="樣板名稱" />
<el-table-column prop="isParent" label="母版" width="100"> <el-table-column prop="isParent" label="母版" width="100">
@ -75,11 +103,16 @@ import {
Edit, Edit,
CopyDocument, CopyDocument,
View, View,
Search,
} from "@element-plus/icons-vue"; } from "@element-plus/icons-vue";
import PaginatedTable from "../Common/PaginatedTable.vue"; import PaginatedTable from "../Common/PaginatedTable.vue";
import AddTemplateDialog from "./AddTemplateDialog.vue"; import AddTemplateDialog from "./AddTemplateDialog.vue";
import { templateList } from "../../constants/templateList.js"; import { templateList } from "../../constants/templateList.js";
//
const filterFactory = ref("");
const filterTemplate = ref("");
// //
const showDialog = ref(false); const showDialog = ref(false);
@ -101,135 +134,56 @@ function resetAddTemplateForm() {
showDialog.value = false; showDialog.value = false;
} }
function handleAddTemplate() { function handleAddTemplate(templateData) {
// tableData API // tableData API
console.log("新增樣板資料:", templateData);
// TODO: API
resetAddTemplateForm(); 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 currentPage = ref(1);
const pageSize = ref(10); const pageSize = ref(10);
// //
const tableData = templateList; const allTableData = templateList;
// //
const factoryFilters = computed(() => const tableData = computed(() => {
Array.from(new Set(tableData.map((item) => item.factory))).map((factory) => ({ let result = allTableData;
text: factory,
value: factory,
}))
);
const templateFilters = computed(() => //
Array.from(new Set(tableData.map((item) => item.template))).map( if (filterFactory.value) {
(template) => ({ text: template, value: template }) result = result.filter((item) => item.factory === filterFactory.value);
) }
);
// //
const filterFactory = (value, row) => row.factory === value; if (filterTemplate.value) {
const filterTemplate = (value, row) => row.template === value; result = result.filter((item) => item.template === filterTemplate.value);
}
return result;
});
//
const handleSearch = () => {
console.log("搜尋條件:", {
factory: filterFactory.value,
template: filterTemplate.value,
});
// computed tableData
};
</script> </script>
<style scoped></style> <style scoped>
.filter-section {
background: #f5f7fa;
padding: 20px;
margin-bottom: 20px;
border-radius: 4px;
}
.filter-section .el-form-item {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<el-dialog
:model-value="visible"
:title="isEdit ? '修改選項' : '新增選項'"
width="400px"
@close="close"
>
<el-form :model="form" label-width="70px">
<el-form-item label="名稱">
<el-input v-model="form.name" placeholder="請輸入選項名稱" />
</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>
import { reactive, watch } from "vue";
const props = defineProps({
visible: { type: Boolean, default: false },
optionData: { type: Object, default: null }, //
});
const emit = defineEmits(["update:visible", "add", "update"]);
const form = reactive({ name: "" });
const isEdit = reactive({ value: false });
// optionData
watch(
() => props.optionData,
(newData) => {
if (newData) {
isEdit.value = true;
form.name = newData.name;
} else {
isEdit.value = false;
form.name = "";
}
},
{ immediate: true }
);
function reset() {
form.name = "";
isEdit.value = false;
}
function close() {
emit("update:visible", false);
reset();
}
function confirm() {
if (!form.name.trim()) {
return;
}
if (isEdit.value) {
emit("update", { ...form });
} else {
emit("add", { ...form });
}
close();
}
</script>
<style scoped></style>

View File

@ -2,12 +2,12 @@
<el-dialog <el-dialog
:model-value="visible" :model-value="visible"
title="新增查證設定" title="新增查證設定"
width="480px" width="600px"
@close="close" @close="close"
> >
<el-form :model="form" label-width="90px"> <el-form :model="form" label-width="90px">
<el-form-item label="欄位名稱"> <el-form-item label="欄位名稱">
<el-input v-model="form.fieldName" /> <el-input v-model="form.fieldName" placeholder="請填寫欄位名" />
</el-form-item> </el-form-item>
<el-form-item label="類型"> <el-form-item label="類型">
<el-select v-model="form.type" placeholder="選擇類型"> <el-select v-model="form.type" placeholder="選擇類型">
@ -16,29 +16,153 @@
<el-option label="單一選擇" value="單一選擇" /> <el-option label="單一選擇" value="單一選擇" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<!-- 當選擇單一選擇時顯示選項表格 -->
<template v-if="form.type === '單一選擇'">
<el-row :align="'middle'" justify="space-between">
<el-divider content-position="left">選項設定</el-divider>
<h3>單一選擇</h3>
<el-button type="primary" size="small" @click="openOptionDialog">
新增選項
</el-button>
</el-row>
<el-table :data="options" >
<el-table-column
prop="index"
label="項目"
width="80"
/>
<el-table-column prop="name" label="名稱" />
<el-table-column label="編輯" width="160" >
<template #default="scope">
<el-button
size="small"
type="primary"
plain
@click="editOption(scope.row, scope.$index)"
>
修改
</el-button>
<el-button
size="small"
type="danger"
plain
@click="deleteOption(scope.$index)"
>
刪除
</el-button>
</template>
</el-table-column>
</el-table>
</template>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="close">取消</el-button> <el-button @click="close">取消</el-button>
<el-button type="primary" @click="confirm">加入</el-button> <el-button type="primary" @click="confirm">加入</el-button>
</template> </template>
<!-- 選項管理對話框 -->
<VerificationOptionDialog
v-model:visible="optionDialogVisible"
:option-data="editingOption"
@add="addOption"
@update="updateOption"
/>
</el-dialog> </el-dialog>
</template> </template>
<script setup> <script setup>
import { reactive, ref } from "vue";
import VerificationOptionDialog from "./VerificationOptionDialog.vue";
const props = defineProps({ visible: { type: Boolean, default: false } }); const props = defineProps({ visible: { type: Boolean, default: false } });
const emit = defineEmits(["update:visible", "add"]); const emit = defineEmits(["update:visible", "add"]);
import { reactive } from "vue";
const form = reactive({ fieldName: "", type: "" }); const form = reactive({ fieldName: "", type: "" });
const options = ref([
{ index: 1, name: "符合" },
{ index: 2, name: "不符合" },
]); // """"
const optionDialogVisible = ref(false);
const editingOption = ref(null); //
const editingIndex = ref(-1); //
function reset() { function reset() {
form.fieldName = ""; form.fieldName = "";
form.type = ""; form.type = "";
options.value = [
{ index: 1, name: "符合" },
{ index: 2, name: "不符合" },
];
editingOption.value = null;
editingIndex.value = -1;
} }
function close() { function close() {
emit("update:visible", false); emit("update:visible", false);
reset(); reset();
} }
function confirm() { function confirm() {
emit("add", { ...form }); if (!form.fieldName.trim()) {
return;
}
const data = {
fieldName: form.fieldName,
type: form.type,
};
// ,
if (form.type === "單一選擇") {
data.options = options.value.map((opt) => opt.name);
}
emit("add", data);
close(); close();
} }
//
function openOptionDialog() {
editingOption.value = null;
editingIndex.value = -1;
optionDialogVisible.value = true;
}
//
function addOption(option) {
options.value.push({
index: options.value.length + 1,
name: option.name,
});
}
//
function editOption(option, index) {
editingOption.value = { ...option };
editingIndex.value = index;
optionDialogVisible.value = true;
}
//
function updateOption(option) {
if (editingIndex.value !== -1) {
options.value[editingIndex.value].name = option.name;
}
}
//
function deleteOption(index) {
options.value.splice(index, 1);
//
options.value.forEach((opt, idx) => {
opt.index = idx + 1;
});
}
</script> </script>
<style scoped></style>
<style scoped>
.el-divider--horizontal {
margin: 20px 0 15px 0;
}
</style>

View File

@ -0,0 +1,172 @@
<template>
<el-dialog
v-model="dialogVisible"
title="填寫運維表單"
width="800px"
:before-close="handleClose"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="120px"
label-position="right"
>
<el-form-item label="電廠" prop="factory">
<el-input v-model="form.factory" disabled />
</el-form-item>
<el-form-item label="項目" prop="item">
<el-input v-model="form.item" disabled />
</el-form-item>
<el-form-item label="類型" prop="type">
<el-input v-model="form.type" disabled />
</el-form-item>
<el-form-item label="處理人員" prop="operator">
<el-input v-model="form.operator" placeholder="請輸入處理人員姓名" />
</el-form-item>
<el-form-item label="作業日期" prop="workDate">
<el-date-picker
v-model="form.workDate"
type="date"
placeholder="選擇作業日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="作業內容" prop="content">
<el-input
v-model="form.content"
type="textarea"
:rows="4"
placeholder="請描述作業內容"
/>
</el-form-item>
<el-form-item label="上傳照片" prop="photos">
<el-upload
v-model:file-list="form.photoList"
action="#"
list-type="picture-card"
:auto-upload="false"
:limit="5"
accept="image/*"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="備註" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="3"
placeholder="其他備註事項"
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit">確認送出</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from "vue";
import { Plus } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
rowData: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(["update:visible", "submit"]);
const dialogVisible = ref(props.visible);
const formRef = ref();
const form = ref({
factory: "",
item: "",
type: "",
operator: "",
workDate: "",
content: "",
photoList: [],
remark: "",
});
const rules = {
operator: [{ required: true, message: "請輸入處理人員", trigger: "blur" }],
workDate: [{ required: true, message: "請選擇作業日期", trigger: "change" }],
content: [{ required: true, message: "請描述作業內容", trigger: "blur" }],
};
watch(
() => props.visible,
(val) => {
dialogVisible.value = val;
if (val && props.rowData) {
//
form.value.factory = props.rowData.factory || "";
form.value.item = props.rowData.item || "";
form.value.type = props.rowData.type || "";
form.value.operator = props.rowData.operator || "";
form.value.workDate = "";
form.value.content = "";
form.value.photoList = [];
form.value.remark = "";
}
}
);
watch(dialogVisible, (val) => {
emit("update:visible", val);
});
const handleClose = () => {
dialogVisible.value = false;
formRef.value?.resetFields();
};
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate((valid) => {
if (valid) {
//
const submitData = {
...props.rowData,
...form.value,
photos: form.value.photoList.map((file) => file.url || file.raw),
};
emit("submit", submitData);
ElMessage.success("表單提交成功");
handleClose();
} else {
ElMessage.error("請填寫必填欄位");
return false;
}
});
};
</script>
<style scoped>
:deep(.el-upload--picture-card) {
width: 100px;
height: 100px;
}
:deep(.el-upload-list--picture-card .el-upload-list__item) {
width: 100px;
height: 100px;
}
</style>

View File

@ -0,0 +1,242 @@
<template>
<div>
<div class="header-row">
<el-radio-group v-model="tabType" size="large">
<el-radio-button label="全部" value="all" />
<el-radio-button label="維修" value="repair" />
<el-radio-button label="保養" value="maintenance" />
</el-radio-group>
<div>
<el-date-picker
v-model="dateValue"
type="daterange"
size="large"
:shortcuts="shortcuts"
range-separator="至"
start-placeholder="開始日期"
end-placeholder="結束日期"
format="YYYY-MM-DD"
date-format="YYYY/MM/DD"
/>
</div>
<div class="button-row">
<el-button type="primary" size="large" :icon="Search">查詢</el-button>
<el-button type="success" size="large" :icon="Printer">匯出</el-button>
</div>
</div>
<PaginatedTable
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:data="filteredData"
:page-sizes="[5, 10, 20]"
row-key="index"
>
<el-table-column prop="factory" label="電廠" width="100" />
<el-table-column prop="item" label="項目" width="100" />
<el-table-column prop="type" label="類型" />
<el-table-column prop="status" label="狀態" width="140">
<template #default="scope">
<el-tag :type="getTagType(scope.row.status)" disable-transitions>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="operator" label="處理人員" width="100" />
<el-table-column prop="plan" label="本次作業預計">
<template #default="scope">
<el-text type="danger" v-if="scope.row.status === '未完成-過期'">{{
scope.row.plan
}}</el-text>
<span v-else>{{ scope.row.plan }}</span>
</template>
</el-table-column>
<el-table-column prop="photos" label="照片">
<template #default="scope">
<div v-if="!scope.row.photos || scope.row.photos.length === 0">
無照片
</div>
<div v-else>
<el-link
v-for="(img, idx) in scope.row.photos"
:key="img || `${scope.row.index}-${idx}`"
:href="img"
target="_blank"
rel="noopener noreferrer"
style="display: inline-block"
>
<el-image
:src="img"
style="width: 40px; height: 40px; margin-right: 4px"
fit="cover"
/>
</el-link>
</div>
</template>
</el-table-column>
<el-table-column prop="finishDate" label="完成時間" />
<el-table-column prop="formNo" label="表單號" width="160" fixed="right">
<template #default="scope">
<template v-if="scope.row.formNo">
<el-link
:href="scope.row.formNo"
type="primary"
target="_blank"
underline="always"
>{{ scope.row.formNo }}</el-link
>
</template>
<template v-else>
<el-button
type="primary"
size="small"
:icon="Edit"
@click="openFormDialog(scope.row)"
>
填寫表單
</el-button>
</template>
</template>
</el-table-column>
</PaginatedTable>
<OperationFormDialog
v-model:visible="dialogVisible"
:row-data="selectedRow"
@submit="handleFormSubmit"
/>
</div>
</template>
<script setup>
import { ref, computed } from "vue";
import { Search, Printer, Edit } from "@element-plus/icons-vue";
import PaginatedTable from "../Common/PaginatedTable.vue";
import OperationFormDialog from "./OperationFormDialog.vue";
const tabType = ref("all");
const currentPage = ref(1);
const pageSize = ref(10);
const dateValue = ref("");
const dialogVisible = ref(false);
const selectedRow = ref({});
const shortcuts = [
{
text: "近7天",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7);
return [start, end];
},
},
{
text: "近30天",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30);
return [start, end];
},
},
{
text: "近3個月",
value: () => {
const end = new Date();
const start = new Date();
start.setTime(start.getTime() - 3600 * 1000 * 24 * 90);
return [start, end];
},
},
];
const tableData = ref([
{
index: 1,
factory: "四磺子坪",
formNo: "op20210630001",
item: "維修",
type: "逆變器異常",
status: "完成",
checked: true,
operator: "王小明",
plan: "2025/03/01 ~ 2025/03/15",
photos: [
"https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=300",
"https://images.unsplash.com/photo-1464983953574-0892a716854b?w=300",
],
finishDate: "2025-03-5",
},
{
index: 2,
factory: "四磺子坪",
formNo: null,
item: "保養",
type: "巡檢",
status: "未完成-過期",
checked: false,
operator: "",
plan: "2025/05/01 ~ 2025/05/15",
photos: [],
finishDate: "",
},
{
index: 3,
factory: "四磺子坪",
formNo: null,
item: "保養",
type: "清洗",
status: "未完成",
checked: false,
operator: "",
plan: "2025/06/01 ~ 2025/06/15",
photos: [],
finishDate: "",
},
]);
const filteredData = computed(() => {
if (tabType.value === "all") return tableData.value;
if (tabType.value === "repair")
return tableData.value.filter((row) => row.item === "維修");
if (tabType.value === "maintenance")
return tableData.value.filter((row) => row.item === "保養");
return tableData.value;
});
function getTagType(status) {
if (status === "完成") return "success";
if (status === "未完成") return "warning";
if (status === "未完成-過期") return "danger";
return "";
}
function openFormDialog(row) {
selectedRow.value = row;
dialogVisible.value = true;
}
function handleFormSubmit(data) {
console.log("表單提交資料:", data);
// TODO: API
//
const index = tableData.value.findIndex((item) => item.index === data.index);
if (index !== -1) {
tableData.value[index] = {
...tableData.value[index],
operator: data.operator,
finishDate: data.workDate,
status: "完成",
formNo: `op${Date.now()}`, //
};
}
}
</script>
<style scoped>
.header-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 20px;
margin: 20px 0;
}
</style>

View File

@ -61,7 +61,7 @@
</template> </template>
<div class="button-row"> <div class="button-row">
<el-button type="primary" size="large" :icon="Search">查詢</el-button> <el-button type="primary" size="large" :icon="Search">查詢</el-button>
<el-button type="success" size="large" :icon="Printer">查詢</el-button> <el-button type="success" size="large" :icon="Printer">匯出</el-button>
</div> </div>
</div> </div>
</el-card> </el-card>

View File

@ -0,0 +1,567 @@
/**
* 模板配置文件
* 定義每個模板類型的欄位結構和表格配置
*/
export const templateSchemas = {
1: {
sections: [
{
title: "表單內容",
showInTemplate: false, // 新增樣板時不顯示
showInFill: true, // 填寫任務時顯示
fields: [
{ key: "siteName", label: "案場名稱", span: 24 },
{ key: "dutyPerson", label: "值班人員", span: 12 },
{ key: "shift", label: "班別", span: 12 },
{ key: "weather", label: "天氣", span: 12 },
{ key: "temperature", label: "環境溫度/濕度", span: 12 },
{ key: "remark", label: "備註", type: "textarea", span: 24 },
],
},
],
tables: [
{
key: "dutyLog",
title: "值班日誌",
addButtonText: "新增日誌項目",
dialogType: "dutyLog",
showInTemplate: false, // 新增樣板時不顯示
showInFill: true, // 填寫任務時顯示
columns: [
{ prop: "index", label: "項次", width: "60" },
{ prop: "change", label: "變更內容" },
{ prop: "continue", label: "持續" },
{ prop: "result", label: "結案" },
{ prop: "remark", label: "備註" },
],
},
{
key: "handover",
title: "交接事項",
addButtonText: "新增交接事項",
dialogType: "handover",
showInTemplate: false, // 新增樣板時不顯示
showInFill: true, // 填寫任務時顯示
columns: [
{ prop: "index", label: "項次", width: "60" },
{ prop: "content", label: "工作內容" },
{ prop: "continue", label: "持續" },
{ prop: "result", label: "結案" },
{ prop: "remark", label: "備註" },
],
},
{
key: "inspection",
title: "巡檢表",
addButtonText: "新增巡檢項目",
dialogType: "inspection",
usePagination: true,
showInTemplate: true, // 新增樣板時顯示
showInFill: true, // 填寫任務時顯示
allowAdd: false, // 填寫時不顯示新增按鈕
allowEdit: false, // 填寫時不顯示編輯按鈕
useVerification: true, // 使用查證欄位
columns: [
{ prop: "index", label: "項次", width: "60" },
{ prop: "system1", label: "系統1" },
{ prop: "system2", label: "系統2" },
{ prop: "deviceId", label: "設備編號" },
{ prop: "item", label: "項目" },
{ prop: "unit", label: "單位" },
{ prop: "lowerLimit", label: "下限" },
{ prop: "upperLimit", label: "上限" },
{ prop: "dcsValue", label: "DCS" },
],
defaultData: [
{
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",
},
{
index: 11,
system1: "熱源系統",
system2: "#4生產井",
deviceId: "TE_1006",
item: "#4地熱井溫度",
unit: "°C",
},
{
index: 12,
system1: "熱源系統",
system2: "#4生產井",
deviceId: "TG_1006",
item: "#4地熱井溫度表",
unit: "°C",
},
{
index: 13,
system1: "熱源系統",
system2: "#4生產井",
deviceId: "PT_1006",
item: "#4地熱井壓力",
unit: "bar",
},
{
index: 14,
system1: "熱源系統",
system2: "#4生產井",
deviceId: "PG_1006",
item: "#4地熱井壓力表",
unit: "kg/cm2",
},
{
index: 15,
system1: "熱源系統",
system2: "#4生產井",
deviceId: "",
item: "H2S洩漏偵測",
unit: "ppm",
},
],
},
{
key: "verification",
title: "查證",
addButtonText: "新增查證項目",
dialogType: "verification",
usePagination: true,
showInTemplate: true, // 新增樣板時顯示
showInFill: false, // 填寫任務時顯示
columns: [
{ prop: "index", label: "項次", width: "60" },
{ prop: "fieldName", label: "欄位名稱" },
{ prop: "type", label: "類型" },
],
defaultData: [
{
index: 1,
fieldName: "現場",
label: "數值輸入",
},
{
index: 2,
fieldName: "比對結果",
label: "單一選擇",
},
],
},
],
},
2: {
sections: [
{
title: "樣板內容",
showInTemplate: false, // 新增樣板時不顯示
showInFill: true, // 填寫任務時顯示
fields: [
{ key: "siteName", label: "案場名稱", span: 12 },
{ key: "inspectionDate", label: "點檢日期", span: 12 },
],
},
],
tables: [
{
key: "checkItems",
title: "點檢項目",
addButtonText: "新增點檢項目",
dialogType: "checkItems",
usePagination: true,
showInTemplate: true, // 新增樣板時顯示
showInFill: true, // 填寫任務時顯示
columns: [
{ prop: "index", label: "設備" },
{ prop: "item", label: "項目" },
{ prop: "content", label: "內容" },
{ prop: "resultMorning", label: "檢查結果 (早)" },
{ prop: "resultNoon", label: "檢查結果 (中)" },
{ prop: "resultAfternoon", label: "檢查結果 (下)" },
{ prop: "resultEvening", label: "檢查結果 (晚)" },
{ prop: "remark", label: "備註" },
],
},
],
},
3: {
sections: [
{
title: "樣板內容",
showInTemplate: false,
showInFill: true,
fields: [
{ key: "siteName", label: "案場名稱", span: 24 },
{ key: "inspector", label: "檢查人員", span: 12 },
{ key: "checkDate", label: "檢查日期", span: 12 },
{ key: "weather", label: "天氣狀況", span: 12 },
{ key: "temperature", label: "環境溫度", span: 12 },
],
},
],
tables: [
{
key: "dailyCheck",
title: "每日檢查項目",
addButtonText: "新增檢查項目",
dialogType: "dailyCheck",
usePagination: true,
showInTemplate: true,
showInFill: true,
columns: [
{ prop: "index", label: "項次", width: "60" },
{ prop: "equipment", label: "設備名稱" },
{ prop: "checkItem", label: "檢查項目" },
{ prop: "standard", label: "判定基準" },
{ prop: "result", label: "檢查結果" },
{ prop: "remark", label: "備註" },
],
},
{
key: "abnormal",
title: "異常記錄",
addButtonText: "新增異常記錄",
dialogType: "abnormal",
showInTemplate: false,
showInFill: true,
columns: [
{ prop: "index", label: "項次", width: "60" },
{ prop: "time", label: "發現時間" },
{ prop: "equipment", label: "設備名稱" },
{ prop: "description", label: "異常描述" },
{ prop: "action", label: "處理措施" },
],
},
],
},
4: {
sections: [
{
title: "樣板內容",
showInTemplate: false,
showInFill: true,
fields: [
{ key: "siteName", label: "案場名稱", span: 24 },
{ key: "inspector", label: "檢查人員", span: 12 },
{ key: "checkMonth", label: "檢查月份", span: 12 },
{ key: "supervisor", label: "督導人員", span: 12 },
{ key: "approver", label: "核准人員", span: 12 },
],
},
],
tables: [
{
key: "monthlyCheck",
title: "每月檢查項目",
addButtonText: "新增檢查項目",
dialogType: "monthlyCheck",
usePagination: true,
showInTemplate: true,
showInFill: true,
columns: [
{ prop: "index", label: "項次", width: "60" },
{ prop: "category", label: "檢查類別" },
{ prop: "equipment", label: "設備名稱" },
{ prop: "checkItem", label: "檢查項目" },
{ prop: "standard", label: "判定基準" },
{ prop: "result", label: "檢查結果" },
],
},
],
},
5: {
sections: [
{
title: "申請資訊",
showInTemplate: false,
showInFill: true,
fields: [
{ key: "applicant", label: "申請人", span: 12 },
{ key: "applyDate", label: "申請日期", span: 12 },
{ key: "workLocation", label: "作業地點", span: 24 },
{ key: "workContent", label: "作業內容", span: 24, type: "textarea" },
{ key: "startTime", label: "開始時間", span: 12 },
{ key: "endTime", label: "結束時間", span: 12 },
],
},
],
tables: [
{
key: "safetyMeasures",
title: "安全措施",
addButtonText: "新增安全措施",
dialogType: "safetyMeasures",
showInTemplate: true,
showInFill: true,
columns: [
{ prop: "index", label: "項次", width: "60" },
{ prop: "measure", label: "措施項目" },
{ prop: "responsible", label: "負責人" },
{ prop: "checkResult", label: "確認結果" },
],
},
{
key: "personnel",
title: "作業人員",
addButtonText: "新增人員",
dialogType: "personnel",
showInTemplate: true,
showInFill: true,
columns: [
{ prop: "index", label: "項次", width: "60" },
{ prop: "name", label: "姓名" },
{ prop: "role", label: "職務" },
{ prop: "certNumber", label: "證照編號" },
],
},
],
},
6: {
sections: [
{
title: "申請資訊",
showInTemplate: false,
showInFill: true,
fields: [
{ key: "applicant", label: "申請人", span: 12 },
{ key: "applyDate", label: "申請日期", span: 12 },
{ key: "confinedSpace", label: "俺限空間名稱", span: 24 },
{ key: "workContent", label: "作業內容", span: 24, type: "textarea" },
{ key: "startTime", label: "開始時間", span: 12 },
{ key: "endTime", label: "結束時間", span: 12 },
],
},
{
title: "環境檢測",
showInTemplate: false,
showInFill: true,
fields: [
{ key: "oxygenLevel", label: "氧氣濃度 (%)", span: 12 },
{ key: "toxicGas", label: "有毒氣體 (ppm)", span: 12 },
{ key: "combustibleGas", label: "可燃性氣體 (%LEL)", span: 12 },
{ key: "testTime", label: "檢測時間", span: 12 },
],
},
],
tables: [
{
key: "safetyEquipment",
title: "安全設備",
addButtonText: "新增設備",
dialogType: "safetyEquipment",
showInTemplate: true,
showInFill: true,
columns: [
{ prop: "index", label: "項次", width: "60" },
{ prop: "equipment", label: "設備名稱" },
{ prop: "quantity", label: "數量" },
{ prop: "checkResult", label: "檢查結果" },
],
},
{
key: "personnel",
title: "作業人員與監視人",
addButtonText: "新增人員",
dialogType: "personnel",
showInTemplate: true,
showInFill: true,
columns: [
{ prop: "index", label: "項次", width: "60" },
{ prop: "name", label: "姓名" },
{ prop: "role", label: "角色" },
{ prop: "training", label: "教育訓練" },
],
},
],
},
7: {
sections: [
{
title: "申請資訊",
showInTemplate: false,
showInFill: true,
fields: [
{ key: "applicant", label: "申請人", span: 12 },
{ key: "applyDate", label: "申請日期", span: 12 },
{ key: "fireLocation", label: "動火地點", span: 24 },
{ key: "fireType", label: "動火類型", span: 12 },
{ key: "workContent", label: "作業內容", span: 24, type: "textarea" },
{ key: "startTime", label: "開始時間", span: 12 },
{ key: "endTime", label: "結束時間", span: 12 },
],
},
],
tables: [
{
key: "fireSafety",
title: "消防安全措施",
addButtonText: "新增措施",
dialogType: "fireSafety",
showInTemplate: true,
showInFill: true,
columns: [
{ prop: "index", label: "項次", width: "60" },
{ prop: "measure", label: "措施項目" },
{ prop: "checkPoint", label: "檢查重點" },
{ prop: "checkResult", label: "確認結果" },
],
},
{
key: "fireEquipment",
title: "消防器材",
addButtonText: "新增器材",
dialogType: "fireEquipment",
showInTemplate: true,
showInFill: true,
columns: [
{ prop: "index", label: "項次", width: "60" },
{ prop: "equipment", label: "器材名稱" },
{ prop: "quantity", label: "數量" },
{ prop: "location", label: "放置位置" },
],
},
{
key: "firePersonnel",
title: "動火作業人員",
addButtonText: "新增人員",
dialogType: "personnel",
showInTemplate: true,
showInFill: true,
columns: [
{ prop: "index", label: "項次", width: "60" },
{ prop: "name", label: "姓名" },
{ prop: "role", label: "職務" },
{ prop: "certNumber", label: "證照編號" },
],
},
],
},
};
/**
* 根據模板類型獲取對應的 schema
* @param {string} templateType - 模板類型
* @returns {object|null} 對應的 schema null
*/
export function getTemplateSchema(templateType) {
const schema = templateSchemas[templateType];
if (!schema) return null;
return schema;
}
/**
* 初始化模板表格資料
* @param {object} schema - 模板 schema
* @returns {object} 包含所有表格初始資料的物件
*/
export function initializeTableData(schema) {
if (!schema) return {};
const tableData = {};
// 初始化一般表格
if (schema.tables) {
schema.tables.forEach((table) => {
// 如果有預設資料,使用深拷貝避免引用問題
tableData[table.key] = table.defaultData
? JSON.parse(JSON.stringify(table.defaultData))
: [];
});
}
return tableData;
}
/**
* 初始化模板欄位資料
* @param {object} schema - 模板 schema
* @returns {object} 包含所有欄位初始值的物件
*/
export function initializeFieldData(schema) {
if (!schema || !schema.sections) return {};
const fieldData = {};
schema.sections.forEach((section) => {
section.fields.forEach((field) => {
fieldData[field.key] = "";
});
});
return fieldData;
}

View File

@ -9,28 +9,10 @@
</el-radio-group> </el-radio-group>
</template> </template>
<div v-if="radio === 'pending'"> <div v-if="radio === 'pending'">
<!-- 待完成內容 --> <PendingTasks />
<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>
<div v-if="radio === 'completed'"> <div v-if="radio === 'completed'">
<!-- 已完成內容 --> <CompletedTasks />
已完成任務列表
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
@ -38,43 +20,11 @@
</template> </template>
<script setup> <script setup>
import { ref, computed, reactive } from "vue"; import { ref } from "vue";
import { TakeawayBox, Loading, RefreshLeft } from "@element-plus/icons-vue"; import PendingTasks from "../components/PatrolMission/PendingTasks.vue";
import dayjs from "dayjs"; import CompletedTasks from "../components/PatrolMission/CompletedTasks.vue";
const radio = ref("pending"); 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 scoped>

View File

@ -31,6 +31,9 @@
<template v-else-if="tab.name === 'abnormal-record'"> <template v-else-if="tab.name === 'abnormal-record'">
異常紀錄 異常紀錄
</template> </template>
<template v-else-if="tab.name === 'operation-record'">
<OperationRecordTable />
</template>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</el-card> </el-card>
@ -42,6 +45,7 @@
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import CustomIframe from "../components/Common/CustomIframe.vue"; import CustomIframe from "../components/Common/CustomIframe.vue";
import OperationRecordTable from "../components/PlantInfo/OperationRecordTable.vue";
import { import {
Odometer, Odometer,
Postcard, Postcard,
@ -109,6 +113,12 @@ const tabList = computed(() => [
icon: Warning, icon: Warning,
src: null, src: null,
}, },
{
name: "operation-record",
label: "運維紀錄",
icon: Operation,
src: null,
},
]); ]);
</script> </script>