Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -12,10 +12,8 @@ DATA_FILE = "data.csv"
|
|
| 12 |
def load_fixed_database():
|
| 13 |
if os.path.exists(DATA_FILE):
|
| 14 |
try:
|
| 15 |
-
# 讀取固定的健保資料庫
|
| 16 |
df = pd.read_csv(DATA_FILE)
|
| 17 |
df.columns = df.columns.str.strip()
|
| 18 |
-
# 清理欄位前後空白
|
| 19 |
for col in ['成分', '劑型', '劑量', '廠商', '年度']:
|
| 20 |
if col in df.columns:
|
| 21 |
df[col] = df[col].astype(str).str.strip()
|
|
@@ -24,19 +22,17 @@ def load_fixed_database():
|
|
| 24 |
print(f"讀取本地資料庫失敗: {e}")
|
| 25 |
return pd.DataFrame()
|
| 26 |
else:
|
| 27 |
-
print(f"錯誤:找不到固定資料庫檔案 {DATA_FILE},請確保檔案已上傳至同目錄。")
|
| 28 |
return pd.DataFrame()
|
| 29 |
|
| 30 |
-
# 程式啟動時載入全域資料
|
| 31 |
df_global = load_fixed_database()
|
| 32 |
if not df_global.empty:
|
| 33 |
ALL_COMPONENTS = sorted(df_global['成分'].dropna().unique())
|
| 34 |
-
INIT_MESSAGE = f"✅ 成功
|
| 35 |
else:
|
| 36 |
ALL_COMPONENTS = []
|
| 37 |
-
INIT_MESSAGE = "❌
|
| 38 |
|
| 39 |
-
# --- 2. 輔助函式
|
| 40 |
def parse_dosage_to_numeric(dose_str):
|
| 41 |
if not isinstance(dose_str, str):
|
| 42 |
return 0.0
|
|
@@ -48,64 +44,75 @@ def parse_dosage_to_numeric(dose_str):
|
|
| 48 |
return val
|
| 49 |
return 0.0
|
| 50 |
|
| 51 |
-
# --- 3. 動態
|
| 52 |
def update_component_choices(search_text):
|
| 53 |
if not ALL_COMPONENTS:
|
| 54 |
-
return gr.update(choices=[])
|
| 55 |
-
|
| 56 |
-
# 修正:如果使用者沒有輸入任何關鍵字,保持空的選項不顯示,避免畫面太混亂
|
| 57 |
if not search_text or search_text.strip() == "":
|
| 58 |
-
return gr.update(choices=[])
|
| 59 |
-
|
| 60 |
-
# 修正原本放在 return 後面的縮排錯誤
|
| 61 |
search_text = search_text.strip().lower()
|
| 62 |
filtered = [c for c in ALL_COMPONENTS if search_text in c.lower()]
|
| 63 |
-
return gr.update(choices=filtered)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
if df_global.empty:
|
| 68 |
-
return None, "❌ 系統未成功載入 data.csv 資料庫
|
| 69 |
-
if not selected_components:
|
| 70 |
-
return None, "❌ 請
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
-
# 篩選選定的成分資料
|
| 73 |
-
df_filtered = df_global[df_global['成分'].isin(selected_components)].copy()
|
| 74 |
if df_filtered.empty:
|
| 75 |
-
return None, "❌ 找不到
|
| 76 |
-
|
| 77 |
-
# 自動判斷數量欄位名稱
|
| 78 |
qty_col = '數量(顆)' if '數量(顆)' in df_filtered.columns else [col for col in df_filtered.columns if '數量' in col][0]
|
| 79 |
-
|
| 80 |
-
# 【新增:強制將數量欄位轉換為數值型態,並移除非數字字元】
|
| 81 |
df_filtered[qty_col] = df_filtered[qty_col].astype(str).str.replace(',', '').str.strip()
|
| 82 |
df_filtered[qty_col] = pd.to_numeric(df_filtered[qty_col], errors='coerce').fillna(0)
|
| 83 |
-
|
| 84 |
-
# 進行樞紐分析 (Pivot Table)
|
| 85 |
pivot_df = df_filtered.groupby(['成分', '劑型', '劑量', '廠商', '年度'])[qty_col].sum().unstack(fill_value=0)
|
| 86 |
-
|
| 87 |
-
# 確保三年欄位皆完整
|
| 88 |
for year_col in ['2022年', '2023年', '2024年']:
|
| 89 |
if year_col not in pivot_df.columns:
|
| 90 |
pivot_df[year_col] = 0
|
| 91 |
|
| 92 |
pivot_df = pivot_df.reindex(columns=['2022年', '2023年', '2024年']).reset_index()
|
| 93 |
-
|
| 94 |
-
# 高階排序:劑量由小到大 -> 2024年數量由大到小
|
| 95 |
pivot_df['dose_numeric'] = pivot_df['劑量'].apply(parse_dosage_to_numeric)
|
| 96 |
-
pivot_df = pivot_df.sort_values(
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
|
|
|
| 99 |
wb = Workbook()
|
| 100 |
ws = wb.active
|
| 101 |
ws.title = "廠商排名報表"
|
| 102 |
ws.views.sheetView[0].showGridLines = True
|
| 103 |
-
|
| 104 |
-
# 樣式與底色定義
|
| 105 |
font_family = "微軟正黑體"
|
| 106 |
-
header_fill = PatternFill(start_color="1F497D", end_color="1F497D", fill_type="solid")
|
| 107 |
-
subtotal_fill = PatternFill(start_color="DCE6F1", end_color="DCE6F1", fill_type="solid")
|
| 108 |
-
total_fill = PatternFill(start_color="B8CCE4", end_color="B8CCE4", fill_type="solid")
|
| 109 |
|
| 110 |
header_font = Font(name=font_family, size=12, bold=True, color="FFFFFF")
|
| 111 |
data_font = Font(name=font_family, size=12)
|
|
@@ -116,197 +123,438 @@ def generate_standard_excel(selected_components):
|
|
| 116 |
right_align = Alignment(horizontal="right", vertical="center")
|
| 117 |
|
| 118 |
thin_side = Side(border_style="thin", color="D9D9D9")
|
|
|
|
| 119 |
cell_border = Border(left=thin_side, right=thin_side, top=thin_side, bottom=thin_side)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
ws.row_dimensions[1].height = 30
|
| 125 |
-
|
|
|
|
|
|
|
| 126 |
cell = ws.cell(row=1, column=col_idx)
|
| 127 |
cell.font = header_font
|
| 128 |
cell.fill = header_fill
|
| 129 |
cell.alignment = center_align
|
| 130 |
cell.border = cell_border
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
grand_total_2022 = 0
|
| 135 |
grand_total_2023 = 0
|
| 136 |
grand_total_2024 = 0
|
| 137 |
-
|
| 138 |
current_row = 2
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
|
|
|
| 147 |
|
| 148 |
-
|
|
|
|
|
|
|
| 149 |
|
| 150 |
for _, row in dose_group.iterrows():
|
| 151 |
-
qty_2022 = row['2022年']
|
| 152 |
-
qty_2023 = row['2023年']
|
| 153 |
-
qty_2024 = row['2024年']
|
| 154 |
ratio = (qty_2024 / dose_2024_sum) if dose_2024_sum > 0 else 0.0
|
| 155 |
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
d_val = row['劑量'] if is_first_row else ""
|
| 160 |
|
| 161 |
ws.append([c_val, f_val, d_val, row['廠商'], qty_2022, qty_2023, qty_2024, ratio])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
-
# 設定一般資料列格式
|
| 164 |
for col_idx in range(1, 9):
|
| 165 |
cell = ws.cell(row=current_row, column=col_idx)
|
| 166 |
cell.font = data_font
|
| 167 |
cell.border = cell_border
|
| 168 |
-
if col_idx in [1, 2, 3]:
|
| 169 |
-
|
| 170 |
-
elif col_idx ==
|
| 171 |
-
|
| 172 |
-
elif col_idx in [5, 6, 7]:
|
| 173 |
-
cell.alignment = right_align
|
| 174 |
-
cell.number_format = '#,##0' # 千分位、至個位數
|
| 175 |
-
elif col_idx == 8:
|
| 176 |
-
cell.alignment = right_align
|
| 177 |
-
cell.number_format = '0.0%' # 占比至小數點第一位
|
| 178 |
|
| 179 |
-
dose_subtotal_2022 += qty_2022
|
| 180 |
-
dose_subtotal_2023 += qty_2023
|
| 181 |
-
dose_subtotal_2024 += qty_2024
|
| 182 |
current_row += 1
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
-
#
|
| 186 |
ws.append(["", "", f"{dose} 合計", "", dose_subtotal_2022, dose_subtotal_2023, dose_subtotal_2024, 1.0])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
for col_idx in range(1, 9):
|
| 188 |
cell = ws.cell(row=current_row, column=col_idx)
|
| 189 |
cell.font = bold_font
|
| 190 |
cell.fill = subtotal_fill
|
| 191 |
-
cell.border = cell_border
|
| 192 |
-
if col_idx == 3:
|
| 193 |
-
|
| 194 |
-
elif col_idx
|
| 195 |
-
cell.alignment = right_align
|
| 196 |
-
cell.number_format = '#,##0'
|
| 197 |
-
elif col_idx == 8:
|
| 198 |
-
cell.alignment = right_align
|
| 199 |
-
cell.number_format = '0.0%'
|
| 200 |
|
| 201 |
grand_total_2022 += dose_subtotal_2022
|
| 202 |
grand_total_2023 += dose_subtotal_2023
|
| 203 |
grand_total_2024 += dose_subtotal_2024
|
| 204 |
current_row += 1
|
| 205 |
-
|
| 206 |
-
#
|
| 207 |
-
ws.append(["", "", "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
for col_idx in range(1, 9):
|
| 209 |
cell = ws.cell(row=current_row, column=col_idx)
|
| 210 |
cell.font = bold_font
|
| 211 |
cell.fill = total_fill
|
| 212 |
cell.border = cell_border
|
| 213 |
-
if col_idx ==
|
| 214 |
-
|
| 215 |
-
elif col_idx
|
| 216 |
-
|
| 217 |
-
cell.number_format = '#,##0'
|
| 218 |
-
elif col_idx == 8:
|
| 219 |
-
cell.alignment = right_align
|
| 220 |
-
cell.number_format = '0.0%'
|
| 221 |
-
|
| 222 |
-
# 自動調整欄寬
|
| 223 |
for col in ws.columns:
|
| 224 |
-
max_len = 0
|
| 225 |
col_letter = get_column_letter(col[0].column)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
for cell in col:
|
| 227 |
val_str = str(cell.value or '')
|
| 228 |
lines = val_str.split('\n')
|
| 229 |
for line in lines:
|
| 230 |
line_len = sum(2 if '\u4e00' <= char <= '\u9fff' else 1 for char in line)
|
| 231 |
-
if line_len > max_len:
|
| 232 |
-
max_len = line_len
|
| 233 |
ws.column_dimensions[col_letter].width = max(max_len + 4, 12)
|
| 234 |
-
|
| 235 |
-
# 專業列印與邊界設定 (公分轉英吋)
|
| 236 |
ws.sheet_properties.pageSetUpPr.fitToPage = True
|
| 237 |
-
ws.page_setup.fitToWidth = 1
|
| 238 |
-
ws.page_setup.fitToHeight = 0
|
| 239 |
-
ws.page_margins.top = 2.7 / 2.54
|
| 240 |
-
ws.page_margins.
|
| 241 |
-
ws.page_margins.
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
ws.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
ws.oddFooter.right.text = "&12https://data.gov.tw/dataset/22131"
|
| 258 |
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
wb.save(output_filename)
|
| 265 |
|
| 266 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
gr.Markdown(f"**系統狀態:** {INIT_MESSAGE}")
|
| 272 |
|
|
|
|
|
|
|
| 273 |
with gr.Row():
|
|
|
|
| 274 |
with gr.Column(scale=1):
|
| 275 |
-
gr.Markdown("### 🔍
|
| 276 |
-
search_input = gr.Textbox(
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
)
|
| 281 |
-
|
| 282 |
-
# 【主要修改處】:將原本的 choices=ALL_COMPONENTS 改為 choices=[]
|
| 283 |
-
# 這樣一打開網頁時,第二步就會是乾淨的,只有輸入關鍵字後才會顯示對應成分。
|
| 284 |
-
component_choices = gr.CheckboxGroup(
|
| 285 |
-
label="📋 第二步:勾選欲納入報表的成分品項 (可多選)",
|
| 286 |
-
choices=[],
|
| 287 |
-
value=[]
|
| 288 |
-
)
|
| 289 |
-
|
| 290 |
-
submit_btn = gr.Button("🚀 產生標準化 Excel 報表", variant="primary")
|
| 291 |
|
|
|
|
| 292 |
with gr.Column(scale=1):
|
| 293 |
-
gr.Markdown("### 📥
|
| 294 |
status_output = gr.Textbox(label="系統處理結果", interactive=False)
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
-
|
| 298 |
-
search_input.change(
|
| 299 |
-
fn=update_component_choices,
|
| 300 |
-
inputs=search_input,
|
| 301 |
-
outputs=component_choices
|
| 302 |
-
)
|
| 303 |
|
| 304 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
submit_btn.click(
|
| 306 |
-
fn=
|
| 307 |
-
inputs=component_choices,
|
| 308 |
-
outputs=[
|
| 309 |
)
|
|
|
|
|
|
|
|
|
|
| 310 |
|
| 311 |
if __name__ == "__main__":
|
| 312 |
demo.launch()
|
|
|
|
| 12 |
def load_fixed_database():
|
| 13 |
if os.path.exists(DATA_FILE):
|
| 14 |
try:
|
|
|
|
| 15 |
df = pd.read_csv(DATA_FILE)
|
| 16 |
df.columns = df.columns.str.strip()
|
|
|
|
| 17 |
for col in ['成分', '劑型', '劑量', '廠商', '年度']:
|
| 18 |
if col in df.columns:
|
| 19 |
df[col] = df[col].astype(str).str.strip()
|
|
|
|
| 22 |
print(f"讀取本地資料庫失敗: {e}")
|
| 23 |
return pd.DataFrame()
|
| 24 |
else:
|
|
|
|
| 25 |
return pd.DataFrame()
|
| 26 |
|
|
|
|
| 27 |
df_global = load_fixed_database()
|
| 28 |
if not df_global.empty:
|
| 29 |
ALL_COMPONENTS = sorted(df_global['成分'].dropna().unique())
|
| 30 |
+
INIT_MESSAGE = f"✅ 成功載入資料庫!共 {len(df_global)} 筆數據,包含 {len(ALL_COMPONENTS)} 種成分。"
|
| 31 |
else:
|
| 32 |
ALL_COMPONENTS = []
|
| 33 |
+
INIT_MESSAGE = "❌ 未找到 data.csv 檔案,請將資料庫檔案上傳至同目錄。"
|
| 34 |
|
| 35 |
+
# --- 2. 輔助函式 ---
|
| 36 |
def parse_dosage_to_numeric(dose_str):
|
| 37 |
if not isinstance(dose_str, str):
|
| 38 |
return 0.0
|
|
|
|
| 44 |
return val
|
| 45 |
return 0.0
|
| 46 |
|
| 47 |
+
# --- 3. 動態介面連動函式 ---
|
| 48 |
def update_component_choices(search_text):
|
| 49 |
if not ALL_COMPONENTS:
|
| 50 |
+
return gr.update(choices=[], value=[])
|
|
|
|
|
|
|
| 51 |
if not search_text or search_text.strip() == "":
|
| 52 |
+
return gr.update(choices=[], value=[])
|
|
|
|
|
|
|
| 53 |
search_text = search_text.strip().lower()
|
| 54 |
filtered = [c for c in ALL_COMPONENTS if search_text in c.lower()]
|
| 55 |
+
return gr.update(choices=filtered, value=[])
|
| 56 |
+
|
| 57 |
+
def update_form_choices(selected_comps):
|
| 58 |
+
if not selected_comps or df_global.empty:
|
| 59 |
+
return gr.update(choices=[], value=[])
|
| 60 |
+
df_filtered = df_global[df_global['成分'].isin(selected_comps)]
|
| 61 |
+
forms = sorted(df_filtered['劑型'].dropna().unique())
|
| 62 |
+
return gr.update(choices=forms, value=forms)
|
| 63 |
|
| 64 |
+
def update_dose_choices(selected_comps, selected_forms):
|
| 65 |
+
if not selected_comps or not selected_forms or df_global.empty:
|
| 66 |
+
return gr.update(choices=[], value=[])
|
| 67 |
+
df_filtered = df_global[
|
| 68 |
+
(df_global['成分'].isin(selected_comps)) &
|
| 69 |
+
(df_global['劑型'].isin(selected_forms))
|
| 70 |
+
]
|
| 71 |
+
doses = sorted(df_filtered['劑量'].dropna().unique(), key=parse_dosage_to_numeric)
|
| 72 |
+
return gr.update(choices=doses, value=doses)
|
| 73 |
+
|
| 74 |
+
# --- 4. 產出標準化 Excel 與 HTML 預覽報表 ---
|
| 75 |
+
def generate_reports(selected_components, selected_forms, selected_doses):
|
| 76 |
if df_global.empty:
|
| 77 |
+
return None, "❌ 系統未成功載入 data.csv 資料庫。", ""
|
| 78 |
+
if not selected_components or not selected_forms or not selected_doses:
|
| 79 |
+
return None, "❌ 請確認成分、劑型、劑量皆已勾選!", ""
|
| 80 |
+
|
| 81 |
+
df_filtered = df_global[
|
| 82 |
+
(df_global['成分'].isin(selected_components)) &
|
| 83 |
+
(df_global['劑型'].isin(selected_forms)) &
|
| 84 |
+
(df_global['劑量'].isin(selected_doses))
|
| 85 |
+
].copy()
|
| 86 |
|
|
|
|
|
|
|
| 87 |
if df_filtered.empty:
|
| 88 |
+
return None, "❌ 找不到符合該條件的資料!", ""
|
| 89 |
+
|
|
|
|
| 90 |
qty_col = '數量(顆)' if '數量(顆)' in df_filtered.columns else [col for col in df_filtered.columns if '數量' in col][0]
|
|
|
|
|
|
|
| 91 |
df_filtered[qty_col] = df_filtered[qty_col].astype(str).str.replace(',', '').str.strip()
|
| 92 |
df_filtered[qty_col] = pd.to_numeric(df_filtered[qty_col], errors='coerce').fillna(0)
|
| 93 |
+
|
|
|
|
| 94 |
pivot_df = df_filtered.groupby(['成分', '劑型', '劑量', '廠商', '年度'])[qty_col].sum().unstack(fill_value=0)
|
|
|
|
|
|
|
| 95 |
for year_col in ['2022年', '2023年', '2024年']:
|
| 96 |
if year_col not in pivot_df.columns:
|
| 97 |
pivot_df[year_col] = 0
|
| 98 |
|
| 99 |
pivot_df = pivot_df.reindex(columns=['2022年', '2023年', '2024年']).reset_index()
|
|
|
|
|
|
|
| 100 |
pivot_df['dose_numeric'] = pivot_df['劑量'].apply(parse_dosage_to_numeric)
|
| 101 |
+
pivot_df = pivot_df.sort_values(
|
| 102 |
+
by=['成分', '劑型', 'dose_numeric', '2024年'],
|
| 103 |
+
ascending=[True, True, True, False]
|
| 104 |
+
).drop(columns=['dose_numeric'])
|
| 105 |
+
|
| 106 |
+
# --- Excel 處理邏輯 ---
|
| 107 |
wb = Workbook()
|
| 108 |
ws = wb.active
|
| 109 |
ws.title = "廠商排名報表"
|
| 110 |
ws.views.sheetView[0].showGridLines = True
|
| 111 |
+
|
|
|
|
| 112 |
font_family = "微軟正黑體"
|
| 113 |
+
header_fill = PatternFill(start_color="1F497D", end_color="1F497D", fill_type="solid")
|
| 114 |
+
subtotal_fill = PatternFill(start_color="DCE6F1", end_color="DCE6F1", fill_type="solid")
|
| 115 |
+
total_fill = PatternFill(start_color="B8CCE4", end_color="B8CCE4", fill_type="solid")
|
| 116 |
|
| 117 |
header_font = Font(name=font_family, size=12, bold=True, color="FFFFFF")
|
| 118 |
data_font = Font(name=font_family, size=12)
|
|
|
|
| 123 |
right_align = Alignment(horizontal="right", vertical="center")
|
| 124 |
|
| 125 |
thin_side = Side(border_style="thin", color="D9D9D9")
|
| 126 |
+
thick_bottom_side = Side(border_style="medium", color="000000")
|
| 127 |
cell_border = Border(left=thin_side, right=thin_side, top=thin_side, bottom=thin_side)
|
| 128 |
+
thick_bottom_border = Border(left=thin_side, right=thin_side, top=thin_side, bottom=thick_bottom_side)
|
| 129 |
+
|
| 130 |
+
# --- HTML 處理邏輯 ---
|
| 131 |
+
html_content = f"""
|
| 132 |
+
<style>
|
| 133 |
+
.report-container {{
|
| 134 |
+
background: white;
|
| 135 |
+
padding: 10px;
|
| 136 |
+
width: 100%;
|
| 137 |
+
box-sizing: border-box;
|
| 138 |
+
-webkit-text-size-adjust: 100%; /* 【關鍵 1】嚴格禁止手機瀏覽器自動縮小整個網頁的字體 */
|
| 139 |
+
}}
|
| 140 |
+
.table-responsive {{
|
| 141 |
+
width: 100%;
|
| 142 |
+
overflow-x: auto;
|
| 143 |
+
-webkit-overflow-scrolling: touch;
|
| 144 |
+
padding-bottom: 10px;
|
| 145 |
+
}}
|
| 146 |
+
.report-table {{
|
| 147 |
+
width: max-content;
|
| 148 |
+
min-width: 100%;
|
| 149 |
+
border-collapse: collapse;
|
| 150 |
+
font-family: '微軟正黑體', sans-serif;
|
| 151 |
+
font-size: 15px !important; /* 【關鍵 2】從原本的 13px 放大至 15px,並加上 !important 強制生效 */
|
| 152 |
+
}}
|
| 153 |
+
.report-table th, .report-table td {{
|
| 154 |
+
border: 1px solid #D9D9D9;
|
| 155 |
+
padding: 8px 12px;
|
| 156 |
+
white-space: nowrap !important; /* 【關鍵 3】絕對不換行,讓表格直接往右邊長過去 */
|
| 157 |
+
}}
|
| 158 |
+
.report-table th {{
|
| 159 |
+
background-color: #1F497D !important;
|
| 160 |
+
color: #FFFFFF !important;
|
| 161 |
+
text-align: center;
|
| 162 |
+
font-weight: bold;
|
| 163 |
+
}}
|
| 164 |
+
.thick-bottom {{ border-bottom: 2px solid black !important; }}
|
| 165 |
+
</style>
|
| 166 |
|
| 167 |
+
<div id="report-capture-area" class="report-container">
|
| 168 |
+
"""
|
| 169 |
+
|
| 170 |
+
comp_names = "、".join(df_filtered['成分'].unique())
|
| 171 |
+
form_mapping = {"注射劑": "Inj.", "一般錠劑膠囊劑": "Tab./Cap.", "膜衣錠": "F.C. Tab.", "膠囊劑": "Cap.", "錠劑": "Tab."}
|
| 172 |
+
unique_forms = df_filtered['劑型'].unique()
|
| 173 |
+
form_abbr = f" {form_mapping.get(unique_forms[0], unique_forms[0])}" if len(unique_forms) == 1 else ""
|
| 174 |
+
doses_for_header = sorted(df_filtered['劑量'].unique(), key=parse_dosage_to_numeric)
|
| 175 |
+
doses_str = "、".join(doses_for_header)
|
| 176 |
+
header_title_text = f"{comp_names}{form_abbr} {doses_str}廠商申報量排名"
|
| 177 |
+
|
| 178 |
+
html_content += f'<h2 style="text-align:center; font-family: \'微軟正黑體\'; font-weight:bold;">{header_title_text}</h2>'
|
| 179 |
+
html_content += '<div class="table-responsive"><table class="report-table">'
|
| 180 |
+
|
| 181 |
+
headers = ["成分", "劑型", "劑量", "廠商", "2022年<br>數量", "2023年<br>數量", "2024年<br>數量", "2024年<br>占比(%)"]
|
| 182 |
+
ws_headers = [h.replace("<br>", "\n") for h in headers]
|
| 183 |
+
ws.append(ws_headers)
|
| 184 |
ws.row_dimensions[1].height = 30
|
| 185 |
+
|
| 186 |
+
html_content += "<tr>"
|
| 187 |
+
for col_idx, h in enumerate(ws_headers, 1):
|
| 188 |
cell = ws.cell(row=1, column=col_idx)
|
| 189 |
cell.font = header_font
|
| 190 |
cell.fill = header_fill
|
| 191 |
cell.alignment = center_align
|
| 192 |
cell.border = cell_border
|
| 193 |
+
html_content += f"<th style='color: #FFFFFF !important; background-color: #1F497D !important;'>{headers[col_idx-1]}</th>"
|
| 194 |
+
html_content += "</tr>"
|
| 195 |
+
|
| 196 |
+
unique_groups = pivot_df[['成分', '劑型', '劑量']].drop_duplicates()
|
| 197 |
+
unique_groups_list = list(unique_groups.iterrows())
|
| 198 |
|
| 199 |
grand_total_2022 = 0
|
| 200 |
grand_total_2023 = 0
|
| 201 |
grand_total_2024 = 0
|
|
|
|
| 202 |
current_row = 2
|
| 203 |
+
|
| 204 |
+
last_comp = None
|
| 205 |
+
last_form = None
|
| 206 |
+
|
| 207 |
+
for idx, (_, group_keys) in enumerate(unique_groups_list):
|
| 208 |
+
comp = group_keys['成分']
|
| 209 |
+
form = group_keys['劑型']
|
| 210 |
+
dose = group_keys['劑量']
|
| 211 |
+
|
| 212 |
+
is_last_of_form = False
|
| 213 |
+
if idx == len(unique_groups_list) - 1:
|
| 214 |
+
is_last_of_form = True
|
| 215 |
+
else:
|
| 216 |
+
next_comp = unique_groups_list[idx+1][1]['成分']
|
| 217 |
+
next_form = unique_groups_list[idx+1][1]['劑型']
|
| 218 |
+
if comp != next_comp or form != next_form:
|
| 219 |
+
is_last_of_form = True
|
| 220 |
+
|
| 221 |
+
dose_group = pivot_df[
|
| 222 |
+
(pivot_df['成分'] == comp) &
|
| 223 |
+
(pivot_df['劑型'] == form) &
|
| 224 |
+
(pivot_df['劑量'] == dose)
|
| 225 |
+
]
|
| 226 |
|
| 227 |
+
dose_2024_sum = dose_group['2024年'].sum()
|
| 228 |
+
dose_subtotal_2022 = dose_group['2022年'].sum()
|
| 229 |
+
dose_subtotal_2023 = dose_group['2023年'].sum()
|
| 230 |
+
dose_subtotal_2024 = dose_2024_sum
|
| 231 |
|
| 232 |
+
print_comp = (comp != last_comp)
|
| 233 |
+
print_form = print_comp or (form != last_form)
|
| 234 |
+
is_first_row_in_dose = True
|
| 235 |
|
| 236 |
for _, row in dose_group.iterrows():
|
| 237 |
+
qty_2022 = float(row['2022年'])
|
| 238 |
+
qty_2023 = float(row['2023年'])
|
| 239 |
+
qty_2024 = float(row['2024年'])
|
| 240 |
ratio = (qty_2024 / dose_2024_sum) if dose_2024_sum > 0 else 0.0
|
| 241 |
|
| 242 |
+
c_val = row['成分'] if print_comp and is_first_row_in_dose else ""
|
| 243 |
+
f_val = row['劑型'] if print_form and is_first_row_in_dose else ""
|
| 244 |
+
d_val = row['劑量'] if is_first_row_in_dose else ""
|
|
|
|
| 245 |
|
| 246 |
ws.append([c_val, f_val, d_val, row['廠商'], qty_2022, qty_2023, qty_2024, ratio])
|
| 247 |
+
ws.row_dimensions[current_row].height = 25
|
| 248 |
+
|
| 249 |
+
html_content += f"<tr>"
|
| 250 |
+
html_content += f"<td style='text-align:center;'>{c_val}</td>"
|
| 251 |
+
html_content += f"<td style='text-align:center;'>{f_val}</td>"
|
| 252 |
+
html_content += f"<td style='text-align:center;'>{d_val}</td>"
|
| 253 |
+
html_content += f"<td style='text-align:left;'>{row['廠商']}</td>"
|
| 254 |
+
html_content += f"<td style='text-align:right;'>{qty_2022:,.0f}</td>"
|
| 255 |
+
html_content += f"<td style='text-align:right;'>{qty_2023:,.0f}</td>"
|
| 256 |
+
html_content += f"<td style='text-align:right;'>{qty_2024:,.0f}</td>"
|
| 257 |
+
html_content += f"<td style='text-align:right;'>{ratio:.1%}</td>"
|
| 258 |
+
html_content += "</tr>"
|
| 259 |
|
|
|
|
| 260 |
for col_idx in range(1, 9):
|
| 261 |
cell = ws.cell(row=current_row, column=col_idx)
|
| 262 |
cell.font = data_font
|
| 263 |
cell.border = cell_border
|
| 264 |
+
if col_idx in [1, 2, 3]: cell.alignment = center_align
|
| 265 |
+
elif col_idx == 4: cell.alignment = left_align
|
| 266 |
+
elif col_idx in [5, 6, 7]: cell.alignment = right_align; cell.number_format = '#,##0'
|
| 267 |
+
elif col_idx == 8: cell.alignment = right_align; cell.number_format = '0.0%'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
|
|
|
|
|
|
|
|
|
| 269 |
current_row += 1
|
| 270 |
+
is_first_row_in_dose = False
|
| 271 |
+
|
| 272 |
+
last_comp = comp
|
| 273 |
+
last_form = form
|
| 274 |
|
| 275 |
+
# 合計列
|
| 276 |
ws.append(["", "", f"{dose} 合計", "", dose_subtotal_2022, dose_subtotal_2023, dose_subtotal_2024, 1.0])
|
| 277 |
+
ws.merge_cells(start_row=current_row, start_column=3, end_row=current_row, end_column=4)
|
| 278 |
+
ws.row_dimensions[current_row].height = 25
|
| 279 |
+
|
| 280 |
+
thick_class = "thick-bottom" if is_last_of_form else ""
|
| 281 |
+
html_content += f"""
|
| 282 |
+
<tr style="background-color: #DCE6F1; font-weight: bold;">
|
| 283 |
+
<td class="{thick_class}"></td><td class="{thick_class}"></td>
|
| 284 |
+
<td colspan="2" class="{thick_class}" style="text-align:center;">{dose} 合計</td>
|
| 285 |
+
<td class="{thick_class}" style="text-align:right;">{dose_subtotal_2022:,.0f}</td>
|
| 286 |
+
<td class="{thick_class}" style="text-align:right;">{dose_subtotal_2023:,.0f}</td>
|
| 287 |
+
<td class="{thick_class}" style="text-align:right;">{dose_subtotal_2024:,.0f}</td>
|
| 288 |
+
<td class="{thick_class}" style="text-align:right;">100.0%</td>
|
| 289 |
+
</tr>
|
| 290 |
+
"""
|
| 291 |
+
|
| 292 |
for col_idx in range(1, 9):
|
| 293 |
cell = ws.cell(row=current_row, column=col_idx)
|
| 294 |
cell.font = bold_font
|
| 295 |
cell.fill = subtotal_fill
|
| 296 |
+
cell.border = thick_bottom_border if is_last_of_form else cell_border
|
| 297 |
+
if col_idx == 3: cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False)
|
| 298 |
+
elif col_idx in [5, 6, 7]: cell.alignment = right_align; cell.number_format = '#,##0'
|
| 299 |
+
elif col_idx == 8: cell.alignment = right_align; cell.number_format = '0.0%'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
|
| 301 |
grand_total_2022 += dose_subtotal_2022
|
| 302 |
grand_total_2023 += dose_subtotal_2023
|
| 303 |
grand_total_2024 += dose_subtotal_2024
|
| 304 |
current_row += 1
|
| 305 |
+
|
| 306 |
+
# 總計列
|
| 307 |
+
ws.append(["總計", "", "", "", grand_total_2022, grand_total_2023, grand_total_2024, 1.0])
|
| 308 |
+
ws.merge_cells(start_row=current_row, start_column=1, end_row=current_row, end_column=4)
|
| 309 |
+
ws.row_dimensions[current_row].height = 25
|
| 310 |
+
|
| 311 |
+
html_content += f"""
|
| 312 |
+
<tr style="background-color: #B8CCE4; font-weight: bold;">
|
| 313 |
+
<td colspan="4" style="text-align:center;">總計</td>
|
| 314 |
+
<td style="text-align:right;">{grand_total_2022:,.0f}</td>
|
| 315 |
+
<td style="text-align:right;">{grand_total_2023:,.0f}</td>
|
| 316 |
+
<td style="text-align:right;">{grand_total_2024:,.0f}</td>
|
| 317 |
+
<td style="text-align:right;">100.0%</td>
|
| 318 |
+
</tr>
|
| 319 |
+
</table></div>
|
| 320 |
+
"""
|
| 321 |
+
|
| 322 |
+
# 頁尾
|
| 323 |
+
html_content += """
|
| 324 |
+
<div style="display:flex; justify-content:space-between; margin-top: 15px; font-size: 12px; color: #555;">
|
| 325 |
+
<span>中央健康保險署 政府資料開放平台 2024年資料</span>
|
| 326 |
+
<span>https://data.gov.tw/dataset/22131</span>
|
| 327 |
+
</div>
|
| 328 |
+
</div>
|
| 329 |
+
"""
|
| 330 |
+
|
| 331 |
for col_idx in range(1, 9):
|
| 332 |
cell = ws.cell(row=current_row, column=col_idx)
|
| 333 |
cell.font = bold_font
|
| 334 |
cell.fill = total_fill
|
| 335 |
cell.border = cell_border
|
| 336 |
+
if col_idx == 1: cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=False)
|
| 337 |
+
elif col_idx in [5, 6, 7]: cell.alignment = right_align; cell.number_format = '#,##0'
|
| 338 |
+
elif col_idx == 8: cell.alignment = right_align; cell.number_format = '0.0%'
|
| 339 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
for col in ws.columns:
|
|
|
|
| 341 |
col_letter = get_column_letter(col[0].column)
|
| 342 |
+
if col_letter == 'H':
|
| 343 |
+
ws.column_dimensions[col_letter].width = 11.5
|
| 344 |
+
continue
|
| 345 |
+
max_len = 0
|
| 346 |
for cell in col:
|
| 347 |
val_str = str(cell.value or '')
|
| 348 |
lines = val_str.split('\n')
|
| 349 |
for line in lines:
|
| 350 |
line_len = sum(2 if '\u4e00' <= char <= '\u9fff' else 1 for char in line)
|
| 351 |
+
if line_len > max_len: max_len = line_len
|
|
|
|
| 352 |
ws.column_dimensions[col_letter].width = max(max_len + 4, 12)
|
| 353 |
+
|
|
|
|
| 354 |
ws.sheet_properties.pageSetUpPr.fitToPage = True
|
| 355 |
+
ws.page_setup.fitToWidth = 1
|
| 356 |
+
ws.page_setup.fitToHeight = 0
|
| 357 |
+
ws.page_margins.top = 2.7 / 2.54; ws.page_margins.bottom = 2.5 / 2.54
|
| 358 |
+
ws.page_margins.left = 1.5 / 2.54; ws.page_margins.right = 1.5 / 2.54
|
| 359 |
+
ws.page_margins.header = 1.5 / 2.54; ws.page_margins.footer = 1.0 / 2.54
|
| 360 |
+
|
| 361 |
+
header_string_for_print = f'&"微軟正黑體,Bold"&16{header_title_text}'
|
| 362 |
+
ws.oddHeader.center.text = header_string_for_print
|
| 363 |
+
ws.oddFooter.left.text = '&"微軟正黑體,Regular"&12中央健康保險署 政府資料開放平台 2024年資料'
|
| 364 |
+
ws.oddFooter.right.text = '&"微軟正黑體,Regular"&12https://data.gov.tw/dataset/22131'
|
| 365 |
+
ws.page_setup.scaleWithDoc = True
|
| 366 |
+
ws.page_setup.alignWithMargins = True
|
| 367 |
+
|
| 368 |
+
safe_filename = re.sub(r'[\\/*?:"<>|]', "_", header_title_text) + ".xlsx"
|
| 369 |
+
wb.save(safe_filename)
|
| 370 |
|
| 371 |
+
success_msg = "🎉 成功!已順利產出「Excel 報表」與下方「預覽畫面」。請點擊下方按鈕下載檔案或存為圖片!"
|
| 372 |
+
return safe_filename, success_msg, html_content
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
# --- 5. 終極優化:完美截圖與手機互動 JavaScript ---
|
| 376 |
+
download_js = """
|
| 377 |
+
function() {
|
| 378 |
+
var element = document.getElementById('report-capture-area');
|
| 379 |
+
if (!element) {
|
| 380 |
+
alert('請先產生報表再下載圖片!');
|
| 381 |
+
return [];
|
| 382 |
+
}
|
| 383 |
|
| 384 |
+
if (typeof html2canvas === 'undefined') {
|
| 385 |
+
alert('截圖套件載入中,請稍後再試或重新整理網頁。');
|
| 386 |
+
return [];
|
| 387 |
+
}
|
|
|
|
| 388 |
|
| 389 |
+
var table = document.querySelector('.report-table');
|
| 390 |
+
var wrapper = document.querySelector('.table-responsive');
|
| 391 |
+
|
| 392 |
+
var originalElementWidth = element.style.width;
|
| 393 |
+
var originalWrapperOverflow = wrapper ? wrapper.style.overflowX : '';
|
| 394 |
+
var targetWidth = table ? (table.offsetWidth + 20) : element.scrollWidth;
|
| 395 |
|
| 396 |
+
element.style.width = targetWidth + 'px';
|
| 397 |
+
if(wrapper) wrapper.style.overflowX = 'visible';
|
|
|
|
| 398 |
|
| 399 |
+
var titleElement = element.querySelector('h2');
|
| 400 |
+
var fileName = titleElement ? titleElement.innerText.replace(/[\\\\/*?:"<>|]/g, "_") : '廠商排名報表';
|
| 401 |
+
|
| 402 |
+
html2canvas(element, {
|
| 403 |
+
scale: 2,
|
| 404 |
+
backgroundColor: '#FFFFFF',
|
| 405 |
+
width: targetWidth,
|
| 406 |
+
windowWidth: targetWidth
|
| 407 |
+
}).then(function(canvas) {
|
| 408 |
+
|
| 409 |
+
// 截圖完畢,瞬間把排版縮回去
|
| 410 |
+
element.style.width = originalElementWidth;
|
| 411 |
+
if(wrapper) wrapper.style.overflowX = originalWrapperOverflow;
|
| 412 |
+
|
| 413 |
+
// 【修正 1:強化手機與平板判斷,涵蓋 iPad 的桌面網站模式】
|
| 414 |
+
var isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
| 415 |
+
|
| 416 |
+
canvas.toBlob(function(blob) {
|
| 417 |
+
var file = new File([blob], fileName + '.png', { type: 'image/png' });
|
| 418 |
+
var imgDataUrl = canvas.toDataURL('image/png');
|
| 419 |
+
// 【修正 2:優先嘗試 Web Share API,呼叫原生「儲存影像」】
|
| 420 |
+
if (isMobile && navigator.canShare && navigator.canShare({ files: [file] })) {
|
| 421 |
+
navigator.share({
|
| 422 |
+
files: [file],
|
| 423 |
+
title: fileName
|
| 424 |
+
}).then(() => {
|
| 425 |
+
console.log('成功呼叫原生分享選單');
|
| 426 |
+
}).catch((error) => {
|
| 427 |
+
// 若使用者取消或 iframe 權限遭擋,降級使用長按視窗
|
| 428 |
+
console.log('無法使用原生分享,改用長按模式', error);
|
| 429 |
+
showLongPressModal(imgDataUrl);
|
| 430 |
+
});
|
| 431 |
+
}
|
| 432 |
+
// 【修正 3:若不支援 Share API,強制顯示長按視窗】
|
| 433 |
+
else if (isMobile) {
|
| 434 |
+
showLongPressModal(imgDataUrl);
|
| 435 |
+
}
|
| 436 |
+
// 電腦版維持自動下載
|
| 437 |
+
else {
|
| 438 |
+
var link = document.createElement('a');
|
| 439 |
+
link.download = fileName + '.png';
|
| 440 |
+
link.href = imgDataUrl;
|
| 441 |
+
link.click();
|
| 442 |
+
}
|
| 443 |
+
}, 'image/png');
|
| 444 |
+
// 將長按視窗獨立為函式,方便降級呼叫
|
| 445 |
+
function showLongPressModal(imgSrc) {
|
| 446 |
+
var modal = document.createElement('div');
|
| 447 |
+
modal.style.position = 'fixed';
|
| 448 |
+
modal.style.top = '0';
|
| 449 |
+
modal.style.left = '0';
|
| 450 |
+
modal.style.width = '100vw';
|
| 451 |
+
modal.style.height = '100vh';
|
| 452 |
+
modal.style.backgroundColor = 'rgba(0,0,0,0.85)';
|
| 453 |
+
modal.style.zIndex = '9999';
|
| 454 |
+
modal.style.display = 'flex';
|
| 455 |
+
modal.style.flexDirection = 'column';
|
| 456 |
+
modal.style.alignItems = 'center';
|
| 457 |
+
modal.style.justifyContent = 'center';
|
| 458 |
+
|
| 459 |
+
var closeBtn = document.createElement('button');
|
| 460 |
+
closeBtn.innerText = '✕ 關閉';
|
| 461 |
+
closeBtn.style.position = 'absolute';
|
| 462 |
+
closeBtn.style.top = '20px';
|
| 463 |
+
closeBtn.style.right = '20px';
|
| 464 |
+
closeBtn.style.padding = '8px 16px';
|
| 465 |
+
closeBtn.style.fontSize = '16px';
|
| 466 |
+
closeBtn.style.backgroundColor = '#ff4444';
|
| 467 |
+
closeBtn.style.color = 'white';
|
| 468 |
+
closeBtn.style.border = 'none';
|
| 469 |
+
closeBtn.style.borderRadius = '5px';
|
| 470 |
+
closeBtn.style.fontWeight = 'bold';
|
| 471 |
+
closeBtn.onclick = function() { document.body.removeChild(modal); };
|
| 472 |
+
|
| 473 |
+
var hint = document.createElement('div');
|
| 474 |
+
hint.innerText = '👇 請「長按」下方圖片,選擇「儲存到照片 / 儲存影像」';
|
| 475 |
+
hint.style.color = 'white';
|
| 476 |
+
hint.style.fontSize = '16px';
|
| 477 |
+
hint.style.fontWeight = 'bold';
|
| 478 |
+
hint.style.marginBottom = '20px';
|
| 479 |
+
hint.style.padding = '10px';
|
| 480 |
+
hint.style.textAlign = 'center';
|
| 481 |
+
hint.style.backgroundColor = '#1F497D';
|
| 482 |
+
hint.style.borderRadius = '8px';
|
| 483 |
+
|
| 484 |
+
var img = document.createElement('img');
|
| 485 |
+
img.src = imgSrc;
|
| 486 |
+
img.style.maxWidth = '95%';
|
| 487 |
+
img.style.maxHeight = '75vh';
|
| 488 |
+
img.style.border = '2px solid white';
|
| 489 |
+
img.style.borderRadius = '5px';
|
| 490 |
+
img.style.objectFit = 'contain';
|
| 491 |
+
|
| 492 |
+
// 確保圖片支援 iOS 長按呼叫選單
|
| 493 |
+
img.style.webkitTouchCallout = 'default';
|
| 494 |
+
img.style.userSelect = 'auto';
|
| 495 |
+
|
| 496 |
+
modal.appendChild(closeBtn);
|
| 497 |
+
modal.appendChild(hint);
|
| 498 |
+
modal.appendChild(img);
|
| 499 |
+
document.body.appendChild(modal);
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
});
|
| 503 |
+
return [];
|
| 504 |
+
}
|
| 505 |
+
"""
|
| 506 |
|
| 507 |
+
|
| 508 |
+
# --- 6. 介面層 (載入 html2canvas 套件) ---
|
| 509 |
+
with gr.Blocks(
|
| 510 |
+
theme=gr.themes.Default(primary_hue="blue", secondary_hue="slate"), # 【新增這行】將系統按鈕主色調改為藍色
|
| 511 |
+
title="健保資料庫數據分析工具",
|
| 512 |
+
head='<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>'
|
| 513 |
+
) as demo:
|
| 514 |
+
|
| 515 |
+
gr.Markdown("# 💊 健保資料庫數據分析工具")
|
| 516 |
gr.Markdown(f"**系統狀態:** {INIT_MESSAGE}")
|
| 517 |
|
| 518 |
+
gr.Markdown("⚠️ **提醒使用手機版的同仁:請務必使用系統預設瀏覽器(如 Safari 或 Chrome)開啟本網頁,才能成功下載檔案與圖片。**")
|
| 519 |
+
|
| 520 |
with gr.Row():
|
| 521 |
+
# 左側區塊:輸入條件
|
| 522 |
with gr.Column(scale=1):
|
| 523 |
+
gr.Markdown("### 🔍 輸入條件")
|
| 524 |
+
search_input = gr.Textbox(label="第一步:輸入成分關鍵字", placeholder="例如:Levofloxacin", value="")
|
| 525 |
+
component_choices = gr.CheckboxGroup(label="📋 第二步:勾選成分品項 (可多選)", choices=[], value=[])
|
| 526 |
+
form_choices = gr.CheckboxGroup(label="💊 第三步:勾選欲包含的劑型", choices=[], value=[])
|
| 527 |
+
dose_choices = gr.CheckboxGroup(label="🧪 第四步:勾選欲包含的劑量", choices=[], value=[])
|
| 528 |
+
submit_btn = gr.Button("🚀 第五步:產生 Excel 報表與預覽", variant="primary")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
|
| 530 |
+
# 右側區塊:下載與輸出
|
| 531 |
with gr.Column(scale=1):
|
| 532 |
+
gr.Markdown("### 📥 報表與圖片下載")
|
| 533 |
status_output = gr.Textbox(label="系統處理結果", interactive=False)
|
| 534 |
+
|
| 535 |
+
# 【修改重點 1】:將 gr.File 換成 gr.DownloadButton,設定 variant="primary" 和 size="lg" 讓它變成大橘色按鈕
|
| 536 |
+
download_excel_btn = gr.DownloadButton("📄 一鍵下載 Excel 報表", variant="primary", size="lg")
|
| 537 |
+
download_img_btn = gr.Button("🖼️ 一鍵下載為高畫質圖片 (PNG)", variant="primary", size="lg")
|
| 538 |
|
| 539 |
+
gr.Markdown("---")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
|
| 541 |
+
gr.Markdown("### 網頁即時預覽")
|
| 542 |
+
html_output = gr.HTML(label="報表預覽區")
|
| 543 |
+
|
| 544 |
+
# --- 綁定事件 ---
|
| 545 |
+
search_input.change(fn=update_component_choices, inputs=search_input, outputs=component_choices)
|
| 546 |
+
component_choices.change(fn=update_form_choices, inputs=component_choices, outputs=form_choices)
|
| 547 |
+
form_choices.change(fn=update_dose_choices, inputs=[component_choices, form_choices], outputs=dose_choices)
|
| 548 |
+
|
| 549 |
+
# 【修改重點 2】:將原本輸出對象的 file_output 替換為 download_excel_btn
|
| 550 |
submit_btn.click(
|
| 551 |
+
fn=generate_reports,
|
| 552 |
+
inputs=[component_choices, form_choices, dose_choices],
|
| 553 |
+
outputs=[download_excel_btn, status_output, html_output]
|
| 554 |
)
|
| 555 |
+
|
| 556 |
+
# 綁定進化版的 JS 截圖指令
|
| 557 |
+
download_img_btn.click(fn=None, js=download_js)
|
| 558 |
|
| 559 |
if __name__ == "__main__":
|
| 560 |
demo.launch()
|