refactor: 優化三個錦囊 UI 設計

- 縮小錦囊按鈕至原大小的 1/4:
  * padding: 20px → 8px 12px
  * font-size: 圖標 2rem → 1rem,標題 0.95rem → 0.75rem
  * gap: 16px → 8px
  * 整體更緊湊,節省空間

- 優化 Prompt 編輯模態框 Layout:
  * 寬度增加:700px → 900px
  * 高度限制:90vh,防止超出螢幕
  * 標題和副標題改為兩欄並排顯示
  * Textarea 高度增加:12 → 16 行
  * 使用等寬字體 Consolas/Monaco,提升 prompt 編輯體驗
  * 提示區塊優化為藍色主題,更醒目
  * 按鈕添加 SVG 圖標,更直觀
  * 使用 flexbox 確保 footer 固定底部

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-06 01:25:47 +08:00
parent 8069f1b628
commit ea745ffefc
3 changed files with 317 additions and 41 deletions

View File

@@ -4112,34 +4112,55 @@ ${contextInfo}
<!-- ==================== Prompt 編輯模態框 ==================== -->
<div class="modal-overlay" id="promptEditModal">
<div class="modal" style="max-width: 700px;">
<div class="modal" style="max-width: 900px; max-height: 90vh; display: flex; flex-direction: column;">
<div class="modal-header">
<h3 id="promptModalTitle">編輯 Prompt</h3>
<button class="modal-close" onclick="closePromptEditModal()"></button>
</div>
<div class="modal-body">
<div class="modal-body" style="overflow-y: auto; flex: 1;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 16px;">
<div class="form-group">
<label>錦囊標題</label>
<input type="text" id="promptTitle" placeholder="例如:簡化版、標準版、詳細版">
</div>
<div class="form-group" style="margin-top: 16px;">
<div class="form-group">
<label>副標題(選填)</label>
<input type="text" id="promptSubtitle" placeholder="例如:僅必填欄位、常用欄位">
</div>
<div class="form-group" style="margin-top: 16px;">
<label>Prompt 內容</label>
<textarea id="promptContent" rows="12" placeholder="請輸入 AI Prompt 內容&#10;&#10;可使用的變數:&#10;{existingData} - 當前表單已填寫的資料&#10;{positionName} - 崗位名稱"></textarea>
</div>
<div style="margin-top: 12px; padding: 12px; background: #f8fafc; border-radius: 6px; font-size: 0.85rem; color: #64748b;">
<strong>提示:</strong>Prompt 會自動替換以下變數:<br>
<code>{existingData}</code> → 當前表單已填寫的資料JSON 格式)<br>
<code>{positionName}</code> → 崗位名稱(用於招聘要求頁籤)
<div class="form-group">
<label style="display: flex; justify-content: space-between; align-items: center;">
<span>Prompt 內容</span>
<span style="font-size: 0.75rem; color: #94a3b8; font-weight: normal;">支援變數:{existingData}, {positionName}</span>
</label>
<textarea id="promptContent" rows="16" style="font-family: 'Consolas', 'Monaco', monospace; font-size: 0.85rem; line-height: 1.6;" placeholder="請輸入 AI Prompt 內容...&#10;&#10;範例:&#10;你是專業人資顧問,請根據以下資料生成崗位描述:&#10;&#10;已填寫的資料:{existingData}&#10;崗位名稱:{positionName}&#10;&#10;請用繁體中文,返回 JSON 格式。"></textarea>
</div>
<div style="margin-top: 12px; padding: 10px 12px; background: #eff6ff; border-left: 3px solid #3b82f6; border-radius: 4px; font-size: 0.8rem; color: #1e40af;">
<div style="font-weight: 600; margin-bottom: 4px;">💡 可用變數</div>
<div style="line-height: 1.6;">
<code style="background: #dbeafe; padding: 2px 6px; border-radius: 3px;">{existingData}</code> 當前表單已填寫的資料JSON 格式)<br>
<code style="background: #dbeafe; padding: 2px 6px; border-radius: 3px;">{positionName}</code> 崗位名稱(用於招聘要求頁籤)
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="resetToDefaultPrompt()">重置為預設</button>
</div>
<div class="modal-footer" style="flex-shrink: 0;">
<button class="btn btn-secondary" onclick="resetToDefaultPrompt()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
<path d="M21 3v5h-5"/>
</svg>
重置為預設
</button>
<div style="flex: 1;"></div>
<button class="btn btn-cancel" onclick="closePromptEditModal()">取消</button>
<button class="btn btn-primary" onclick="savePromptEdit()">保存</button>
<button class="btn btn-primary" onclick="savePromptEdit()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>
保存
</button>
</div>
</div>
</div>

