refactor(query-tool): align UI to semantic CSS classes matching system-wide conventions
Convert all 18 query-tool Vue components from Tailwind utility classes to semantic CSS classes (.header, .card, .btn-primary, .query-tool-tab, etc.) consistent with reject-history, hold-overview, and other pages. Create self-contained style.css with design tokens, base classes, and page-specific styles. Fix portal-shell native module loader to load query-tool/style.css instead of resource-shared/styles.css. Add CSS link tags to Django template for standalone page rendering. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -60,7 +60,7 @@ const NATIVE_MODULE_LOADERS = Object.freeze({
|
||||
),
|
||||
'/query-tool': createNativeLoader(
|
||||
() => import('../query-tool/App.vue'),
|
||||
[() => import('../resource-shared/styles.css')],
|
||||
[() => import('../query-tool/style.css')],
|
||||
),
|
||||
'/tmtt-defect': createNativeLoader(
|
||||
() => import('../tmtt-defect/App.vue'),
|
||||
|
||||
@@ -433,23 +433,23 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="u-content-shell space-y-3 p-3 lg:p-5">
|
||||
<header class="rounded-shell bg-gradient-to-r from-brand-500 to-accent-500 px-5 py-4 text-white shadow-shell">
|
||||
<h1 class="text-xl font-semibold tracking-wide">批次追蹤工具</h1>
|
||||
<p class="mt-1 text-xs text-indigo-100">正向/反向批次追溯與設備生產批次查詢整合入口</p>
|
||||
<div class="dashboard query-tool-page">
|
||||
<header class="header query-tool-header">
|
||||
<div class="header-left">
|
||||
<h1>批次追蹤工具</h1>
|
||||
<p class="query-tool-subtitle">正向/反向批次追溯與設備生產批次查詢整合入口</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="rounded-shell border border-stroke-panel bg-surface-card shadow-panel">
|
||||
<div class="border-b border-stroke-soft px-3 pt-3 lg:px-5">
|
||||
<nav class="flex flex-wrap gap-2" aria-label="query-tool tabs">
|
||||
<section class="card">
|
||||
<div class="card-header">
|
||||
<nav class="query-tool-tab-bar" aria-label="query-tool tabs">
|
||||
<button
|
||||
v-for="tab in tabItems"
|
||||
:key="tab.key"
|
||||
type="button"
|
||||
class="rounded-card border px-4 py-2 text-sm font-medium transition"
|
||||
:class="tab.key === activeTab
|
||||
? 'border-brand-500 bg-brand-50 text-brand-700 shadow-soft'
|
||||
: 'border-transparent bg-surface-muted text-slate-600 hover:border-stroke-soft hover:text-slate-800'"
|
||||
class="query-tool-tab"
|
||||
:class="{ active: tab.key === activeTab }"
|
||||
:aria-selected="tab.key === activeTab"
|
||||
:aria-current="tab.key === activeTab ? 'page' : undefined"
|
||||
@click="activateTab(tab.key)"
|
||||
@@ -459,11 +459,11 @@ watch(
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 px-3 py-4 lg:px-5">
|
||||
<div class="rounded-card border border-stroke-soft bg-surface-muted/60 px-4 py-3">
|
||||
<p class="text-xs font-medium tracking-wide text-slate-500">目前頁籤</p>
|
||||
<h2 class="mt-1 text-base font-semibold text-slate-800">{{ activeTabMeta.label }}</h2>
|
||||
<p class="mt-1 text-sm text-slate-600">{{ activeTabMeta.subtitle }}</p>
|
||||
<div class="card-body">
|
||||
<div class="query-tool-tab-desc">
|
||||
<p class="query-tool-tab-desc-label">目前頁籤</p>
|
||||
<h2 class="query-tool-tab-desc-title">{{ activeTabMeta.label }}</h2>
|
||||
<p class="query-tool-tab-desc-subtitle">{{ activeTabMeta.subtitle }}</p>
|
||||
</div>
|
||||
|
||||
<LotTraceView
|
||||
|
||||
@@ -63,9 +63,9 @@ const columns = Object.freeze([
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="rounded-card border border-stroke-soft bg-white p-3">
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<h4 class="text-sm font-semibold text-slate-800">維修紀錄</h4>
|
||||
<div>
|
||||
<div class="query-tool-section-header">
|
||||
<h4 class="card-title">維修紀錄</h4>
|
||||
<ExportButton
|
||||
:disabled="exportDisabled"
|
||||
:loading="exporting"
|
||||
@@ -74,28 +74,24 @@ const columns = Object.freeze([
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="mb-2 rounded-card border border-state-danger/40 bg-rose-50 px-3 py-2 text-xs text-state-danger">
|
||||
<p v-if="error" class="error-banner">
|
||||
{{ error }}
|
||||
</p>
|
||||
|
||||
<div v-if="loading" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-if="loading" class="placeholder">
|
||||
載入中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="rows.length === 0" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-else-if="rows.length === 0" class="placeholder">
|
||||
無維修紀錄
|
||||
</div>
|
||||
|
||||
<div v-else class="max-h-[460px] overflow-auto rounded-card border border-stroke-soft">
|
||||
<table class="min-w-full border-collapse text-xs">
|
||||
<thead class="sticky top-0 z-10 bg-slate-100 text-slate-700">
|
||||
<div v-else class="query-tool-table-wrap tall">
|
||||
<table class="query-tool-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">展開</th>
|
||||
<th
|
||||
v-for="column in columns"
|
||||
:key="column"
|
||||
class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold"
|
||||
>
|
||||
<th>展開</th>
|
||||
<th v-for="column in columns" :key="column">
|
||||
{{ column }}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -103,19 +99,15 @@ const columns = Object.freeze([
|
||||
|
||||
<tbody>
|
||||
<template v-for="(row, rowIndex) in rows" :key="rowKey(row, rowIndex)">
|
||||
<tr class="cursor-pointer odd:bg-white even:bg-slate-50" @click="toggleRow(row, rowIndex)">
|
||||
<td class="border-b border-stroke-soft/70 px-2 py-1.5 text-center text-slate-500">{{ isExpanded(row, rowIndex) ? '▾' : '▸' }}</td>
|
||||
<td
|
||||
v-for="column in columns"
|
||||
:key="`${rowIndex}-${column}`"
|
||||
class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700"
|
||||
>
|
||||
<tr style="cursor: pointer" @click="toggleRow(row, rowIndex)">
|
||||
<td style="text-align: center">{{ isExpanded(row, rowIndex) ? '▾' : '▸' }}</td>
|
||||
<td v-for="column in columns" :key="`${rowIndex}-${column}`">
|
||||
{{ formatCellValue(row[column]) }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="isExpanded(row, rowIndex)" class="bg-slate-50/60">
|
||||
<td class="border-b border-stroke-soft/70 px-2 py-2" colspan="9">
|
||||
<tr v-if="isExpanded(row, rowIndex)">
|
||||
<td colspan="9" style="padding: 8px 10px">
|
||||
<div class="grid gap-2 text-[11px] text-slate-600 md:grid-cols-2">
|
||||
<p><span class="font-semibold text-slate-700">RESOURCEID:</span> {{ formatCellValue(row.RESOURCEID) }}</p>
|
||||
<p><span class="font-semibold text-slate-700">JOBMODELNAME:</span> {{ formatCellValue(row.JOBMODELNAME) }}</p>
|
||||
@@ -129,5 +121,5 @@ const columns = Object.freeze([
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -41,9 +41,9 @@ const columns = Object.freeze([
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="rounded-card border border-stroke-soft bg-white p-3">
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<h4 class="text-sm font-semibold text-slate-800">生產紀錄</h4>
|
||||
<div>
|
||||
<div class="query-tool-section-header">
|
||||
<h4 class="card-title">生產紀錄</h4>
|
||||
<ExportButton
|
||||
:disabled="exportDisabled"
|
||||
:loading="exporting"
|
||||
@@ -52,44 +52,36 @@ const columns = Object.freeze([
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="mb-2 rounded-card border border-state-danger/40 bg-rose-50 px-3 py-2 text-xs text-state-danger">
|
||||
<p v-if="error" class="error-banner">
|
||||
{{ error }}
|
||||
</p>
|
||||
|
||||
<div v-if="loading" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-if="loading" class="placeholder">
|
||||
載入中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="rows.length === 0" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-else-if="rows.length === 0" class="placeholder">
|
||||
無生產紀錄
|
||||
</div>
|
||||
|
||||
<div v-else class="max-h-[460px] overflow-auto rounded-card border border-stroke-soft">
|
||||
<table class="min-w-full border-collapse text-xs">
|
||||
<thead class="sticky top-0 z-10 bg-slate-100 text-slate-700">
|
||||
<div v-else class="query-tool-table-wrap tall">
|
||||
<table class="query-tool-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="column in columns"
|
||||
:key="column"
|
||||
class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold"
|
||||
>
|
||||
<th v-for="column in columns" :key="column">
|
||||
{{ column }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="(row, rowIndex) in rows" :key="row.HISTORYMAINLINEID || rowIndex" class="odd:bg-white even:bg-slate-50">
|
||||
<td
|
||||
v-for="column in columns"
|
||||
:key="`${rowIndex}-${column}`"
|
||||
class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700"
|
||||
>
|
||||
<tr v-for="(row, rowIndex) in rows" :key="row.HISTORYMAINLINEID || rowIndex">
|
||||
<td v-for="column in columns" :key="`${rowIndex}-${column}`">
|
||||
{{ formatCellValue(row[column]) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,9 +37,9 @@ const columns = Object.freeze([
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="rounded-card border border-stroke-soft bg-white p-3">
|
||||
<div class="mb-2 flex items-center justify-between gap-2">
|
||||
<h4 class="text-sm font-semibold text-slate-800">報廢紀錄</h4>
|
||||
<div>
|
||||
<div class="query-tool-section-header">
|
||||
<h4 class="card-title">報廢紀錄</h4>
|
||||
<ExportButton
|
||||
:disabled="exportDisabled"
|
||||
:loading="exporting"
|
||||
@@ -48,44 +48,36 @@ const columns = Object.freeze([
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="mb-2 rounded-card border border-state-danger/40 bg-rose-50 px-3 py-2 text-xs text-state-danger">
|
||||
<p v-if="error" class="error-banner">
|
||||
{{ error }}
|
||||
</p>
|
||||
|
||||
<div v-if="loading" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-if="loading" class="placeholder">
|
||||
載入中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="rows.length === 0" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-else-if="rows.length === 0" class="placeholder">
|
||||
無報廢紀錄
|
||||
</div>
|
||||
|
||||
<div v-else class="max-h-[460px] overflow-auto rounded-card border border-stroke-soft">
|
||||
<table class="min-w-full border-collapse text-xs">
|
||||
<thead class="sticky top-0 z-10 bg-slate-100 text-slate-700">
|
||||
<div v-else class="query-tool-table-wrap tall">
|
||||
<table class="query-tool-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="column in columns"
|
||||
:key="column"
|
||||
class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold"
|
||||
>
|
||||
<th v-for="column in columns" :key="column">
|
||||
{{ column }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="(row, rowIndex) in rows" :key="rowIndex" class="odd:bg-white even:bg-slate-50">
|
||||
<td
|
||||
v-for="column in columns"
|
||||
:key="`${rowIndex}-${column}`"
|
||||
class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700"
|
||||
>
|
||||
<tr v-for="(row, rowIndex) in rows" :key="rowIndex">
|
||||
<td v-for="column in columns" :key="`${rowIndex}-${column}`">
|
||||
{{ formatCellValue(row[column]) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -209,9 +209,9 @@ const showEmpty = computed(() => tracks.value.length === 0 || (tracks.value.ever
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="rounded-card border border-stroke-soft bg-white p-3">
|
||||
<div class="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<h4 class="text-sm font-semibold text-slate-800">設備 Timeline</h4>
|
||||
<div>
|
||||
<div class="query-tool-section-header">
|
||||
<h4 class="card-title">設備 Timeline</h4>
|
||||
<ExportButton
|
||||
:disabled="exportDisabled"
|
||||
:loading="exporting"
|
||||
@@ -220,15 +220,15 @@ const showEmpty = computed(() => tracks.value.length === 0 || (tracks.value.ever
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="mb-2 rounded-card border border-state-danger/40 bg-rose-50 px-3 py-2 text-xs text-state-danger">
|
||||
<p v-if="error" class="error-banner">
|
||||
{{ error }}
|
||||
</p>
|
||||
|
||||
<div v-if="loading" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-if="loading" class="placeholder">
|
||||
Timeline 資料載入中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="showEmpty" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-else-if="showEmpty" class="placeholder">
|
||||
無 Timeline 資料
|
||||
</div>
|
||||
|
||||
@@ -242,5 +242,5 @@ const showEmpty = computed(() => tracks.value.length === 0 || (tracks.value.ever
|
||||
:label-width="220"
|
||||
:min-chart-width="1200"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -88,82 +88,83 @@ const subTabs = Object.keys(tabMeta);
|
||||
|
||||
<template>
|
||||
<div class="space-y-3">
|
||||
<section class="rounded-card border border-stroke-soft bg-white p-3 shadow-soft">
|
||||
<FilterToolbar>
|
||||
<label class="flex min-w-[320px] flex-col gap-1 text-xs text-slate-500">
|
||||
<span class="font-medium">設備(可複選)</span>
|
||||
<MultiSelect
|
||||
:model-value="selectedEquipmentIds"
|
||||
:options="equipmentOptions"
|
||||
:disabled="loading.bootstrapping"
|
||||
searchable
|
||||
placeholder="請選擇設備"
|
||||
@update:model-value="emit('update:selected-equipment-ids', $event)"
|
||||
/>
|
||||
</label>
|
||||
<section class="card">
|
||||
<div class="card-body">
|
||||
<FilterToolbar>
|
||||
<label class="filter-group" style="min-width: 320px">
|
||||
<span class="filter-label">設備(可複選)</span>
|
||||
<MultiSelect
|
||||
:model-value="selectedEquipmentIds"
|
||||
:options="equipmentOptions"
|
||||
:disabled="loading.bootstrapping"
|
||||
searchable
|
||||
placeholder="請選擇設備"
|
||||
@update:model-value="emit('update:selected-equipment-ids', $event)"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="flex min-w-[180px] flex-col gap-1 text-xs text-slate-500">
|
||||
<span class="font-medium">開始日期</span>
|
||||
<input
|
||||
type="date"
|
||||
class="h-9 rounded-card border border-stroke-soft bg-white px-3 text-sm text-slate-700 outline-none focus:border-brand-500"
|
||||
:value="startDate"
|
||||
@input="emit('update:start-date', $event.target.value)"
|
||||
/>
|
||||
</label>
|
||||
<label class="filter-group">
|
||||
<span class="filter-label">開始日期</span>
|
||||
<input
|
||||
type="date"
|
||||
class="query-tool-date-input"
|
||||
:value="startDate"
|
||||
@input="emit('update:start-date', $event.target.value)"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="flex min-w-[180px] flex-col gap-1 text-xs text-slate-500">
|
||||
<span class="font-medium">結束日期</span>
|
||||
<input
|
||||
type="date"
|
||||
class="h-9 rounded-card border border-stroke-soft bg-white px-3 text-sm text-slate-700 outline-none focus:border-brand-500"
|
||||
:value="endDate"
|
||||
@input="emit('update:end-date', $event.target.value)"
|
||||
/>
|
||||
</label>
|
||||
<label class="filter-group">
|
||||
<span class="filter-label">結束日期</span>
|
||||
<input
|
||||
type="date"
|
||||
class="query-tool-date-input"
|
||||
:value="endDate"
|
||||
@input="emit('update:end-date', $event.target.value)"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<template #actions>
|
||||
<button
|
||||
type="button"
|
||||
class="h-9 rounded-card border border-stroke-soft bg-white px-3 text-xs font-medium text-slate-600 transition hover:bg-slate-50"
|
||||
@click="emit('reset-date-range')"
|
||||
>
|
||||
近 30 天
|
||||
</button>
|
||||
<template #actions>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost"
|
||||
@click="emit('reset-date-range')"
|
||||
>
|
||||
近 30 天
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="h-9 rounded-card bg-brand-500 px-4 text-sm font-medium text-white transition hover:bg-brand-600"
|
||||
:disabled="loading[activeSubTab] || loading.timeline"
|
||||
@click="emit('query-active-sub-tab')"
|
||||
>
|
||||
{{ loading[activeSubTab] || loading.timeline ? '查詢中...' : '查詢' }}
|
||||
</button>
|
||||
</template>
|
||||
</FilterToolbar>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
:disabled="loading[activeSubTab] || loading.timeline"
|
||||
@click="emit('query-active-sub-tab')"
|
||||
>
|
||||
{{ loading[activeSubTab] || loading.timeline ? '查詢中...' : '查詢' }}
|
||||
</button>
|
||||
</template>
|
||||
</FilterToolbar>
|
||||
|
||||
<p v-if="errors.filters" class="mt-2 rounded-card border border-state-danger/40 bg-rose-50 px-3 py-2 text-xs text-state-danger">
|
||||
{{ errors.filters }}
|
||||
</p>
|
||||
<p v-if="errors.filters" class="error-banner" style="margin-top: 8px">
|
||||
{{ errors.filters }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-card border border-stroke-soft bg-white p-3 shadow-soft">
|
||||
<div class="mb-3 flex flex-wrap gap-2 border-b border-stroke-soft pb-2">
|
||||
<button
|
||||
v-for="tab in subTabs"
|
||||
:key="tab"
|
||||
type="button"
|
||||
class="rounded-card border px-3 py-1.5 text-xs font-medium transition"
|
||||
:class="tab === activeSubTab
|
||||
? 'border-brand-500 bg-brand-50 text-brand-700'
|
||||
: 'border-transparent bg-surface-muted/70 text-slate-600 hover:border-stroke-soft hover:text-slate-800'"
|
||||
@click="emit('change-sub-tab', tab)"
|
||||
>
|
||||
{{ tabMeta[tab] }}
|
||||
</button>
|
||||
</div>
|
||||
<section class="card">
|
||||
<div class="card-body">
|
||||
<div class="query-tool-sub-tab-bar">
|
||||
<button
|
||||
v-for="tab in subTabs"
|
||||
:key="tab"
|
||||
type="button"
|
||||
class="query-tool-sub-tab"
|
||||
:class="{ active: tab === activeSubTab }"
|
||||
@click="emit('change-sub-tab', tab)"
|
||||
>
|
||||
{{ tabMeta[tab] }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<EquipmentLotsTable
|
||||
<EquipmentLotsTable
|
||||
v-if="activeSubTab === 'lots'"
|
||||
:rows="lotsRows"
|
||||
:loading="loading.lots"
|
||||
@@ -208,6 +209,7 @@ const subTabs = Object.keys(tabMeta);
|
||||
:exporting="exporting.timeline"
|
||||
@export="emit('export-sub-tab', 'timeline')"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -18,7 +18,7 @@ const props = defineProps({
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-card bg-emerald-600 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-emerald-700 disabled:cursor-not-allowed disabled:bg-emerald-300"
|
||||
class="btn btn-export"
|
||||
:disabled="disabled || loading"
|
||||
>
|
||||
{{ loading ? '匯出中...' : label }}
|
||||
|
||||
@@ -707,12 +707,12 @@ function exportRelationCsv() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="rounded-card border border-stroke-soft bg-white p-3 shadow-soft">
|
||||
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<section class="card"><div class="card-body">
|
||||
<div class="query-tool-section-header">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-slate-800">{{ title }}</h3>
|
||||
<p class="text-xs text-slate-500">{{ description }}</p>
|
||||
<p class="text-[11px] text-slate-500">
|
||||
<h3 class="card-title">{{ title }}</h3>
|
||||
<p class="query-tool-muted">{{ description }}</p>
|
||||
<p class="query-tool-muted" style="font-size: 11px">
|
||||
讀圖方向由左至右;節點前綴 <code>←拆/←併/←晶/←重</code> 代表本節點由左側來源而來,
|
||||
<code>→拆/→併/→晶/→重</code> 代表左側節點由本節點而來。
|
||||
</p>
|
||||
@@ -775,20 +775,17 @@ function exportRelationCsv() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="exportErrorMessage" class="mb-2 rounded border border-rose-200 bg-rose-50 px-2 py-1 text-xs text-rose-700">
|
||||
<p v-if="exportErrorMessage" class="error-banner">
|
||||
{{ exportErrorMessage }}
|
||||
</p>
|
||||
|
||||
<!-- Loading overlay -->
|
||||
<div v-if="loading" class="flex items-center justify-center rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 py-16">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<span class="inline-block size-5 animate-spin rounded-full border-2 border-brand-500 border-t-transparent" />
|
||||
<span class="text-xs text-slate-500">正在載入血緣資料…</span>
|
||||
</div>
|
||||
<div v-if="loading" class="placeholder" style="padding: 48px 12px">
|
||||
正在載入血緣資料…
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-else-if="!hasData" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-10 text-center text-xs text-slate-500">
|
||||
<div v-else-if="!hasData" class="placeholder">
|
||||
{{ emptyMessage }}
|
||||
</div>
|
||||
|
||||
@@ -804,52 +801,51 @@ function exportRelationCsv() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<details v-if="relationRows.length > 0" class="mt-3 rounded-card border border-stroke-soft bg-surface-muted/50 px-3 py-2">
|
||||
<summary class="cursor-pointer text-xs font-medium text-slate-700">
|
||||
<details v-if="relationRows.length > 0" style="margin-top: 12px; border: 1px solid var(--border); border-radius: 8px; background: #f8fafc; padding: 8px 12px">
|
||||
<summary style="cursor: pointer; font-size: 12px; font-weight: 600; color: #334155">
|
||||
關係清單({{ relationRows.length }})
|
||||
</summary>
|
||||
<div class="mt-2 max-h-56 overflow-auto rounded border border-stroke-soft bg-white">
|
||||
<table class="min-w-full text-left text-xs text-slate-700">
|
||||
<thead class="bg-slate-50 text-[11px] text-slate-500">
|
||||
<div class="query-tool-table-wrap short" style="margin-top: 8px; max-height: 224px">
|
||||
<table class="query-tool-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="px-2 py-1.5 font-medium">來源批次</th>
|
||||
<th class="px-2 py-1.5 font-medium">目標批次</th>
|
||||
<th class="px-2 py-1.5 font-medium">關係</th>
|
||||
<th>來源批次</th>
|
||||
<th>目標批次</th>
|
||||
<th>關係</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in relationRows.slice(0, 200)"
|
||||
:key="row.key"
|
||||
class="border-t border-slate-100"
|
||||
>
|
||||
<td class="px-2 py-1.5 font-mono text-[11px]">
|
||||
<td style="font-family: monospace; font-size: 11px">
|
||||
{{ row.fromName }}
|
||||
</td>
|
||||
<td class="px-2 py-1.5 font-mono text-[11px]">
|
||||
<td style="font-family: monospace; font-size: 11px">
|
||||
{{ row.toName }}
|
||||
</td>
|
||||
<td class="px-2 py-1.5 text-[11px]">
|
||||
<td style="font-size: 11px">
|
||||
{{ row.edgeLabel }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-if="relationRows.length > 200" class="mt-1 text-[11px] text-slate-500">
|
||||
<p v-if="relationRows.length > 200" class="query-tool-muted" style="margin-top: 4px; font-size: 11px">
|
||||
僅顯示前 200 筆,請搭配上方樹圖與節點點選進一步縮小範圍。
|
||||
</p>
|
||||
</details>
|
||||
|
||||
<!-- Not found warning -->
|
||||
<div v-if="notFound.length > 0" class="mt-3 rounded-card border border-state-warning/40 bg-amber-50 px-3 py-2 text-xs text-amber-700">
|
||||
<div v-if="notFound.length > 0" class="query-tool-success" style="border-color: #fde68a; background: #fefce8; color: #92400e">
|
||||
未命中:{{ notFound.join(', ') }}
|
||||
</div>
|
||||
|
||||
<!-- Selection summary -->
|
||||
<div v-if="selectedContainerIds.length > 0" class="mt-3 rounded-card border border-brand-200 bg-brand-50/60 px-3 py-2">
|
||||
<div v-if="selectedContainerIds.length > 0" class="query-tool-success" style="border-color: #c7d2fe; background: rgba(238,242,255,0.6); color: inherit">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<span class="mr-1 text-xs font-medium text-brand-700">已選 {{ selectedContainerIds.length }} 個節點</span>
|
||||
<span class="mr-1 text-xs font-medium" style="color: #4338ca">已選 {{ selectedContainerIds.length }} 個節點</span>
|
||||
<span
|
||||
v-for="cid in selectedContainerIds.slice(0, 8)"
|
||||
:key="cid"
|
||||
@@ -860,7 +856,7 @@ function exportRelationCsv() {
|
||||
<span v-if="selectedContainerIds.length > 8" class="text-xs text-brand-600">+{{ selectedContainerIds.length - 8 }} 更多</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div></section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -65,41 +65,33 @@ function resolveColumnLabel(column) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="rounded-card border border-stroke-soft bg-white p-3">
|
||||
<div v-if="loading" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div>
|
||||
<div v-if="loading" class="placeholder">
|
||||
讀取中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="rows.length === 0" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-else-if="rows.length === 0" class="placeholder">
|
||||
{{ emptyText }}
|
||||
</div>
|
||||
|
||||
<div v-else class="max-h-[420px] overflow-auto rounded-card border border-stroke-soft">
|
||||
<table class="min-w-full border-collapse text-xs">
|
||||
<thead class="sticky top-0 z-10 bg-slate-100 text-slate-700">
|
||||
<div v-else class="query-tool-table-wrap">
|
||||
<table class="query-tool-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="column in columns"
|
||||
:key="column"
|
||||
class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold"
|
||||
>
|
||||
<th v-for="column in columns" :key="column">
|
||||
{{ resolveColumnLabel(column) }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="(row, rowIndex) in rows" :key="row.id || row.JOBID || rowIndex" class="odd:bg-white even:bg-slate-50">
|
||||
<td
|
||||
v-for="column in columns"
|
||||
:key="`${rowIndex}-${column}`"
|
||||
class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700"
|
||||
>
|
||||
<tr v-for="(row, rowIndex) in rows" :key="row.id || row.JOBID || rowIndex">
|
||||
<td v-for="column in columns" :key="`${rowIndex}-${column}`">
|
||||
{{ formatCellValue(row[column]) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -193,46 +193,45 @@ const detailCountLabel = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="rounded-card border border-stroke-soft bg-white p-3 shadow-soft">
|
||||
<div v-if="!selectedContainerId" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-8 text-center text-xs text-slate-500">
|
||||
請從上方血緣樹選擇節點後查看明細。
|
||||
</div>
|
||||
<section class="card">
|
||||
<div class="card-body">
|
||||
<div v-if="!selectedContainerId" class="placeholder">
|
||||
請從上方血緣樹選擇節點後查看明細。
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-slate-800">LOT 明細{{ detailCountLabel }}</h3>
|
||||
<p class="text-xs text-slate-500">{{ detailDisplayNames }}</p>
|
||||
<template v-else>
|
||||
<div class="query-tool-section-header">
|
||||
<div>
|
||||
<div class="card-title">LOT 明細{{ detailCountLabel }}</div>
|
||||
<span class="query-tool-muted">{{ detailDisplayNames }}</span>
|
||||
</div>
|
||||
|
||||
<ExportButton
|
||||
:disabled="!canExport"
|
||||
:loading="activeExporting"
|
||||
:label="`${tabMeta[activeSubTab]?.label || ''} 匯出 CSV`"
|
||||
@click="emit('export-tab', activeSubTab)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ExportButton
|
||||
:disabled="!canExport"
|
||||
:loading="activeExporting"
|
||||
:label="`${tabMeta[activeSubTab]?.label || ''} 匯出 CSV`"
|
||||
@click="emit('export-tab', activeSubTab)"
|
||||
/>
|
||||
</div>
|
||||
<div class="query-tool-sub-tab-bar">
|
||||
<button
|
||||
v-for="tab in subTabs"
|
||||
:key="tab"
|
||||
type="button"
|
||||
class="query-tool-sub-tab"
|
||||
:class="{ active: tab === activeSubTab }"
|
||||
@click="emit('change-sub-tab', tab)"
|
||||
>
|
||||
{{ tabMeta[tab].label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 flex flex-wrap gap-2 border-b border-stroke-soft pb-2">
|
||||
<button
|
||||
v-for="tab in subTabs"
|
||||
:key="tab"
|
||||
type="button"
|
||||
class="rounded-card border px-3 py-1.5 text-xs font-medium transition"
|
||||
:class="tab === activeSubTab
|
||||
? 'border-brand-500 bg-brand-50 text-brand-700'
|
||||
: 'border-transparent bg-surface-muted/70 text-slate-600 hover:border-stroke-soft hover:text-slate-800'"
|
||||
@click="emit('change-sub-tab', tab)"
|
||||
>
|
||||
{{ tabMeta[tab].label }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="activeError" class="error-banner">
|
||||
{{ activeError }}
|
||||
</p>
|
||||
|
||||
<p v-if="activeError" class="mb-2 rounded-card border border-state-danger/40 bg-rose-50 px-3 py-2 text-xs text-state-danger">
|
||||
{{ activeError }}
|
||||
</p>
|
||||
|
||||
<div v-if="activeSubTab === 'history'" class="space-y-3">
|
||||
<div v-if="activeSubTab === 'history'" class="space-y-3">
|
||||
<LotTimeline
|
||||
:history-rows="historyRows"
|
||||
:hold-rows="associationRows.holds || []"
|
||||
@@ -271,6 +270,7 @@ const detailCountLabel = computed(() => {
|
||||
:loading="activeLoading"
|
||||
:empty-text="activeLoaded ? activeEmptyText : '尚未查詢此分頁資料'"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -43,12 +43,12 @@ const workcenterOptions = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="rounded-card border border-stroke-soft bg-white p-3">
|
||||
<div class="mb-2 flex flex-wrap items-end justify-between gap-2">
|
||||
<h4 class="text-sm font-semibold text-slate-800">歷程資料</h4>
|
||||
<div>
|
||||
<div class="query-tool-section-header">
|
||||
<h4 class="card-title">歷程資料</h4>
|
||||
|
||||
<label class="min-w-[260px] text-xs text-slate-500">
|
||||
<span class="mb-1 block font-medium">站點群組篩選</span>
|
||||
<label class="filter-group" style="min-width: 260px">
|
||||
<span class="filter-label">站點群組篩選</span>
|
||||
<MultiSelect
|
||||
:model-value="selectedWorkcenterGroups"
|
||||
:options="workcenterOptions"
|
||||
@@ -60,23 +60,19 @@ const workcenterOptions = computed(() => {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-if="loading" class="placeholder">
|
||||
歷程資料讀取中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="rows.length === 0" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-else-if="rows.length === 0" class="placeholder">
|
||||
無歷程資料
|
||||
</div>
|
||||
|
||||
<div v-else class="max-h-[360px] overflow-auto rounded-card border border-stroke-soft">
|
||||
<table class="min-w-full border-collapse text-xs">
|
||||
<thead class="sticky top-0 z-10 bg-slate-100 text-slate-700">
|
||||
<div v-else class="query-tool-table-wrap short">
|
||||
<table class="query-tool-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="column in columns"
|
||||
:key="column"
|
||||
class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold"
|
||||
>
|
||||
<th v-for="column in columns" :key="column">
|
||||
{{ column }}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -86,18 +82,13 @@ const workcenterOptions = computed(() => {
|
||||
<tr
|
||||
v-for="(row, rowIndex) in rows"
|
||||
:key="row.HISTORYMAINLINEID || row.TRACKINTIMESTAMP || rowIndex"
|
||||
class="odd:bg-white even:bg-slate-50"
|
||||
>
|
||||
<td
|
||||
v-for="column in columns"
|
||||
:key="`${rowIndex}-${column}`"
|
||||
class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700"
|
||||
>
|
||||
<td v-for="column in columns" :key="`${rowIndex}-${column}`">
|
||||
{{ formatCellValue(row[column]) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -174,46 +174,39 @@ async function loadTxn(jobId) {
|
||||
|
||||
<template>
|
||||
<section class="space-y-3">
|
||||
<div class="rounded-card border border-stroke-soft bg-white p-3">
|
||||
<div v-if="loading" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div>
|
||||
<div v-if="loading" class="placeholder">
|
||||
讀取中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="sortedRows.length === 0" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-else-if="sortedRows.length === 0" class="placeholder">
|
||||
{{ emptyText }}
|
||||
</div>
|
||||
|
||||
<div v-else class="max-h-[420px] overflow-auto rounded-card border border-stroke-soft">
|
||||
<table class="min-w-full border-collapse text-xs">
|
||||
<thead class="sticky top-0 z-10 bg-slate-100 text-slate-700">
|
||||
<div v-else class="query-tool-table-wrap">
|
||||
<table class="query-tool-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">操作</th>
|
||||
<th
|
||||
v-for="column in jobColumns"
|
||||
:key="column"
|
||||
class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold"
|
||||
>
|
||||
<th>操作</th>
|
||||
<th v-for="column in jobColumns" :key="column">
|
||||
{{ column }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr v-for="(row, rowIndex) in sortedRows" :key="rowKey(row, rowIndex)" class="odd:bg-white even:bg-slate-50">
|
||||
<td class="border-b border-stroke-soft/70 px-2 py-1.5">
|
||||
<tr v-for="(row, rowIndex) in sortedRows" :key="rowKey(row, rowIndex)">
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-card border border-stroke-soft bg-white px-2 py-1 text-[11px] font-medium text-slate-600 transition hover:bg-surface-muted/70 hover:text-slate-800"
|
||||
class="btn btn-ghost"
|
||||
style="padding: 2px 8px; font-size: 11px"
|
||||
@click="loadTxn(row?.JOBID)"
|
||||
>
|
||||
查看交易歷程
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
v-for="column in jobColumns"
|
||||
:key="`${rowKey(row, rowIndex)}-${column}`"
|
||||
class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700"
|
||||
>
|
||||
<td v-for="column in jobColumns" :key="`${rowKey(row, rowIndex)}-${column}`">
|
||||
<StatusBadge
|
||||
v-if="column === 'JOBSTATUS'"
|
||||
:tone="buildStatusTone(row?.[column])"
|
||||
@@ -227,33 +220,29 @@ async function loadTxn(jobId) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedJobId" class="rounded-card border border-stroke-soft bg-white p-3">
|
||||
<div class="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<h4 class="text-sm font-semibold text-slate-800">交易歷程:{{ selectedJobId }}</h4>
|
||||
<span class="text-xs text-slate-500">{{ txnRows.length }} 筆</span>
|
||||
<div v-if="selectedJobId">
|
||||
<div class="query-tool-section-header">
|
||||
<h4 class="card-title">交易歷程:{{ selectedJobId }}</h4>
|
||||
<span class="query-tool-muted">{{ txnRows.length }} 筆</span>
|
||||
</div>
|
||||
|
||||
<p v-if="txnError" class="mb-2 rounded-card border border-state-danger/40 bg-rose-50 px-3 py-2 text-xs text-state-danger">
|
||||
<p v-if="txnError" class="error-banner">
|
||||
{{ txnError }}
|
||||
</p>
|
||||
|
||||
<div v-if="loadingTxn" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-if="loadingTxn" class="placeholder">
|
||||
載入交易歷程中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="txnRows.length === 0" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-else-if="txnRows.length === 0" class="placeholder">
|
||||
無交易歷程資料
|
||||
</div>
|
||||
|
||||
<div v-else class="max-h-[420px] overflow-auto rounded-card border border-stroke-soft">
|
||||
<table class="min-w-full border-collapse text-xs">
|
||||
<thead class="sticky top-0 z-10 bg-slate-100 text-slate-700">
|
||||
<div v-else class="query-tool-table-wrap">
|
||||
<table class="query-tool-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
v-for="column in txnColumns"
|
||||
:key="column"
|
||||
class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold"
|
||||
>
|
||||
<th v-for="column in txnColumns" :key="column">
|
||||
{{ column }}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -263,13 +252,8 @@ async function loadTxn(jobId) {
|
||||
<tr
|
||||
v-for="(row, rowIndex) in txnRows"
|
||||
:key="row?.JOBTXNHISTORYID || `${selectedJobId}-${rowIndex}`"
|
||||
class="odd:bg-white even:bg-slate-50"
|
||||
>
|
||||
<td
|
||||
v-for="column in txnColumns"
|
||||
:key="`${row?.JOBTXNHISTORYID || rowIndex}-${column}`"
|
||||
class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700"
|
||||
>
|
||||
<td v-for="column in txnColumns" :key="`${row?.JOBTXNHISTORYID || rowIndex}-${column}`">
|
||||
<StatusBadge
|
||||
v-if="column === 'JOBSTATUS' || column === 'FROMJOBSTATUS'"
|
||||
:tone="buildStatusTone(row?.[column])"
|
||||
|
||||
@@ -96,43 +96,43 @@ const sortedRows = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="rounded-card border border-stroke-soft bg-white p-3">
|
||||
<div v-if="loading" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div>
|
||||
<div v-if="loading" class="placeholder">
|
||||
讀取中...
|
||||
</div>
|
||||
|
||||
<div v-else-if="sortedRows.length === 0" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-else-if="sortedRows.length === 0" class="placeholder">
|
||||
{{ emptyText }}
|
||||
</div>
|
||||
|
||||
<div v-else class="max-h-[420px] overflow-auto rounded-card border border-stroke-soft">
|
||||
<table class="min-w-full border-collapse text-xs">
|
||||
<thead class="sticky top-0 z-10 bg-slate-100 text-slate-700">
|
||||
<div v-else class="query-tool-table-wrap">
|
||||
<table class="query-tool-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">LOT</th>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">WORKCENTER</th>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">Package</th>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">FUNCTION</th>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">TYPE</th>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">PRODUCT</th>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">原因</th>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">EQUIPMENT</th>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">COMMENT</th>
|
||||
<th>LOT</th>
|
||||
<th>WORKCENTER</th>
|
||||
<th>Package</th>
|
||||
<th>FUNCTION</th>
|
||||
<th>TYPE</th>
|
||||
<th>PRODUCT</th>
|
||||
<th>原因</th>
|
||||
<th>EQUIPMENT</th>
|
||||
<th>COMMENT</th>
|
||||
<th
|
||||
class="cursor-pointer whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold hover:text-brand-700"
|
||||
style="cursor: pointer"
|
||||
@click="showRejectBreakdown = !showRejectBreakdown"
|
||||
>
|
||||
扣帳報廢量 <span>{{ showRejectBreakdown ? '▾' : '▸' }}</span>
|
||||
</th>
|
||||
<template v-if="showRejectBreakdown">
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">REJECT</th>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">STANDBY</th>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">QTYTOPROCESS</th>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">INPROCESS</th>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">PROCESSED</th>
|
||||
<th>REJECT</th>
|
||||
<th>STANDBY</th>
|
||||
<th>QTYTOPROCESS</th>
|
||||
<th>INPROCESS</th>
|
||||
<th>PROCESSED</th>
|
||||
</template>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">不扣帳報廢量</th>
|
||||
<th class="whitespace-nowrap border-b border-stroke-soft px-2 py-1.5 text-left font-semibold">報廢時間</th>
|
||||
<th>不扣帳報廢量</th>
|
||||
<th>報廢時間</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -140,30 +140,29 @@ const sortedRows = computed(() => {
|
||||
<tr
|
||||
v-for="(row, idx) in sortedRows"
|
||||
:key="`${row.TXN_TIME_RAW}-${row.CONTAINERNAME}-${row.LOSSREASONNAME}-${idx}`"
|
||||
class="odd:bg-white even:bg-slate-50"
|
||||
>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ row.CONTAINERNAME }}</td>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ row.WORKCENTERNAME || '-' }}</td>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ row.PRODUCTLINENAME || '-' }}</td>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ row.PJ_FUNCTION || '-' }}</td>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ row.PJ_TYPE || '-' }}</td>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ row.PRODUCTNAME || '-' }}</td>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ row.LOSSREASONNAME || '-' }}</td>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ row.EQUIPMENTNAME || '-' }}</td>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ row.REJECTCOMMENT || '-' }}</td>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ formatNumber(row.REJECT_TOTAL_QTY) }}</td>
|
||||
<td>{{ row.CONTAINERNAME }}</td>
|
||||
<td>{{ row.WORKCENTERNAME || '-' }}</td>
|
||||
<td>{{ row.PRODUCTLINENAME || '-' }}</td>
|
||||
<td>{{ row.PJ_FUNCTION || '-' }}</td>
|
||||
<td>{{ row.PJ_TYPE || '-' }}</td>
|
||||
<td>{{ row.PRODUCTNAME || '-' }}</td>
|
||||
<td>{{ row.LOSSREASONNAME || '-' }}</td>
|
||||
<td>{{ row.EQUIPMENTNAME || '-' }}</td>
|
||||
<td>{{ row.REJECTCOMMENT || '-' }}</td>
|
||||
<td>{{ formatNumber(row.REJECT_TOTAL_QTY) }}</td>
|
||||
<template v-if="showRejectBreakdown">
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ formatNumber(row.REJECT_QTY) }}</td>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ formatNumber(row.STANDBY_QTY) }}</td>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ formatNumber(row.QTYTOPROCESS_QTY) }}</td>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ formatNumber(row.INPROCESS_QTY) }}</td>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ formatNumber(row.PROCESSED_QTY) }}</td>
|
||||
<td>{{ formatNumber(row.REJECT_QTY) }}</td>
|
||||
<td>{{ formatNumber(row.STANDBY_QTY) }}</td>
|
||||
<td>{{ formatNumber(row.QTYTOPROCESS_QTY) }}</td>
|
||||
<td>{{ formatNumber(row.INPROCESS_QTY) }}</td>
|
||||
<td>{{ formatNumber(row.PROCESSED_QTY) }}</td>
|
||||
</template>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ formatNumber(row.DEFECT_QTY) }}</td>
|
||||
<td class="whitespace-nowrap border-b border-stroke-soft/70 px-2 py-1.5 text-slate-700">{{ row.TXN_TIME || '-' }}</td>
|
||||
<td>{{ formatNumber(row.DEFECT_QTY) }}</td>
|
||||
<td>{{ row.TXN_TIME || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -325,10 +325,10 @@ const timeRange = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="rounded-card border border-stroke-soft bg-white p-3">
|
||||
<div class="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||
<h4 class="text-sm font-semibold text-slate-800">LOT 生產 Timeline</h4>
|
||||
<div class="flex items-center gap-3 text-xs text-slate-500">
|
||||
<div>
|
||||
<div class="query-tool-section-header">
|
||||
<h4 class="card-title">LOT 生產 Timeline</h4>
|
||||
<div class="flex items-center gap-3 query-tool-muted">
|
||||
<span v-if="timeRange">{{ formatDateTime(timeRange.start) }} — {{ formatDateTime(timeRange.end) }}</span>
|
||||
<span>Hold / Material 事件已覆蓋標記</span>
|
||||
<span v-if="materialMappingStats.total > 0">
|
||||
@@ -340,11 +340,11 @@ const timeRange = computed(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tracks.length === 0" class="rounded-card border border-dashed border-stroke-soft bg-surface-muted/40 px-3 py-5 text-center text-xs text-slate-500">
|
||||
<div v-if="tracks.length === 0" class="placeholder">
|
||||
歷程資料不足,無法產生 Timeline
|
||||
</div>
|
||||
|
||||
<div v-else class="max-h-[520px] overflow-y-auto">
|
||||
<div v-else style="max-height: 520px; overflow-y: auto">
|
||||
<TimelineChart
|
||||
:tracks="tracks"
|
||||
:events="events"
|
||||
@@ -355,5 +355,5 @@ const timeRange = computed(() => {
|
||||
:min-chart-width="600"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -147,7 +147,7 @@ const emit = defineEmits([
|
||||
@resolve="emit('resolve')"
|
||||
/>
|
||||
|
||||
<p v-if="resolveSuccessMessage" class="rounded-card border border-state-success/40 bg-emerald-50 px-3 py-2 text-xs text-emerald-700">
|
||||
<p v-if="resolveSuccessMessage" class="query-tool-success">
|
||||
{{ resolveSuccessMessage }}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -51,53 +51,55 @@ function handleResolve() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="rounded-card border border-stroke-soft bg-white p-3 shadow-soft">
|
||||
<FilterToolbar>
|
||||
<label class="flex min-w-[220px] flex-col gap-1 text-xs text-slate-500">
|
||||
<span class="font-medium">查詢類型</span>
|
||||
<select
|
||||
class="h-9 rounded-card border border-stroke-soft bg-white px-3 text-sm text-slate-700 outline-none focus:border-brand-500"
|
||||
:value="inputType"
|
||||
:disabled="resolving"
|
||||
@change="emit('update:inputType', $event.target.value)"
|
||||
>
|
||||
<option
|
||||
v-for="option in inputTypeOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
<section class="card">
|
||||
<div class="card-body">
|
||||
<FilterToolbar>
|
||||
<label class="filter-group">
|
||||
<span class="filter-label">查詢類型</span>
|
||||
<select
|
||||
class="query-tool-select"
|
||||
:value="inputType"
|
||||
:disabled="resolving"
|
||||
@change="emit('update:inputType', $event.target.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<option
|
||||
v-for="option in inputTypeOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<template #actions>
|
||||
<button
|
||||
type="button"
|
||||
class="h-9 rounded-card bg-brand-500 px-4 text-sm font-medium text-white transition hover:bg-brand-600 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
<template #actions>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
:disabled="resolving"
|
||||
@click="handleResolve"
|
||||
>
|
||||
{{ resolving ? '解析中...' : '解析' }}
|
||||
</button>
|
||||
</template>
|
||||
</FilterToolbar>
|
||||
|
||||
<div style="margin-top: 12px">
|
||||
<textarea
|
||||
:value="inputText"
|
||||
class="query-tool-textarea"
|
||||
:placeholder="`請輸入 ${inputTypeLabel}(換行或逗號分隔),最多 ${inputLimit} 筆`"
|
||||
:disabled="resolving"
|
||||
@click="handleResolve"
|
||||
>
|
||||
{{ resolving ? '解析中...' : '解析' }}
|
||||
</button>
|
||||
</template>
|
||||
</FilterToolbar>
|
||||
|
||||
<div class="mt-3">
|
||||
<textarea
|
||||
:value="inputText"
|
||||
class="min-h-28 w-full rounded-card border border-stroke-soft bg-surface-muted/40 px-3 py-2 text-sm text-slate-700 outline-none transition focus:border-brand-500"
|
||||
:placeholder="`請輸入 ${inputTypeLabel}(換行或逗號分隔),最多 ${inputLimit} 筆`"
|
||||
:disabled="resolving"
|
||||
@input="emit('update:inputText', $event.target.value)"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-slate-500">
|
||||
支援萬用字元:<code>%</code>(任意長度)、<code>_</code>(單一字元),也可用 <code>*</code> 代表 <code>%</code>。
|
||||
例如:<code>GA25%01</code>、<code>GA25%</code>、<code>GMSN-1173%</code>
|
||||
</p>
|
||||
<div class="mt-2 flex items-center justify-between text-xs">
|
||||
<p class="text-slate-500">已輸入 {{ inputCount }} / {{ inputLimit }}</p>
|
||||
<p v-if="errorMessage" class="text-state-danger">{{ errorMessage }}</p>
|
||||
@input="emit('update:inputText', $event.target.value)"
|
||||
/>
|
||||
<p class="query-tool-input-help">
|
||||
支援萬用字元:<code>%</code>(任意長度)、<code>_</code>(單一字元),也可用 <code>*</code> 代表 <code>%</code>。
|
||||
例如:<code>GA25%01</code>、<code>GA25%</code>、<code>GMSN-1173%</code>
|
||||
</p>
|
||||
<div class="query-tool-input-counter">
|
||||
<span>已輸入 {{ inputCount }} / {{ inputLimit }}</span>
|
||||
<span v-if="errorMessage" class="query-tool-input-error">{{ errorMessage }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -147,7 +147,7 @@ const emit = defineEmits([
|
||||
@resolve="emit('resolve')"
|
||||
/>
|
||||
|
||||
<p v-if="resolveSuccessMessage" class="rounded-card border border-state-success/40 bg-emerald-50 px-3 py-2 text-xs text-emerald-700">
|
||||
<p v-if="resolveSuccessMessage" class="query-tool-success">
|
||||
{{ resolveSuccessMessage }}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createApp } from 'vue';
|
||||
|
||||
import '../wip-shared/styles.css';
|
||||
import '../styles/tailwind.css';
|
||||
import './style.css';
|
||||
import App from './App.vue';
|
||||
|
||||
createApp(App).mount('#app');
|
||||
|
||||
481
frontend/src/query-tool/style.css
Normal file
481
frontend/src/query-tool/style.css
Normal file
@@ -0,0 +1,481 @@
|
||||
/* ================================================================
|
||||
query-tool/style.css — 自包含頁面樣式
|
||||
包含 wip-shared 基礎變數與元件 + 頁面專屬樣式
|
||||
================================================================ */
|
||||
|
||||
/* ---- 設計 Token (與 wip-shared/styles.css 同步) ---- */
|
||||
:root {
|
||||
--bg: #f5f7fa;
|
||||
--card-bg: #ffffff;
|
||||
--text: #222;
|
||||
--muted: #666;
|
||||
--border: #e2e6ef;
|
||||
--primary: #667eea;
|
||||
--primary-dark: #5568d3;
|
||||
--shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
||||
--shadow-strong: 0 4px 15px rgba(102, 126, 234, 0.2);
|
||||
--success: #22c55e;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
}
|
||||
|
||||
/* ---- 全域重置 ---- */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft JhengHei', Arial, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ---- 頁面容器 ---- */
|
||||
.dashboard {
|
||||
max-width: 1900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.query-tool-page {
|
||||
max-width: 1900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* ---- Header (wip-shared) ---- */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding: 18px 22px;
|
||||
background: var(--header-gradient, linear-gradient(135deg, #667eea 0%, #764ba2 100%));
|
||||
border-radius: 10px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: var(--shadow-strong);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.query-tool-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.query-tool-subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
/* ---- 按鈕 (wip-shared) ---- */
|
||||
.btn {
|
||||
padding: 9px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
padding: 9px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-dark);
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-export {
|
||||
background: #0f766e;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-export:hover {
|
||||
background: #0b5e59;
|
||||
}
|
||||
|
||||
.btn-export:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.btn-ghost:hover {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
/* ---- 佔位 / 空狀態 (wip-shared) ---- */
|
||||
.placeholder {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ---- Card 基礎 ---- */
|
||||
.card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: visible;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #f8fafc;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
/* ---- Error 訊息 ---- */
|
||||
.error-banner {
|
||||
margin-bottom: 14px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ---- Tab 導航 (頂層) ---- */
|
||||
.query-tool-tab-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.query-tool-tab {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.query-tool-tab:hover:not(.active) {
|
||||
border-color: var(--border);
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.query-tool-tab.active {
|
||||
border-color: var(--primary);
|
||||
background: #eef2ff;
|
||||
color: #4c51bf;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* ---- Tab 說明區塊 ---- */
|
||||
.query-tool-tab-desc {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.query-tool-tab-desc-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.query-tool-tab-desc-title {
|
||||
margin-top: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.query-tool-tab-desc-subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
/* ---- Sub-tab 導航 (Detail / Equipment) ---- */
|
||||
.query-tool-sub-tab-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.query-tool-sub-tab {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
background: #f1f5f9;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.query-tool-sub-tab:hover:not(.active) {
|
||||
border-color: var(--border);
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.query-tool-sub-tab.active {
|
||||
border-color: var(--primary);
|
||||
background: #eef2ff;
|
||||
color: #4c51bf;
|
||||
}
|
||||
|
||||
/* ---- 表單控制元素 ---- */
|
||||
.query-tool-select {
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.query-tool-select:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
.query-tool-select:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.query-tool-textarea {
|
||||
width: 100%;
|
||||
min-height: 112px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.query-tool-textarea:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
.query-tool-textarea:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.query-tool-date-input {
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.query-tool-date-input:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.15);
|
||||
}
|
||||
|
||||
.query-tool-input-help {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.query-tool-input-help code {
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
background: #e2e8f0;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.query-tool-input-counter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.query-tool-input-error {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
/* ---- Filter label ---- */
|
||||
.filter-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
/* ---- 區段標頭 (標題 + 動作列) ---- */
|
||||
.query-tool-section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.query-tool-muted {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* ---- 表格 ---- */
|
||||
.query-tool-table-wrap {
|
||||
overflow: auto;
|
||||
max-height: 420px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.query-tool-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.query-tool-table th,
|
||||
.query-tool-table td {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.query-tool-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background: #f8fafc;
|
||||
font-weight: 700;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.query-tool-table tbody tr:hover {
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.query-tool-table tbody tr:nth-child(even) {
|
||||
background: #fafbfd;
|
||||
}
|
||||
|
||||
/* ---- 訊息 ---- */
|
||||
.query-tool-success {
|
||||
margin-bottom: 14px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #bbf7d0;
|
||||
background: #f0fdf4;
|
||||
color: #166534;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ---- 表格高度變體 ---- */
|
||||
.query-tool-table-wrap.short {
|
||||
max-height: 360px;
|
||||
}
|
||||
|
||||
.query-tool-table-wrap.tall {
|
||||
max-height: 460px;
|
||||
}
|
||||
|
||||
/* ---- 響應式 ---- */
|
||||
@media (max-width: 768px) {
|
||||
.dashboard,
|
||||
.query-tool-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.query-tool-tab-bar {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.query-tool-tab {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ test('all route contracts satisfy governance metadata requirements', () => {
|
||||
});
|
||||
|
||||
|
||||
test('admin shell targets are governed and rendered as external targets', () => {
|
||||
test('admin shell targets are governed with correct render modes', () => {
|
||||
const pagesContract = getRouteContract('/admin/pages');
|
||||
const perfContract = getRouteContract('/admin/performance');
|
||||
|
||||
@@ -34,7 +34,7 @@ test('admin shell targets are governed and rendered as external targets', () =>
|
||||
assert.equal(pagesContract.visibilityPolicy, 'admin_only');
|
||||
assert.equal(perfContract.visibilityPolicy, 'admin_only');
|
||||
assert.equal(pagesContract.renderMode, 'external');
|
||||
assert.equal(perfContract.renderMode, 'external');
|
||||
assert.equal(perfContract.renderMode, 'native');
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user