香港公司秘書/公司治理專業資格考試 CGQP|《財務與會計信息解讀》模擬題庫

每位學員用自己的帳號登入,錯題本與練習記錄會分開存放,不會互相混淆。 帳號資料储存在您目前使用的這台裝置瀏覽器內。
📖 考點速覽|12 章重點蒸餾(開考前 5 分鐘過一遍)(點擊展開)
第1章 · 財務報告框架與概念
  • 報告框架:IFRS 與 HKFRS 對應;概念框架的定性特徵(相關性、如實反映)
  • 權責發生制與持續經營基準
  • 會計政策、估計變更與差錯更正的處理差異
第2章 · 報表編製與計量
  • 五大報表:財務狀況、綜合收益、權益變動、現金流、附註
  • 現金流量表三分類:經營、投資、融資
  • 報表之間的勾稽關係
第3章 · 集團與合併報表
  • 控制定義(IFRS 10):權力、可變回報、運用權力影響回報
  • 商譽 = 對價 + NCI − 可辨認淨資產公允值
  • NCI 計量:公允值 or 應佔比例份額
第4章 · 財務分析與比率
  • 四大類比率:盈利能力、流動性、杠桿、營運效率
  • 杜邦分解:ROE = 邊際利潤 × 資產周轉 × 財務杠桿
  • 比率須與行業、歷史、趨勢對比才有意義
第5章 · 審計與治理中的財務角色
  • 審計意見類型:無保留、保留、否定、無法表示意見
  • 強調事項段與其他事項段的含義
  • 審計委員會與外審的溝通要點
第6章 · 融資與資本成本
  • WACC = E/V×Re + D/V×Rd×(1−t)
  • 資本結構:MM 理論、權衡理論、優序融資
  • 股權融資 vs 債務融資的成本與控制權影響
第7章 · 收入與特殊交易
  • IFRS 15 五步法:識別合同→履約義務→交易價格→分攤→履約確認
  • 時點確認 vs 時段確認
  • 合約資產、合約負債、應收帳款三者區分
第8章 · 金融工具
  • IFRS 9 分類:攤餘成本/FVOCI/FVTPL(業務模式 + 現金流特徵)
  • 預期信用損失(ECL)三階段模型
  • 對沖會計的基本概念
第9章 · 租賃與員工福利延伸
  • IFRS 16:承租人確認使用權資產與租賃負債
  • 租賃負債折現率:內含利率 or 增量借款利率
  • 設定受益計劃:義務現值、計劃資產、精算損益
第10章 · 報表勾稽與質量
  • 盈利質量信號:經營現金流與淨利潤背離、應收激增
  • 應計項目比率與收益平滑的識別
  • 舞弊風險指標(收入確認激進、資本化濫用)
第11章 · 估值基礎
  • 估值方法:DCF(現金流折現)、可比公司、可比交易
  • 常用倍數:P/E、EV/EBITDA、P/B
  • 折現率與終值的敏感性
第12章 · 情景分析
  • 綜合情景:解讀報表診斷財務健康、評估投資/融資決策

※ 以上為按課程大綱整理的自編備考重點,並非官方考試內容;系統學習請參閱 HKCGI 官方課程資料。

120題庫總題數
50%合格線
75%優秀線
第 1 / 100 題 模擬練習
0%
合格

📊 各章節表現

❌ 錯題回顧(已加入錯題庫)

📕 我的錯題本(0 題)

每次答錯的題目會自動記錄到這裡;在「錯題重做」中答對後會自動移出。

⚠️ 我的異議清單(0 條)

做題時點「⚠️ 質疑此題」即可記錄。確認後請一次過傳送給我們跟進核實。

🗂 我的練習記錄

👤 我的帳號

🔑 修改密碼

