feat: 實現版本號自動化更新與修復側邊欄 RWD 問題
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Has been skipped
Koori-ERP-Deploy-System / deploy-production (push) Successful in 50s

This commit is contained in:
2026-02-13 14:46:26 +08:00
parent 77a7d31dc1
commit 2e9ff6c832
6 changed files with 123 additions and 130 deletions

View File

@@ -92,6 +92,10 @@ jobs:
key: ${{ secrets.DEMO_SSH_KEY }}
script: |
docker exec -u 1000:1000 -w /var/www/html star-erp-laravel sh -c "
# 0. 更新版本號 (使用 Git Hash)
VERSION=\"v1.0-\$(git rev-parse --short HEAD)\"
sed -i \"s/^APP_VERSION=.*/APP_VERSION=\$VERSION/\" .env || echo \"APP_VERSION=\$VERSION\" >> .env
# 1. 後端依賴 (Demo 環境建議加上 --no-interaction 避免卡住)
composer install --no-dev --optimize-autoloader --no-interaction &&
@@ -191,6 +195,10 @@ jobs:
echo "容器狀態:" && docker ps --filter "name=star-erp-laravel"
docker exec -u 1000:1000 -w /var/www/html star-erp-laravel sh -c "
# 0. 更新版本號
VERSION=\"v1.0-\$(git rev-parse --short HEAD)\"
sed -i \"s/^APP_VERSION=.*/APP_VERSION=\$VERSION/\" .env || echo \"APP_VERSION=\$VERSION\" >> .env
composer install --no-dev --optimize-autoloader &&
npm install &&
npm run build

View File