255
rename_field_ids.py Normal file
View File

@@ -0,0 +1,255 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
欄位 ID 自動重命名腳本
根據 ID重命名對照表.md 批量替換 HTML 和 JavaScript 中的欄位 ID
"""
import re
import os
from pathlib import Path
# ID 重命名對照表
ID_MAPPINGS = {
# 模組 1: 崗位基礎資料 - 基礎資料頁籤 (15個)
'businessUnit': 'pos_businessUnit',
'division': 'pos_division',
'department': 'pos_department',
'section': 'pos_section',
'positionCode': 'pos_code',
'effectiveDate': 'pos_effectiveDate',
'positionName': 'pos_name',
'positionLevel': 'pos_level',
'positionCategory': 'pos_category',
'positionCategoryName': 'pos_categoryName',
'positionNature': 'pos_type',
'positionNatureName': 'pos_typeName',
'headcount': 'pos_headcount',
'positionDesc': 'pos_desc',
'positionRemark': 'pos_remark',
# 模組 2: 崗位基礎資料 - 招聘要求頁籤 (18個)
'minEducation': 'rec_eduLevel',
'requiredGender': 'rec_gender',
'salaryRange': 'rec_salaryRange',
'workExperience': 'rec_expYears',
'minAge': 'rec_minAge',
'maxAge': 'rec_maxAge',
'jobType': 'rec_jobType',
'recruitPosition': 'rec_position',
'jobTitle': 'rec_jobTitle',
'superiorPosition': 'rec_superiorCode',
'jobDesc': 'rec_jobDesc',
'positionReq': 'rec_positionReq',
'titleReq': 'rec_certReq',
'majorReq': 'rec_majorReq',
'skillReq': 'rec_skillReq',
'langReq': 'rec_langReq',
'otherReq': 'rec_otherReq',
'recruitRemark': 'rec_remark',
# 模組 3: 職務基礎資料 (12個)
'jobCategoryCode': 'job_category',
'jobCategoryName': 'job_categoryName',
'jobCode': 'job_code',
'jobName': 'job_name',
'jobNameEn': 'job_nameEn',
'jobEffectiveDate': 'job_effectiveDate',
'jobLevel': 'job_level',
'jobHeadcount': 'job_headcount',
'jobSortOrder': 'job_sortOrder',
'hasAttendanceBonus': 'job_hasAttBonus',
'hasHousingAllowance': 'job_hasHouseAllow',
'jobRemark': 'job_remark',
# 模組 4: 部門職責 (19個 - 包含合併重複欄位)
'deptFunctionCode': 'df_code',
'deptFunctionName': 'df_name',
'deptFunctionBU': 'df_businessUnit',
'deptFunc_businessUnit': 'df_businessUnit', # 合併
'deptFunc_division': 'df_division',
'deptFunc_department': 'df_department',
'deptFunc_section': 'df_section',
'deptFunc_positionTitle': 'df_posTitle',
'deptFunc_positionLevel': 'df_posLevel',
'deptManager': 'df_managerTitle',
'deptFunctionEffectiveDate': 'df_effectiveDate',
'deptHeadcount': 'df_headcountLimit',
'deptStatus': 'df_status',
'deptMission': 'df_mission',
'deptVision': 'df_vision',
'deptCoreFunctions': 'df_coreFunc',
'deptKPIs': 'df_kpis',
'deptCollaboration': 'df_collab',
'deptFunctionRemark': 'df_remark',
# 模組 5: 崗位描述 (8個需要變更的)
'jd_positionCode': 'jd_posCode',
'jd_positionName': 'jd_posName',
'jd_positionLevel': 'jd_posLevel',
'jd_positionEffectiveDate': 'jd_posEffDate',
'jd_directSupervisor': 'jd_supervisor',
'jd_positionGradeJob': 'jd_gradeJob',
'jd_workLocation': 'jd_location',
'jd_empAttribute': 'jd_empAttr',
'jd_deptFunctionCode': 'jd_dfCode',
'jd_positionPurpose': 'jd_purpose',
'jd_mainResponsibilities': 'jd_mainResp',
'jd_education': 'jd_eduLevel',
'jd_basicSkills': 'jd_basicSkills',
'jd_professionalKnowledge': 'jd_proKnowledge',
'jd_workExperienceReq': 'jd_expReq',
'jd_otherRequirements': 'jd_otherReq',
}
# 需要特殊處理的函數名映射onchange事件等
FUNCTION_MAPPINGS = {
'updateCategoryName': 'updateCategoryName', # 保持不變,但內部需要更新
'updateNatureName': 'updateTypeName', # positionNature -> pos_type
'updateJobCategoryName': 'updateJobCategoryName', # 保持不變
}
def replace_in_file(file_path, dry_run=False):
"""
在文件中替換所有匹配的 ID
Args:
file_path: 文件路徑
dry_run: 如果為 True只輸出變更不實際修改
Returns:
(總替換次數, 變更詳情列表)
"""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
original_content = content
changes = []
total_replacements = 0
# 排序按舊ID長度降序避免短ID誤替換長ID
sorted_mappings = sorted(ID_MAPPINGS.items(), key=lambda x: len(x[0]), reverse=True)
for old_id, new_id in sorted_mappings:
if old_id == new_id:
continue
# 匹配模式:
# 1. HTML id="oldId"
# 2. JavaScript getElementById('oldId') 或 getElementById("oldId")
# 3. HTML for="oldId"
# 4. HTML name="oldId"
# 5. 對象屬性 {oldId: ...} 或 data.oldId
patterns = [
# HTML id 屬性
(rf'\bid=["\']({re.escape(old_id)})["\']', rf'id="\1"', lambda m: f'id="{new_id}"'),
# HTML for 屬性
(rf'\bfor=["\']({re.escape(old_id)})["\']', rf'for="\1"', lambda m: f'for="{new_id}"'),
# HTML name 屬性
(rf'\bname=["\']({re.escape(old_id)})["\']', rf'name="\1"', lambda m: f'name="{new_id}"'),
# getElementById
(rf'getElementById\(["\']({re.escape(old_id)})["\']', rf'getElementById("\1")', lambda m: f'getElementById("{new_id}")'),
# 對象屬性訪問 .oldId (謹慎使用,確保前面是合理的對象)
(rf'\.({re.escape(old_id)})\b', rf'.\1', lambda m: f'.{new_id}'),
# 對象字面量屬性 oldId: 或 "oldId":
(rf'\b({re.escape(old_id)}):', rf'\1:', lambda m: f'{new_id}:'),
]
for pattern, _, replacement_func in patterns:
matches = list(re.finditer(pattern, content))
if matches:
# 從後往前替換,避免索引偏移
for match in reversed(matches):
start, end = match.span()
old_text = content[start:end]
new_text = replacement_func(match)
if old_text != new_text:
content = content[:start] + new_text + content[end:]
changes.append({
'old': old_text,
'new': new_text,
'line': content[:start].count('\n') + 1
})
total_replacements += 1
# 如果有變更且非 dry run寫回文件
if content != original_content and not dry_run:
with open(file_path, 'w', encoding='utf-8', newline='') as f:
f.write(content)
return total_replacements, changes
def main():
"""主函數"""
base_dir = Path(__file__).parent
# 需要處理的文件列表
files_to_process = [
base_dir / 'index.html',
base_dir / 'js' / 'ui.js',
base_dir / 'js' / 'ai-bags.js',
base_dir / 'js' / 'main.js',
]
print("=" * 80)
print("欄位 ID 重命名工具")
print("=" * 80)
print(f"\n📋 總計需要重命名:{len(ID_MAPPINGS)} 個 ID")
print(f"📂 需要處理:{len(files_to_process)} 個文件\n")
# 先 dry run 顯示變更
print("🔍 掃描變更Dry Run...")
print("-" * 80)
total_changes = 0
for file_path in files_to_process:
if not file_path.exists():
print(f"⚠️ 文件不存在:{file_path.name}")
continue
count, changes = replace_in_file(file_path, dry_run=True)
total_changes += count
if count > 0:
print(f"\n📄 {file_path.name}: {count} 處變更")
# 顯示前 5 個變更示例
for i, change in enumerate(changes[:5]):
print(f" L{change['line']}: {change['old']}{change['new']}")
if len(changes) > 5:
print(f" ... 還有 {len(changes) - 5} 處變更")
print("\n" + "=" * 80)
print(f"📊 總計:{total_changes} 處需要替換")
print("=" * 80)
# 詢問是否執行
response = input("\n是否執行替換?(y/n): ").strip().lower()
if response == 'y':
print("\n🚀 開始執行替換...")
print("-" * 80)
for file_path in files_to_process:
if not file_path.exists():
continue
count, _ = replace_in_file(file_path, dry_run=False)
if count > 0:
print(f"{file_path.name}: 已替換 {count}")
print("\n✨ 替換完成!")
print("\n⚠️ 請執行以下步驟:")
print(" 1. 測試所有表單功能")
print(" 2. 檢查瀏覽器控制台是否有錯誤")
print(" 3. 使用 git diff 檢查變更")
print(" 4. 提交變更git add -A && git commit -m 'refactor: 標準化欄位 ID 命名'")
else:
print("\n❌ 已取消執行")
if __name__ == '__main__':
main()

View File

@@ -269,58 +269,58 @@ select {
.ai-bags-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 24px;
gap: 8px;
margin-bottom: 16px;
}
.ai-bag {
position: relative;
padding: 20px;
padding: 8px 12px;
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
border-radius: var(--radius);
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(155, 89, 182, 0.3);
box-shadow: 0 2px 8px rgba(155, 89, 182, 0.3);
text-align: center;
}
.ai-bag:hover {
transform: translateY(-4px);
box-shadow: 0 6px 20px rgba(155, 89, 182, 0.5);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(155, 89, 182, 0.5);
}
.ai-bag:active {
transform: translateY(-2px);
transform: translateY(-1px);
}
.ai-bag .bag-icon {
font-size: 2rem;
margin-bottom: 8px;
font-size: 1rem;
margin-bottom: 2px;
}
.ai-bag .bag-title {
color: white;
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 4px;
font-size: 0.75rem;
margin-bottom: 2px;
}
.ai-bag .bag-subtitle {
color: rgba(255, 255, 255, 0.8);
font-size: 0.75rem;
margin-top: 4px;
font-size: 0.65rem;
margin-top: 2px;
}
.ai-bag .bag-edit-btn {
position: absolute;
top: 8px;
right: 8px;
top: 4px;
right: 4px;
background: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 4px;
padding: 4px 8px;
border-radius: 3px;
padding: 2px 4px;
cursor: pointer;
font-size: 0.9rem;
font-size: 0.7rem;
transition: all 0.2s ease;
line-height: 1;
}
@@ -336,13 +336,13 @@ select {
}
.ai-bag .spinner {
width: 20px;
height: 20px;
width: 14px;
height: 14px;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin: 8px auto;
margin: 4px auto;
}
/* ==================== Old AI Button (Deprecated) ==================== */