const BANK = [[1,1,"1","香港財務報告的主要準則制定者是:","香港會計師公會(HKICPA)訂立的香港財務報告準則(HKFRS)","公司註冊處","聯交所","稅務局","A","香港財務報告準則由香港會計師公會制定,與 IFRS 趨同,適用於香港企業財務報表編製。"],[2,1,"1","IASB《概念框架》列出的財務報表五大要素是:","資產、負債、權益、收益、費用","現金、應收、存貨、固定資產、無形資產","收入、成本、毛利、淨利、股息","借方、貸方、試算、結轉、結餘","A","概念框架界定的五要素:Asset、Liability、Equity、Income、Expense,是確認與計量的基礎。"],[3,1,"1","權責發生制(accrual basis)的核心是:","交易於發生時確認,而非現金收付時","只在收到現金時確認收入","按納稅申報需要確認","由董事酌情確認","A","權責發生制要求在交易事項發生時確認財務影響,使報表反映期間真實業績。"],[4,1,"1","財務報表的「真實與公平」(true and fair view)要求源自:","《公司條例》第379條及相關附屬法例","上市規則第7章","稅務條例","銀行公會指引","A","香港《公司條例》要求財務報表真實公平地反映公司財務狀況、表現及現金流。"],[5,1,"1","重要性(materiality)概念在財務匯報中的含義是:","若資訊遺漏或錯報會影響使用者決策,即屬重要","所有項目等額披露","只披露有利項目","金額超過100萬即重要","A","重要性取決於性質與金額對使用者經濟決策的影響,是判斷披露範圍與錯誤處理的核心概念。"],[6,1,"1","「謹慎性」(prudence)原則的應用示例是:","對存貨按成本與可變現淨值孰低計量","高估收入","遞延確認費用","高估資產","A","謹慎性要求不誇大資產或收益、不低估負債或費用,存貨減值即其典型體現。"],[7,1,"1","「持續經營」(going concern)假設若不成立,管理層須:","改按清算基礎編製財務報表並充分披露","照常編製並隱瞞","只修改現金流量表","無須任何處理","A","持續經營存在重大疑問時,須改用其他編製基礎並在報表附註充分披露,否則報表會誤導使用者。"],[8,1,"1","香港上市實體財務匯報的監管主體組合是:","聯交所(上市規則)+會計及財務匯報局(AFRC,查察核數師與財匯匯報失當)","只靠公司註冊處","只靠稅務局","無監管","A","上市公司財報同受《上市規則》財匯匯報規定及 AFRC 的獨立監管約束。"],[9,1,"1","會計政策一致性(consistency)原則是指:","同類交易在不同期間應採用一致的會計政策,變更須說明理由及影響","政策可隨意更改","每年必須更換政策","政策只須口頭說明","A","一致性使報表可比較;政策變更須符合準則要求並追溯調整或充分披露。"],[10,1,"1","綜合財務報表(consolidated financial statements)的基本前提是:","母公司須合併其控制的所有附屬公司","只合併盈利子公司","由核數師選擇","按稅務需要合併","A","HKFRS 10 要求以「控制」(power + 變動收益暴露 + 影響能力)為基礎確定合併範圍。"],[11,2,"2","應收賬款出現減值跡象時,正確處理是:","按預期信用損失模型(ECL)計提減值","等到壞賬實際發生才處理","增加應收賬款","轉為收入","A","HKFRS 9 採用預期信用損失模型,須前瞻性地按違約概率計提減值準備。"],[12,2,"2","收入確認(HKFRS 15)的五步模型是:","識別合同→識別履約義務→確定交易價格→分攤價格→履行義務時確認收入","開票即確認收入","收款即確認收入","發貨前三步確認","A","五步法按履約義務的完成(時點或時段)確認收入,取代行業分散的舊規則。"],[13,2,"2","物業、廠房及設備(PP&E)後續計量的兩種允許模式是:","成本模式或重估模式","市價模式或清算模式","稅務模式","現值模式","A","HKAS 16 允許成本模式或重估模式,但須全類一致並定期重估。"],[14,2,"2","使用權資產與租賃負債(HKFRS 16)的確認要求是:","承租人幾乎所有租賃均確認使用權資產及租賃負債(短期及低價值豁免除外)","全部費用化","由董事選擇","只確認現金租金","A","HKFRS 16 取消經營租賃與融資租賃之分,承租人按單一模型確認資產與負債。"],[15,2,"2","遞延稅項(HKAS 12)的產生源於:","會計利潤與應課稅利潤的暫時性差異","永久性差異","現金流差異","股東要求","A","暫時性差異(如折舊與稅務折讓差異)產生遞延稅項資產或負債,按資產負債表法核算。"],[16,2,"2","「事件後事項」(HKAS 10)中屬調整事項的是:","資產負債表日後證實某債務人於報表日已破產,應計提減值","報表日後的新投資計劃","股息宣布","火災導致的日後損失","A","調整事項為報表日已存在狀況的進一步證據,須調整報表;非調整事項僅須披露。"],[17,2,"2","商譽(goodwill)的會計處理正確的是:","不攤銷,但每年至少進行減值測試","按10年強制攤銷","可隨時重估增值","計入負債","A","HKAS 36/IFRS 3 規定商譽不攤銷、每年減值測試,減值不可轉回。"],[18,2,"2","現金流量表將現金流分為:","經營、投資、融資活動","收入、支出、結餘","內部與外部","法定與自願","A","現金流量表三分法協助使用者判斷企業現金產生與使用結構。"],[19,2,"2","存貨計量(HKAS 2)的基本規則是:","按成本與可變現淨值(NRV)孰低","按售價","按重置成本","按管理層估計","A","存貨減記至 NRV,售價回升時可轉回(在原減記範圍內)。"],[20,2,"2","權益類交易(如發行新股)在報表中的體現是:","增加股本及相關溢價(資本儲備),不經損益表","計入收入","計入負債","計入費用","A","權益交易直接計入權益,不得確認損益,是維持報表質素的重要原則。"],[21,3,"3","「控制」的三要素(HKFRS 10)是:","對被投資方的權力、可變回報的暴露、運用權力影響回報的能力","持股50%以上","任命一名董事","提供貸款","A","合併範圍按實質控制判斷,而非僅看持股比例;可變回報與權力聯動是關鍵。"],[22,3,"3","少數權益(non-controlling interest)在合併報表中呈列於:","權益項目內,與母公司擁有人權益分開列示","負債","少數計入損益表底部","不呈列","A","NCI 屬集團權益的一部分,須單獨列示,損益亦須歸屬母公司擁有人與 NCI。"],[23,3,"3","集團內部交易在合併時的正確處理是:","完全抵銷(如內部銷售、未實現利潤、內部應收應付)","全額合併","只抵銷利潤","由核數師決定","A","合併以單一經濟實體為基礎,內部交易與未實現損益須全額抵銷。"],[24,3,"3","聯營公司(associate)的會計方法是:","權益法(equity method)","完全合併","成本法","公允價值法","A","對有重大影響(通常20%-50%投票權)的聯營,投資者按權益法計入應佔利潤與權益。"],[25,3,"3","合資安排(HKFRS 11)分為:","共同經營(joint operation)與合資企業(joint venture)","全資與合資","上市與非上市","短期與長期","A","按權利義務結構區分:共同經營按份額直接確認資產負債;合資企業按權益法。"],[26,3,"3","收購產生的商譽計算基礎是:","轉讓對價+NCI+原權益公允價值,減去可辨認淨資產公允價值","收購總價","被收購方賬面淨資產","付現金額","A","商譽=合併對價總額超過可辨認淨資產公允價值應佔份額的部分。"],[27,3,"3","外幣折算(HKAS 21)中,境外附屬公司報表折算的處理是:","資產負債按收市匯率、損益按平均匯率,差額計入其他綜合收益(匯兌儲備)","全部按歷史匯率","全部按平均匯率","不折算","A","功能貨幣不同的附屬公司按現行匯率法折算,折算差異計入 OCI。"],[28,3,"3","分部報告(HKFRS 8)的披露基礎是:","按經營分部(以首席營運決策者的內部管理報告為基礎)","按地區強制劃分","按產品數量","由核數師指定","A","分部以內部管理結構(CODM 視角)為基礎識別,披露分部收入、業績與資產。"],[29,3,"3","中期報告(HKAS 34)的要求是:","至少包含簡明財務報表、分部與或有事項等關鍵披露","須與年報同等詳盡","無須審閱","只報收入","A","中期財務報告聚焦變化與重要性,上市公司中期報告另須符合《上市規則》要求。"],[30,3,"3","合併報表附註中「會計政策披露」的目的是:","讓使用者理解計量基礎與判斷,評估可比性","滿足字數要求","替代審計意見","供稅局使用","A","重大會計政策、關鍵判斷與會計估計不確定性(HKAS 1 §122-125)是附註的核心內容。"],[31,4,"4","流動比率(current ratio)的計算是:","流動資產 ÷ 流動負債","流動負債 ÷ 流動資產","速動資產 ÷ 存貨","股東權益 ÷ 總資產","A","流動比率衡量短期償債能力;速動比率再剔除存貨更嚴格。"],[32,4,"4","存貨周轉率的計算是:","銷售成本 ÷ 平均存貨","收入 ÷ 存貨","淨利 ÷ 存貨","存貨 ÷ 銷售成本","A","存貨周轉率反映存貨流動效率,周轉期越短通常營運資金管理越好。"],[33,4,"4","資產回報率(ROA)的計算是:","淨利潤 ÷ 平均總資產","淨利潤 ÷ 收入","收入 ÷ 總資產","淨利潤 ÷ 股東權益","A","ROA 衡量資產的整體盈利效率;ROE 則衡量股東資本回報。"],[34,4,"4","杠桿比率(gearing)升高通常意味着:","財務風險上升,償債與再融資壓力加大","風險必然降低","股息必然增加","現金必然充裕","A","高負債放大回報也放大風險,分析時須結合利息保障倍數與現金流。"],[35,4,"4","利息保障倍數(interest cover)的計算是:","息稅前利潤(EBIT)÷ 利息費用","淨利 ÷ 收入","負債 ÷ 權益","現金 ÷ 利息","A","倍數越低,償付利息的緩衝越小,是債權人關注的核心指標。"],[36,4,"4","營運資金周期(WCC)的長短取決於:","存貨周期+應收周期-應付周期","固定資產使用年限","貸款期限","折舊方法","A","WCC 反映現金被營運佔用的天數;縮短周期可降低融資需求。"],[37,4,"4","杜邦分析將 ROE 拆解為:","利潤率 × 資產周轉率 × 權益乘數","收入 × 成本 × 利潤","毛利、淨利、稅利","市盈率與市帳率","A","杜邦分解揭示 ROE 由盈利能力、資產效率與財務杠桿共同驅動。"],[38,4,"4","盈利質量(earnings quality)分析會警惕:","收益高度依賴一次性項目或會計估計變動","現金流與利潤同步增長","審計意見無保留","毛利穩定","A","利潤與經營現金流長期背離、頻繁重分類或依賴非經常項目,是盈利質量差的信號。"],[39,4,"4","市盈率(P/E)的計算是:","每股市價 ÷ 每股盈利","每股盈利 ÷ 股價","股息 ÷ 股價","市值 ÷ 收入","A","P/E 反映市場對盈利的估值倍數,須結合增長與風險解讀。"],[40,4,"4","自由現金流(FCF)通常定義為:","經營現金流減去資本性支出","淨利潤","經營現金流","收入減成本","A","FCF 衡量維持營運與投資後可供分配的現金,是估值與償債能力的基礎。"],[41,5,"5","審計報告「無保留意見」的含義是:","財務報表在所有重大方面按準則真實公平編製","報表完美無誤","無任何風險","公司必然盈利","A","無保留意見是最高保證級別,但仍基於重大性概念,不保證絕對準確。"],[42,5,"5","「持續經營重大不確定性」段落的出現意味着:","審計師認為存在影響持續經營基礎的重大疑問,已在報告中提請注意","公司必將破產","意見為否定","公司虛報","A","強調事項段不改變意見類型,但提示使用者關注重大不確定性。"],[43,5,"5","審計委員會審閱財務報表時的重點不包括:","替核數師簽發審計意見","重大會計判斷與估計、非標準交易、關聯交易及持續經營假設","重分類與非經常項目","監管新規對披露的影響","A","審計意見由核數師簽發;審計委員會的職責是審閱報表質素、判斷與與核數師溝通。"],[44,5,"5","內部監控在財務匯報流程的目標是:","合理保證交易記錄完整準確、防止與偵測錯報及舞弊","保證公司盈利","替代審計","減少稅務","A","內控提供「合理」而非絕對保證,財務匯報內控(ICFR)是審計與治理關注的核心。"],[45,5,"5","公司秘書在財務匯報周期中的典型角色是:","統籌業績公告、年報刊發時間表及與董事會/審計委員會的流程銜接","親自編製賬目","決定審計意見","代替核數師審計","A","賬目由管理層編製、核數師審計,公司秘書負責披露時間表、程序與文件統籌。"],[46,5,"5","管理層討論與分析(MD&A)與財務報表的關係是:","解釋財務表現背後的經營動因、風險與前景,屬非審計但受規則規範的資訊","代替財務報表","屬審計範圍核心","只作宣傳","A","MD&A 須與財務報表一致且不誤導,是理解「數字以外」的關鍵材料。"],[47,5,"5","舞弊性財務匯報的常見手法包括:","提前確認收入、隱藏負債、操縱估計以虛增利潤","增加披露","聘請核數師","支付股息","A","舞弊多圍繞收入時點、減值低估、表外負債等,審計委員會與核數師須重點監察。"],[48,5,"5","審計師對關聯方交易(HKAS 24)的關注主要是:","交易的商業理由、定價公允性及披露完整性","交易金額大小","是否繳稅","是否上市","A","關聯交易可能被用於利益輸送或粉飾報表,須核實商業實質與披露。"],[49,5,"5","「公允價值計量」(HKFRS 13)的三級層次以什麼為基準?","第一級:活躍市場報價;第二級:可觀察輸入值;第三級:不可觀察輸入值","按資產大小","按持有年限","按稅務評定","A","層次反映估計的可靠性,第三級依賴管理層假設,是審計與治理的重點領域。"],[50,5,"5","董事對財務報表的責任是:","董事會對編製真實公平的報表負最終責任,核數師對報表發表獨立意見","核數師負編製責任","公司秘書負最終責任","股東負責","A","編製責任(management/董事)與審計責任(核數師)分離,是審計基礎概念。"],[51,6,"6","加權平均資本成本(WACC)的組成是:","股權成本與債務成本按資本結構加權,債務成本考慮稅盾","只計算股權成本","只計算債務利率","固定10%","A","WACC=E/V×Re + D/V×Rd×(1-t),是投資決策與估值的基本貼現率。"],[52,6,"6","CAPM 模型計算股權成本的公式是:","無風險利率+Beta×市場風險溢價","無風險利率+市場回報","股息率+增長率","淨利 ÷ 股本","A","CAPM 以系統性風險(Beta)定價股權成本,其假設包括有效市場與單一時期等。"],[53,6,"6","股息增長模型(Gordon Model)適用於:","股息穩定增長的成熟企業估值","初創企業","無股息企業","虧損企業","A","P=D1/(r-g),要求 g q[0]); const QMAP = {}; BANK.forEach(q => { QMAP[q[0]] = q; }); const PASS = 50, EXC = 75; let quiz = [], idx = 0, score = 0, answers = [], mode = 'normal'; let ME = null, quizStart = 0; /* ==================== 本地账号系统 ==================== */ const PAPER = 'cgqp_ifai'; const K_ACCOUNTS = 'sunai_accounts_cgqp_ifai'; const K_SESSION = 'sunai_session_cgqp_ifai'; function wrongKey(){ return 'sunai_wrong_cgqp_ifai::' + (ME ? ME.uid : 'guest'); } function recKey(){ return 'sunai_records_cgqp_ifai::' + (ME ? ME.uid : 'guest'); } function lsGet(k, d){ try { const v = localStorage.getItem(k); return v == null ? d : JSON.parse(v); } catch(e){ return d; } } function lsSet(k, v){ try { localStorage.setItem(k, JSON.stringify(v)); } catch(e){} } function val(id){ const el = document.getElementById(id); return el ? el.value : ''; } function esc(s){ const d = document.createElement('div'); d.textContent = s == null ? '' : s; return d.innerHTML; } /* SHA-256:在 file:// 打开时 crypto.subtle 不可用,用它做兜底 */ function sha256hex(msg){ function rotr(x, n){ return (x >>> n) | (x << (32 - n)); } const K = [ 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 ]; let H = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]; const bytes = []; const u = unescape(encodeURIComponent(msg)); for (let i = 0; i < u.length; i++) bytes.push(u.charCodeAt(i)); const l = bytes.length; bytes.push(0x80); while (bytes.length % 64 !== 56) bytes.push(0); const bitLen = l * 8; bytes.push(0, 0, 0, 0, (bitLen >>> 24) & 255, (bitLen >>> 16) & 255, (bitLen >>> 8) & 255, bitLen & 255); const w = new Array(64); for (let i = 0; i < bytes.length; i += 64){ for (let j = 0; j < 16; j++){ w[j] = (bytes[i+j*4] << 24) | (bytes[i+j*4+1] << 16) | (bytes[i+j*4+2] << 8) | bytes[i+j*4+3]; } for (let j = 16; j < 64; j++){ const s0 = rotr(w[j-15],7) ^ rotr(w[j-15],18) ^ (w[j-15] >>> 3); const s1 = rotr(w[j-2],17) ^ rotr(w[j-2],19) ^ (w[j-2] >>> 10); w[j] = (w[j-16] + s0 + w[j-7] + s1) | 0; } let a=H[0],b=H[1],c=H[2],d=H[3],e=H[4],f=H[5],g=H[6],h=H[7]; for (let j = 0; j < 64; j++){ const S1 = rotr(e,6) ^ rotr(e,11) ^ rotr(e,25); const ch = (e & f) ^ (~e & g); const t1 = (h + S1 + ch + K[j] + w[j]) | 0; const S0 = rotr(a,2) ^ rotr(a,13) ^ rotr(a,22); const maj = (a & b) ^ (a & c) ^ (b & c); const t2 = (S0 + maj) | 0; h=g; g=f; f=e; e=(d+t1)|0; d=c; c=b; b=a; a=(t1+t2)|0; } H = [(H[0]+a)|0,(H[1]+b)|0,(H[2]+c)|0,(H[3]+d)|0,(H[4]+e)|0,(H[5]+f)|0,(H[6]+g)|0,(H[7]+h)|0]; } let out = ''; for (let i = 0; i < 8; i++) out += ('00000000' + (H[i] >>> 0).toString(16)).slice(-8); return out; } function bytesToHex(b){ let s = ''; for (let i = 0; i < b.length; i++) s += ('0' + b[i].toString(16)).slice(-2); return s; } function hexToBytes(h){ const o = new Uint8Array(h.length / 2); for (let i = 0; i < o.length; i++) o[i] = parseInt(h.substr(i*2, 2), 16); return o; } function randSalt(){ const b = new Uint8Array(16); if (window.crypto && crypto.getRandomValues) crypto.getRandomValues(b); else for (let i = 0; i < 16; i++) b[i] = Math.floor(Math.random() * 256); return bytesToHex(b); } /* 有 WebCrypto 时用 PBKDF2(10万次),否则用迭代 SHA-256 */ async function makeHash(pwd, salt, algo){ if (algo !== 'sha256x3000' && window.crypto && crypto.subtle && window.TextEncoder){ try { const enc = new TextEncoder(); const key = await crypto.subtle.importKey('raw', enc.encode(pwd), 'PBKDF2', false, ['deriveBits']); const bits = await crypto.subtle.deriveBits( { name: 'PBKDF2', salt: hexToBytes(salt), iterations: 100000, hash: 'SHA-256' }, key, 256); return { algo: 'pbkdf2', hash: bytesToHex(new Uint8Array(bits)) }; } catch(e) { /* 落到下面 */ } } let h = salt + '|' + pwd; for (let i = 0; i < 3000; i++) h = sha256hex(h); return { algo: 'sha256x3000', hash: h }; } function getAccounts(){ return lsGet(K_ACCOUNTS, []); } function saveAccounts(a){ lsSet(K_ACCOUNTS, a); } function showMsg(id, text, kind){ const el = document.getElementById(id); if (!el) return; el.className = 'auth-msg show ' + (kind || ''); el.textContent = text; } function hideMsg(id){ const el = document.getElementById(id); if (el) el.className = 'auth-msg'; } function authMsg(text, kind){ const onReg = document.getElementById('rg-box').style.display !== 'none'; showMsg(onReg ? 'rg-msg' : 'lg-msg', text, kind); } async function doRegister(){ const login = val('rg-login').trim(); const p1 = val('rg-pwd'), p2 = val('rg-pwd2'); if (!login) return authMsg('請填寫登入帳號', 'bad'); if (login.length < 3) return authMsg('帳號至少 3 個字元', 'bad'); if (!p1) return authMsg('請設定密碼', 'bad'); if (p1.length < 4) return authMsg('密碼至少 4 位', 'bad'); if (p1 !== p2) return authMsg('兩次輸入的密碼不一致', 'bad'); const accs = getAccounts(); if (accs.some(a => a.login.toLowerCase() === login.toLowerCase())) return authMsg('此帳號已被使用,請換一個或直接登入', 'bad'); const salt = randSalt(); const h = await makeHash(p1, salt); const acc = { uid: 'u_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 7), login: login, salt: salt, algo: h.algo, hash: h.hash, name: login, cls: '', createdAt: Date.now() }; accs.push(acc); saveAccounts(accs); loginAs(acc); } async function doLogin(){ const login = val('lg-login').trim(); const pwd = val('lg-pwd'); if (!login || !pwd) return showMsg('lg-msg', '請輸入帳號與密碼', 'bad'); const acc = getAccounts().find(a => a.login.toLowerCase() === login.toLowerCase()); if (!acc) return showMsg('lg-msg', '帳號或密碼不正確', 'bad'); const h = await makeHash(pwd, acc.salt, acc.algo); if (h.hash !== acc.hash) return showMsg('lg-msg', '帳號或密碼不正確', 'bad'); loginAs(acc); } function loginAs(acc){ ME = acc; lsSet(K_SESSION, acc.uid); // 首次登入时,继承旧版(未分账号)的错题本 const legacy = lsGet('sunai_wrong_cgqp_ifai', []); if (legacy && legacy.length){ const mine = loadWrong(); if (!mine.length) saveWrong(legacy.slice()); } hideMsg('lg-msg'); hideMsg('rg-msg'); document.getElementById('lg-pwd').value = ''; document.getElementById('rg-pwd').value = ''; document.getElementById('rg-pwd2').value = ''; renderUserBar(); goHome(); } function doLogout(){ if (!confirm('確定登出?登出後可用其他帳號登入。')) return; ME = null; try { localStorage.removeItem(K_SESSION); } catch(e){} renderUserBar(); document.getElementById('lg-login').value = ''; document.getElementById('lg-pwd').value = ''; show('sc-auth'); } function renderUserBar(){ const bar = document.getElementById('userbar'); if (!bar) return; if (ME){ bar.style.display = 'flex'; document.getElementById('ub-name').textContent = ME.name || ME.login; document.getElementById('ub-cls').textContent = ME.cls ? '(' + ME.cls + ')' : ''; } else { bar.style.display = 'none'; } } function updateMe(){ if (!ME) return; document.getElementById('me-name').value = ME.name || ''; document.getElementById('me-class').value = ME.cls || ''; document.getElementById('me-login').value = ME.login || ''; hideMsg('me-msg'); hideMsg('pw-msg'); document.getElementById('pw-old').value = ''; document.getElementById('pw-new').value = ''; } function saveProfile(){ const accs = getAccounts(); const acc = accs.find(a => a.uid === ME.uid); if (!acc) return showMsg('me-msg', '找不到帳號,請重新登入', 'bad'); acc.name = val('me-name').trim() || acc.login; acc.cls = val('me-class').trim(); saveAccounts(accs); ME = acc; renderUserBar(); showMsg('me-msg', '已儲存', 'good'); } async function changePwd(){ const o = val('pw-old'), n = val('pw-new'); if (!o || !n) return showMsg('pw-msg', '請填寫目前密碼與新密碼', 'bad'); if (n.length < 4) return showMsg('pw-msg', '新密碼至少 4 位', 'bad'); const accs = getAccounts(); const acc = accs.find(a => a.uid === ME.uid); if (!acc) return showMsg('pw-msg', '找不到帳號,請重新登入', 'bad'); const ho = await makeHash(o, acc.salt, acc.algo); if (ho.hash !== acc.hash) return showMsg('pw-msg', '目前密碼不正確', 'bad'); const salt = randSalt(); const hn = await makeHash(n, salt); acc.salt = salt; acc.algo = hn.algo; acc.hash = hn.hash; saveAccounts(accs); ME = acc; document.getElementById('pw-old').value = ''; document.getElementById('pw-new').value = ''; showMsg('pw-msg', '密碼已修改', 'good'); } /* ==================== 错题本 / 记录(按账号) ==================== */ function loadWrong(){ return lsGet(wrongKey(), []); } function wcKey(){ return 'sunai_wrongcnt_' + PAPER + '::' + (ME ? ME.uid : 'guest'); } function loadWC(){ return lsGet(wcKey(), {}); } function saveWC(c){ lsSet(wcKey(), c); } function bumpWrong(id){ const c = loadWC(); c[id] = (c[id] || 0) + 1; saveWC(c); } function clearWrongCnt(id){ const c = loadWC(); if (c[id]) { delete c[id]; saveWC(c); } } function loadFreq(min){ const c = loadWC(); return Object.keys(c) .filter(id => QMAP[id] && c[id] >= min) .sort((a, b) => c[b] - c[a]).map(id => +id); } function saveWrong(w){ lsSet(wrongKey(), w); } function loadRecords(){ return lsGet(recKey(), []); } function addRecord(rec){ const r = loadRecords(); r.unshift(rec); if (r.length > 300) r.length = 300; lsSet(recKey(), r); } /* ==================== 異議回報 ==================== */ function rpKey(){ return 'sunai_reports_' + PAPER + '::' + (ME ? ME.uid : 'guest'); } function loadReports(){ return lsGet(rpKey(), []); } function toggleReport(){ const box = document.getElementById('rp-box'); const show = box.style.display === 'none'; box.style.display = show ? 'block' : 'none'; document.getElementById('rp-msg').style.display = 'none'; document.getElementById('rp-text').value = ''; } function submitReport(){ const q = quiz[idx]; const reason = document.getElementById('rp-text').value.trim(); if (!reason) { alert('請先填寫你的疑問。'); return; } const a = answers[answers.length - 1]; const r = loadReports(); r.push({ paper: PAPER, qid: q.id, ch: q.ch, ref: q.ref, q: q.q.slice(0, 120), myAns: a ? a.pick : '-', correctAns: q.ans, reason: reason.slice(0, 500), at: Date.now() }); lsSet(rpKey(), r); document.getElementById('rp-msg').style.display = 'block'; document.getElementById('rp-msg').textContent = '✅ 已記錄你的回報,感謝!你可以在「異議清單」中一次過傳送給我們。'; setTimeout(()=>{ document.getElementById('rp-box').style.display = 'none'; }, 1800); } function reportTextAll(){ const r = loadReports(); if (!r.length) return ''; return '【IIQE 題目異議回報 - ' + PAPER + '】\n' + r.map((x, i) => (i + 1) + '. 題號#' + x.qid + '(第' + x.ch + '章 ' + (x.ref || '') + ')\n' + '題目:' + x.q + '…\n' + '我的答案:' + x.myAns + '|現有答案:' + x.correctAns + '\n' + '疑問:' + x.reason ).join('\n\n'); } function copyReports(){ const txt = reportTextAll(); if (!txt) { alert('目前沒有異議回報。'); return; } (navigator.clipboard ? navigator.clipboard.writeText(txt) : Promise.reject()) .then(()=>alert('已複製,可貼上給我們。')) .catch(()=>{ const ta = document.createElement('textarea'); ta.value = txt; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); alert('已複製,可貼上給我們。'); }); } function waReports(){ const txt = reportTextAll(); if (!txt) { alert('目前沒有異議回報。'); return; } window.open('https://wa.me/85268751658?text=' + encodeURIComponent(txt), '_blank'); } function openReports(){ const r = loadReports(); const box = document.getElementById('rp-list'); document.getElementById('rp-n').textContent = r.length; document.getElementById('rp-copy').disabled = r.length === 0; document.getElementById('rp-wa').disabled = r.length === 0; box.innerHTML = r.length ? r.slice().reverse().map(x => { const d = new Date(x.at); const pad = n => (n < 10 ? '0' + n : '' + n); return '
' + '
題號#' + x.qid + '(第' + x.ch + '章 ' + (x.ref || '') + ')
' + '
' + esc(x.q) + '…
' + '
我的答案 ' + x.myAns + '|現有答案 ' + x.correctAns + '
' + '
💬 ' + esc(x.reason) + '
' + '
' + d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + '
' + '
'; }).join('') : '
目前沒有異議回報。
'; show('sc-reports'); } function clearWrongBank(){ if (!confirm('確定清空錯題本?此動作無法復原。')) return; saveWrong([]); openWrong(); } function openWrong(){ const w = loadWrong(); document.getElementById('wb-n').textContent = w.length; document.getElementById('wb-redo').disabled = w.length === 0; const fq = loadFreq(2); const wf = document.getElementById('wb-freq'); if (wf) wf.disabled = fq.length === 0; const box = document.getElementById('wb-list'); if (!w.length){ box.innerHTML = '
目前沒有錯題,繼續保持!
'; } else { box.innerHTML = w.map(id => { const q = QMAP[id]; if (!q) return ''; const ai = ['A','B','C','D'].indexOf(q[8]); const at = ai >= 0 ? q[4 + ai] : ''; const wc = loadWC()[id] || 1; const hot = wc >= 2 ? '❌ 錯 ' + wc + ' 次 ' : ''; return '
' + '
' + hot + esc(q[3].slice(0, 90)) + (q[3].length > 90 ? '…' : '') + '
' + '
正確答案:' + q[8] + '. ' + esc(at) + '
' + (q[9] ? '
💡 ' + esc(q[9]) + '
' : '') + '
參考章節 ' + esc(q[2] || '—') + '
' + '
'; }).join(''); } show('sc-wrong'); } function openRecords(){ const r = loadRecords(); const box = document.getElementById('rc-list'); if (!r.length){ box.innerHTML = '
還沒有練習記錄,先做一份模擬練習吧!
'; } else { box.innerHTML = r.map(x => { const d = new Date(x.at); const pad = n => (n < 10 ? '0' + n : '' + n); const when = d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); const tone = x.pct >= EXC ? 'var(--ok)' : (x.pct >= PASS ? 'var(--navy2)' : 'var(--bad)'); return '
' + '
' + x.mode + '' + '' + x.pct + '% ' + x.grade + '
' + '
答對 ' + x.correct + ' / ' + x.total + ' 題 · ' + when + '
' + '
'; }).join(''); } show('sc-records'); } function openMe(){ updateMe(); show('sc-me'); } /* ==================== 首页 / 抽题 / 答题 ==================== */ function refreshHome(){ document.getElementById('st-bank').textContent = BANK.length; const w = loadWrong(); document.getElementById('wrong-n').textContent = w.length; document.getElementById('btn-wrong').disabled = w.length === 0; const fn = loadFreq(2); const fe = document.getElementById('freq-n'); if (fe) fe.textContent = fn.length; const bf = document.getElementById('btn-freq'); if (bf) bf.disabled = fn.length === 0; const rn = loadReports().length; const rh = document.getElementById('rp-home-n'); if (rh) rh.textContent = rn; renderUserBar(); } let COUNT = 100; document.querySelectorAll('#seg-count button').forEach(b=>{ b.onclick = ()=>{ document.querySelectorAll('#seg-count button').forEach(x=>x.classList.remove('on')); b.classList.add('on'); COUNT = b.dataset.n === 'all' ? BANK.length : +b.dataset.n; }; }); (function(){ const ab = document.querySelector('#seg-count button[data-n="all"]'); if(ab) ab.textContent = '全部題庫 ' + BANK.length + ' 題'; })(); document.querySelectorAll('#seg-auth button').forEach(b=>{ b.onclick = ()=>{ document.querySelectorAll('#seg-auth button').forEach(x=>x.classList.remove('on')); b.classList.add('on'); const isLogin = b.dataset.tab === 'login'; document.getElementById('lg-box').style.display = isLogin ? 'block' : 'none'; document.getElementById('rg-box').style.display = isLogin ? 'none' : 'block'; hideMsg('lg-msg'); hideMsg('rg-msg'); }; }); document.addEventListener('keydown', function(e){ if (e.key !== 'Enter') return; if (document.getElementById('sc-auth').classList.contains('on')){ if (document.getElementById('rg-box').style.display === 'none') doLogin(); else doRegister(); } }); function show(id){ document.querySelectorAll('.screen').forEach(s=>s.classList.remove('on')); document.getElementById('sc-'+id.replace('sc-','')).classList.add('on'); window.scrollTo(0,0); } function goHome(){ refreshHome(); document.querySelectorAll('.screen').forEach(s=>s.classList.remove('on')); document.getElementById('sc-home').classList.add('on'); window.scrollTo(0,0); } function pickQuestions(){ const w = loadWrong(); if(mode === 'freq'){ const set = new Set(loadFreq(2)); return BANK.filter(q=>set.has(q[0])).sort((a,b)=>{ const c = loadWC(); return (c[b.id]||0)-(c[a.id]||0); }).map(q=>{ const letters = ['A','B','C','D']; const opts = [q[4],q[5],q[6],q[7]]; const ansIdx = letters.indexOf(q[8]); const idxs = [0,1,2,3].sort(()=>Math.random()-.5); const newOpts = idxs.map(j=>opts[j]); const newAns = letters[idxs.indexOf(ansIdx)]; return {id:q[0], ch:q[1], ref:q[2], q:q[3], opts:newOpts, ans:newAns, expl:q[9]}; }); } if(mode === 'wrong'){ const set = new Set(w); return BANK.filter(q=>set.has(q[0])).sort(()=>Math.random()-.5).map(q=>{ const letters = ['A','B','C','D']; const opts = [q[4],q[5],q[6],q[7]]; const ansIdx = letters.indexOf(q[8]); const idxs = [0,1,2,3].sort(()=>Math.random()-.5); const newOpts = idxs.map(j=>opts[j]); const newAns = letters[idxs.indexOf(ansIdx)]; return {id:q[0], ch:q[1], ref:q[2], q:q[3], opts:newOpts, ans:newAns, expl:q[9]}; }); } const byCh = {}; BANK.forEach(q=>{ (byCh[q[1]] = byCh[q[1]]||[]).push(q); }); Object.values(byCh).forEach(a=>a.sort(()=>Math.random()-.5)); const total = BANK.length; const picks = []; const counts = {}; let assigned = 0; for(const ch in byCh){ counts[ch] = Math.floor(byCh[ch].length/total*COUNT); assigned += counts[ch]; } let rem = COUNT - assigned; const order = Object.keys(byCh).sort((a,b)=>byCh[b].length-byCh[a].length); let i = 0; while(rem > 0){ counts[order[i%order.length]]++; rem--; i++; } for(const ch in counts){ counts[ch] = Math.min(counts[ch], byCh[ch].length); picks.push(...byCh[ch].slice(0, counts[ch])); } if(picks.length < COUNT){ const used = new Set(picks.map(q=>q[0])); for(const q of BANK){ if(picks.length>=COUNT) break; if(!used.has(q[0])) picks.push(q); } } return picks.sort(()=>Math.random()-.5).map(q=>{ const letters = ['A','B','C','D']; const opts = [q[4],q[5],q[6],q[7]]; const ansIdx = letters.indexOf(q[8]); const idxs = [0,1,2,3].sort(()=>Math.random()-.5); const newOpts = idxs.map(j=>opts[j]); const newAns = letters[idxs.indexOf(ansIdx)]; return {id:q[0], ch:q[1], ref:q[2], q:q[3], opts:newOpts, ans:newAns, expl:q[9]}; }); } function startQuiz(isWrong, isFreq){ if(!ME){ show('sc-auth'); return; } mode = isWrong ? 'wrong' : (isFreq ? 'freq' : 'normal'); quiz = pickQuestions(); if(!quiz.length){ alert(mode==='wrong' ? '錯題庫是空的,先去練習吧!' : (mode==='freq' ? '暫時沒有常錯題(答錯 2 次以上才會收錄)。' : '沒有可用題目')); return; } idx = 0; score = 0; answers = []; quizStart = Date.now(); document.getElementById('q-mode').textContent = mode==='wrong' ? '錯題重做' : (mode==='freq' ? '🔥 常錯題重做' : '模擬練習'); document.querySelectorAll('.screen').forEach(s=>s.classList.remove('on')); document.getElementById('sc-quiz').classList.add('on'); window.scrollTo(0,0); renderQ(); } let curLocked = false; function renderQ(){ const q = quiz[idx]; curLocked = false; document.getElementById('q-idx').textContent = '第 ' + (idx+1) + ' / ' + quiz.length + ' 題'; document.getElementById('pbar').style.width = (idx/quiz.length*100)+'%'; document.getElementById('q-text').textContent = q.q; const box = document.getElementById('q-opts'); box.innerHTML = ''; ['A','B','C','D'].forEach(L=>{ const b = document.createElement('button'); b.className = 'opt'; b.innerHTML = '' + L + '.' + esc(q.opts[['A','B','C','D'].indexOf(L)]) + ''; b.onclick = ()=>answer(L, b); box.appendChild(b); }); const ex = document.getElementById('q-expl'); ex.className = 'expl'; ex.innerHTML = ''; document.getElementById('btn-next').disabled = true; document.getElementById('btn-next').textContent = (idx === quiz.length-1) ? '查看成績 🏁' : '下一題 →'; } function answer(L, btn){ if(curLocked) return; curLocked = true; const q = quiz[idx]; const correct = (L === q.ans); const btns = document.querySelectorAll('#q-opts .opt'); btns.forEach(b=>{ b.disabled = true; }); const ansIdx = ['A','B','C','D'].indexOf(q.ans); btns[ansIdx].classList.add('correct'); if(!correct){ btn.classList.add('wrong'); } btns.forEach(b=>{ if(b!==btns[ansIdx] && b!==btn) b.classList.add('dim'); }); if(correct){ score++; clearWrongCnt(q.id); } else { const w = loadWrong(); if(!w.includes(q.id)){ w.push(q.id); saveWrong(w); } bumpWrong(q.id); } answers.push({q, pick:L, correct}); const ex = document.getElementById('q-expl'); ex.className = 'expl show ' + (correct ? 'good' : 'bad'); const expl = q.expl && q.expl.length > 3 ? q.expl : '建議重溫本章節課程內容,加深理解。'; ex.innerHTML = (correct ? '\u2705 答對了!' : '\u274C 答錯了。正確答案:' + q.ans + '
') + '
💡 ' + esc(expl) + '
' + '參考章節 ' + esc(q.ref||'—') + ''; document.getElementById('btn-next').disabled = false; } function nextQ(){ if(idx < quiz.length-1){ idx++; renderQ(); } else showResult(); } function quitQuiz(){ if(confirm('確定離開本次練習?作答紀錄將不會保存。')) goHome(); } function showResult(){ const pct = Math.round(score/quiz.length*100); document.getElementById('score-num').textContent = pct; const ring = document.getElementById('ring'); ring.style.background = 'conic-gradient(' + (pct>=PASS?'var(--navy2)':'var(--bad)') + ' ' + (pct*3.6) + 'deg, #ece7db 0deg)'; const g = document.getElementById('grade-badge'); if(pct >= EXC){ g.className='grade exc'; g.textContent='🏆 優秀'; } else if(pct >= PASS){ g.className='grade pass'; g.textContent='\u2705 合格'; } else { g.className='grade fail'; g.textContent='\u274C 不合格'; } document.getElementById('score-detail').textContent = '答對 ' + score + ' / ' + quiz.length + ' 題(合格線 ' + PASS + '%,優秀線 ' + EXC + '%)'; const byCh = {}; answers.forEach(a=>{ const ch = a.q.ch; byCh[ch] = byCh[ch]||[0,0]; byCh[ch][1]++; if(a.correct) byCh[ch][0]++; }); document.getElementById('ch-stats').innerHTML = Object.keys(byCh).sort((a,b)=>a-b).map(ch=>{ const c = byCh[ch][0], t = byCh[ch][1]; return '
第 ' + ch + ' 章:答對 ' + c + ' / ' + t + ' 題(' + Math.round(c/t*100) + '%)
'; }).join(''); const wrongs = answers.filter(a=>!a.correct); document.getElementById('wrong-review').innerHTML = wrongs.length ? wrongs.map(a=> '
答錯' + esc(a.q.q.slice(0,60)) + '…
' + '正確答案:' + a.q.ans + '. ' + esc(a.q.opts[['A','B','C','D'].indexOf(a.q.ans)].slice(0,50)) + '
' ).join('') : '
🎉 全部答對,沒有錯題!
'; if(mode === 'wrong'){ let w = loadWrong(); const corrected = answers.filter(a=>a.correct).map(a=>a.q.id); w = w.filter(id=>!corrected.includes(id)); saveWrong(w); } if(ME){ const gradeTxt = pct >= EXC ? '優秀' : (pct >= PASS ? '合格' : '不合格'); addRecord({ at: Date.now(), mode: mode === 'wrong' ? '錯題重做' : ('模擬練習 ' + quiz.length + ' 題'), total: quiz.length, correct: score, pct: pct, grade: gradeTxt, sec: Math.round((Date.now() - quizStart)/1000) }); } document.querySelectorAll('.screen').forEach(s=>s.classList.remove('on')); document.getElementById('sc-result').classList.add('on'); window.scrollTo(0,0); } /* ==================== 启动 ==================== */ (function init(){ const sid = lsGet(K_SESSION, null); if (sid){ const acc = getAccounts().find(a => a.uid === sid); if (acc) ME = acc; } renderUserBar(); if (ME){ document.querySelectorAll('.screen').forEach(s=>s.classList.remove('on')); document.getElementById('sc-home').classList.add('on'); refreshHome(); } else { document.querySelectorAll('.screen').forEach(s=>s.classList.remove('on')); document.getElementById('sc-auth').classList.add('on'); } })(); refreshHome();