@@ -46,6 +46,7 @@ class HandleInertiaRequests extends Middleware
return [
...parent::share($request),
'appName' => $appName,
'app_version' => config('app.version'),
'auth' => [
'user' => $user ? [
'id' => $user->id,

View File

@@ -15,6 +15,8 @@ return [
'name' => env('APP_NAME', 'Laravel'),
'version' => env('APP_VERSION', '1.0.0'),
/*
|--------------------------------------------------------------------------
| Application Environment

View File

@@ -386,13 +386,15 @@ export default function AuthenticatedLayout({
});
};
const renderMenuItem = (item: MenuItem, level: number = 0) => {
const renderMenuItem = (item: MenuItem, level: number = 0, forceExpand: boolean = false) => {
const hasChildren = item.children && item.children.length > 0;
const isExpanded = expandedItems.includes(item.id);
const isActive = item.route
? (item.route === '/' ? url === '/' : url.startsWith(item.route))
: false;
const effectivelyCollapsed = isCollapsed && !forceExpand;
return (
<div key={item.id} className="mb-1">
{hasChildren ? (
@@ -401,21 +403,21 @@ export default function AuthenticatedLayout({
className={cn(
"w-full flex items-center transition-all rounded-lg group",
level === 0 ? "px-3 py-2.5" : "px-3 py-2 pl-10",
level === 0 && !isCollapsed && "hover:bg-slate-100",
isCollapsed && level === 0 && "justify-center px-0 h-10 w-10 mx-auto hover:bg-slate-100"
level === 0 && !effectivelyCollapsed && "hover:bg-slate-100",
effectivelyCollapsed && level === 0 && "justify-center px-0 h-10 w-10 mx-auto hover:bg-slate-100"
)}
title={isCollapsed ? item.label : ""}
title={effectivelyCollapsed ? item.label : ""}
>
{level === 0 && (
<span className={cn(
"flex-shrink-0 transition-all",
isCollapsed ? "mr-0" : "mr-3 text-slate-500 group-hover:text-slate-900"
effectivelyCollapsed ? "mr-0" : "mr-3 text-slate-500 group-hover:text-slate-900"
)}>
{item.icon}
</span>
)}
{!isCollapsed && (
{!effectivelyCollapsed && (
<>
<span className="flex-1 text-left text-base font-medium text-slate-700 group-hover:text-slate-900 truncate">
{item.label}
@@ -438,22 +440,22 @@ export default function AuthenticatedLayout({
className={cn(
"w-full flex items-center transition-all rounded-lg group",
level === 0 ? "px-3 py-2.5" : "px-3 py-2",
level > 0 && !isCollapsed && "pl-11",
level > 0 && !effectivelyCollapsed && "pl-11",
isActive ? "bg-primary-lightest text-primary-main" : "text-slate-600 hover:bg-slate-100 hover:text-slate-900",
isCollapsed && level === 0 && "justify-center px-0 h-10 w-10 mx-auto"
effectivelyCollapsed && level === 0 && "justify-center px-0 h-10 w-10 mx-auto"
)}
title={isCollapsed ? item.label : ""}
title={effectivelyCollapsed ? item.label : ""}
>
{item.icon && (
<span className={cn(
"flex-shrink-0 transition-all",
isCollapsed ? "mr-0" : "mr-3",
effectivelyCollapsed ? "mr-0" : "mr-3",
isActive ? "text-primary-main" : "text-slate-500 group-hover:text-slate-900"
)}>
{item.icon}
</span>
)}
{!isCollapsed && (
{!effectivelyCollapsed && (
<span className="text-base font-medium truncate">
{item.label}
</span>
@@ -461,9 +463,9 @@ export default function AuthenticatedLayout({
</Link>
)}
{hasChildren && isExpanded && !isCollapsed && (
{hasChildren && isExpanded && !effectivelyCollapsed && (
<div className="mt-1 space-y-1">
{item.children?.map((child) => renderMenuItem(child, level + 1))}
{item.children?.map((child) => renderMenuItem(child, level + 1, forceExpand))}
</div>
)}
</div>
@@ -573,7 +575,7 @@ export default function AuthenticatedLayout({
</div>
<div className="p-4 border-t border-slate-100 flex items-center justify-between">
{!isCollapsed && <p className="text-[10px] font-medium text-slate-400 uppercase tracking-wider px-2">Version 1.0.0</p>}
{!isCollapsed && <p className="text-[10px] font-medium text-slate-400 uppercase tracking-wider px-2">Version {props.app_version || '1.0.0'}</p>}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className={cn(
@@ -613,7 +615,7 @@ export default function AuthenticatedLayout({
</div>
<div className="flex-1 overflow-y-auto p-4" scroll-region="true">
<nav className="space-y-1">
{menuItems.map((item) => renderMenuItem(item))}
{menuItems.map((item) => renderMenuItem(item, 0, true))}
</nav>
</div>
</aside>

View File

@@ -4,7 +4,7 @@ import { Head, router } from '@inertiajs/react';
import { PageProps } from '@/types/global';
import Pagination from '@/Components/shared/Pagination';
import { SearchableSelect } from "@/Components/ui/searchable-select";
import { FileText, Search, RotateCcw, Calendar, ChevronDown, ChevronUp } from 'lucide-react';
import { FileText, Search, RotateCcw, Calendar } from 'lucide-react';
import LogTable, { Activity } from '@/Components/ActivityLog/LogTable';
import ActivityDetailDialog from '@/Components/ActivityLog/ActivityDetailDialog';
import { Button } from '@/Components/ui/button';
@@ -57,10 +57,7 @@ export default function ActivityLogIndex({ activities, filters, subject_types, u
const [causer, setCauser] = useState(filters.causer_id || 'all');
const [dateRangeType, setDateRangeType] = useState('custom');
// Advanced Filter Toggle
const [showAdvancedFilter, setShowAdvancedFilter] = useState(
!!(filters.date_start || filters.date_end)
);
const handleDateRangeChange = (type: string) => {
setDateRangeType(type);
@@ -161,75 +158,12 @@ export default function ActivityLogIndex({ activities, filters, subject_types, u
</div>
{/* 篩選區塊 */}
<div className="bg-white p-5 rounded-lg shadow-sm border border-grey-4 mb-6">
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 mb-4">
{/* 關鍵字搜尋 */}
<div className="md:col-span-4 space-y-1">
<Label className="text-xs font-medium text-grey-1"></Label>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="搜尋描述、內容..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 h-9 block"
onKeyDown={(e) => e.key === 'Enter' && handleFilter()}
/>
</div>
</div>
{/* 事件類型 */}
<div className="md:col-span-2 space-y-1">
<Label className="text-xs font-medium text-grey-1"></Label>
<Select value={event} onValueChange={setEvent}>
<SelectTrigger className="h-9">
<SelectValue placeholder="所有事件" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="created"> (Created)</SelectItem>
<SelectItem value="updated"> (Updated)</SelectItem>
<SelectItem value="deleted"> (Deleted)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 操作對象 */}
<div className="md:col-span-3 space-y-1">
<Label className="text-xs font-medium text-grey-1"></Label>
<SearchableSelect
value={subjectType}
onValueChange={setSubjectType}
options={[
{ label: "所有對象", value: "all" },
...subject_types
]}
placeholder="選擇對象"
className="w-full h-9"
/>
</div>
{/* 操作人員 */}
<div className="md:col-span-3 space-y-1">
<Label className="text-xs font-medium text-grey-1"></Label>
<SearchableSelect
value={causer}
onValueChange={setCauser}
options={[
{ label: "所有人員", value: "all" },
...users
]}
placeholder="選擇人員"
className="w-full h-9"
/>
</div>
</div>
{/* Row 2: Date Filters (Collapsible) */}
{showAdvancedFilter && (
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 items-end animate-in fade-in slide-in-from-top-2 duration-200">
<div className="md:col-span-6 space-y-2">
<Label className="text-xs font-medium text-grey-1"></Label>
<div className="bg-white rounded-xl shadow-sm border border-grey-4 p-5 mb-6">
<div className="space-y-4">
{/* Top Config: Date Range & Quick Buttons */}
<div className="flex flex-col lg:flex-row gap-4 lg:items-end">
<div className="flex-none space-y-2">
<Label className="text-xs font-medium text-grey-2"></Label>
<div className="flex flex-wrap gap-2">
{[
{ label: "今日", value: "today" },
@@ -254,10 +188,11 @@ export default function ActivityLogIndex({ activities, filters, subject_types, u
</div>
</div>
<div className="md:col-span-6">
<div className="grid grid-cols-2 gap-4 items-end">
{/* Date Inputs */}
<div className="w-full lg:flex-1">
<div className="grid grid-cols-2 gap-2">
<div className="space-y-1">
<Label className="text-xs font-medium text-grey-2"></Label>
<Label className="text-xs text-grey-2 font-medium"></Label>
<div className="relative">
<Calendar className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
<Input
@@ -267,13 +202,12 @@ export default function ActivityLogIndex({ activities, filters, subject_types, u
setDateStart(e.target.value);
setDateRangeType('custom');
}}
// block w-full to ensure it fills space
className="pl-9 block w-full h-9 bg-white"
/>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs font-medium text-grey-2"></Label>
<Label className="text-xs text-grey-2 font-medium"></Label>
<div className="relative">
<Calendar className="absolute left-2.5 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400 pointer-events-none" />
<Input
@@ -290,43 +224,88 @@ export default function ActivityLogIndex({ activities, filters, subject_types, u
</div>
</div>
</div>
)}
{/* Action Bar */}
<div className="flex items-center justify-end border-t border-grey-4 pt-5 gap-3 mt-4">
<Button
variant="ghost"
size="sm"
onClick={() => setShowAdvancedFilter(!showAdvancedFilter)}
className="mr-auto text-gray-500 hover:text-gray-900 h-9"
>
{showAdvancedFilter ? (
<>
<ChevronUp className="h-4 w-4 mr-1" />
</>
) : (
<>
<ChevronDown className="h-4 w-4 mr-1" />
{(dateStart || dateEnd) && (
<span className="ml-2 w-2 h-2 rounded-full bg-primary-main" />
)}
</>
)}
</Button>
<Button
variant="outline"
onClick={handleReset}
className="flex items-center gap-2 button-outlined-primary h-9"
>
<RotateCcw className="h-4 w-4" />
</Button>
<Button onClick={handleFilter} className="button-filled-primary h-9 px-6 gap-2">
<Search className="h-4 w-4" />
</Button>
{/* Detailed Filters row */}
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 items-end">
{/* 事件類型 */}
<div className="md:col-span-2 space-y-1">
<Label className="text-xs font-medium text-grey-2"></Label>
<Select value={event} onValueChange={setEvent}>
<SelectTrigger className="h-9 bg-white">
<SelectValue placeholder="所有事件" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="created"> (Created)</SelectItem>
<SelectItem value="updated"> (Updated)</SelectItem>
<SelectItem value="deleted"> (Deleted)</SelectItem>
</SelectContent>
</Select>
</div>
{/* 操作對象 */}
<div className="md:col-span-2 space-y-1">
<Label className="text-xs font-medium text-grey-2"></Label>
<SearchableSelect
value={subjectType}
onValueChange={setSubjectType}
options={[
{ label: "所有對象", value: "all" },
...subject_types
]}
placeholder="選擇對象"
className="w-full h-9 bg-white"
/>
</div>
{/* 操作人員 */}
<div className="md:col-span-2 space-y-1">
<Label className="text-xs font-medium text-grey-2"></Label>
<SearchableSelect
value={causer}
onValueChange={setCauser}
options={[
{ label: "所有人員", value: "all" },
...users
]}
placeholder="選擇人員"
className="w-full h-9 bg-white"
/>
</div>
{/* 關鍵字搜尋 */}
<div className="md:col-span-3 space-y-1">
<Label className="text-xs font-medium text-grey-2"></Label>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
placeholder="搜尋內容..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-10 h-9 block bg-white"
onKeyDown={(e) => e.key === 'Enter' && handleFilter()}
/>
</div>
</div>
{/* Action Buttons Integrated */}
<div className="md:col-span-3 flex items-center gap-2">
<Button
variant="outline"
onClick={handleReset}
className="flex-1 items-center gap-2 button-outlined-primary h-9"
>
<RotateCcw className="h-4 w-4" />
</Button>
<Button
onClick={handleFilter}
className="flex-1 button-filled-primary h-9 gap-2 shadow-sm"
>
<Search className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</div>

View File

@@ -28,6 +28,7 @@ export interface PageProps {
error?: string;
};
branding?: Branding | null;
app_version?: string;
[key: string]: unknown;
}